diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4b62441..feecf19 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,13 +9,15 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 with: - submodules: true + submodules: recursive - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: - dotnet-version: '7.x.x' + dotnet-version: | + 10.x.x + 9.x.x - name: Restore dependencies run: dotnet restore - name: Download Dalamud @@ -37,7 +39,7 @@ jobs: - name: Archive run: Compress-Archive -Path Glamourer/bin/Release/* -DestinationPath Glamourer.zip - name: Upload a Build Artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v4 with: path: | ./Glamourer/bin/Release/* diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index b3123e0..5639c7b 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -9,13 +9,15 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 with: - submodules: true + submodules: recursive - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: - dotnet-version: '7.x.x' + dotnet-version: | + 10.x.x + 9.x.x - name: Restore dependencies run: dotnet restore - name: Download Dalamud @@ -37,7 +39,7 @@ jobs: - name: Archive run: Compress-Archive -Path Glamourer/bin/Debug/* -DestinationPath Glamourer.zip - name: Upload a Build Artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v4 with: path: | ./Glamourer/bin/Debug/* diff --git a/.gitmodules b/.gitmodules index f74e14d..137c8ba 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,16 +1,20 @@ [submodule "OtterGui"] path = OtterGui - url = git@github.com:Ottermandias/OtterGui.git + url = https://github.com/Ottermandias/OtterGui.git branch = main [submodule "Penumbra.GameData"] path = Penumbra.GameData - url = git@github.com:Ottermandias/Penumbra.GameData.git + url = https://github.com/Ottermandias/Penumbra.GameData.git branch = main [submodule "Penumbra.String"] path = Penumbra.String - url = git@github.com:Ottermandias/Penumbra.String.git + url = https://github.com/Ottermandias/Penumbra.String.git branch = main [submodule "Penumbra.Api"] path = Penumbra.Api - url = git@github.com:Ottermandias/Penumbra.Api.git + url = https://github.com/Ottermandias/Penumbra.Api.git + branch = main +[submodule "Glamourer.Api"] + path = Glamourer.Api + url = https://github.com/Ottermandias/Glamourer.Api.git branch = main diff --git a/Glamourer.Api b/Glamourer.Api new file mode 160000 index 0000000..5b6730d --- /dev/null +++ b/Glamourer.Api @@ -0,0 +1 @@ +Subproject commit 5b6730d46f17bdd02a441e23e2141576cf7acf53 diff --git a/Glamourer.GameData/Customization/CharaMakeParams.cs b/Glamourer.GameData/Customization/CharaMakeParams.cs deleted file mode 100644 index 63806c0..0000000 --- a/Glamourer.GameData/Customization/CharaMakeParams.cs +++ /dev/null @@ -1,126 +0,0 @@ -using Lumina.Data; -using Lumina.Excel; -using Lumina.Excel.GeneratedSheets; - -namespace Glamourer.Customization; - -// A custom version of CharaMakeParams that is easier to parse. -[Sheet("CharaMakeParams")] -public class CharaMakeParams : ExcelRow -{ - public const int NumMenus = 28; - public const int NumVoices = 12; - public const int NumGraphics = 10; - public const int MaxNumValues = 100; - public const int NumFaces = 8; - public const int NumFeatures = 7; - public const int NumEquip = 3; - - public enum MenuType - { - ListSelector = 0, - IconSelector = 1, - ColorPicker = 2, - DoubleColorPicker = 3, - IconCheckmark = 4, - Percentage = 5, - Checkmark = 6, // custom - Nothing = 7, // custom - List1Selector = 8, // custom, 1-indexed lists - } - - public struct Menu - { - public uint Id; - public byte InitVal; - public MenuType Type; - public byte Size; - public byte LookAt; - public uint Mask; - public uint Customize; - public uint[] Values; - public byte[] Graphic; - } - - public struct FacialFeatures - { - public uint[] Icons; - } - - public LazyRow Race { get; set; } = null!; - public LazyRow Tribe { get; set; } = null!; - public sbyte Gender { get; set; } - - public Menu[] Menus { get; set; } = new Menu[NumMenus]; - public byte[] Voices { get; set; } = new byte[NumVoices]; - public FacialFeatures[] FacialFeatureByFace { get; set; } = new FacialFeatures[NumFaces]; - - public CharaMakeType.CharaMakeTypeUnkData3347Obj[] Equip { get; set; } = new CharaMakeType.CharaMakeTypeUnkData3347Obj[NumEquip]; - - public override void PopulateData(RowParser parser, Lumina.GameData gameData, Language language) - { - RowId = parser.RowId; - SubRowId = parser.SubRowId; - Race = new LazyRow(gameData, parser.ReadColumn(0), language); - Tribe = new LazyRow(gameData, parser.ReadColumn(1), language); - Gender = parser.ReadColumn(2); - var currentOffset = 0; - for (var i = 0; i < NumMenus; ++i) - { - currentOffset = 3 + i; - Menus[i].Id = parser.ReadColumn(0 * NumMenus + currentOffset); - Menus[i].InitVal = parser.ReadColumn(1 * NumMenus + currentOffset); - Menus[i].Type = (MenuType)parser.ReadColumn(2 * NumMenus + currentOffset); - Menus[i].Size = parser.ReadColumn(3 * NumMenus + currentOffset); - Menus[i].LookAt = parser.ReadColumn(4 * NumMenus + currentOffset); - Menus[i].Mask = parser.ReadColumn(5 * NumMenus + currentOffset); - Menus[i].Customize = parser.ReadColumn(6 * NumMenus + currentOffset); - Menus[i].Values = new uint[Menus[i].Size]; - - switch (Menus[i].Type) - { - case MenuType.ColorPicker: - case MenuType.DoubleColorPicker: - case MenuType.Percentage: - break; - default: - currentOffset += 7 * NumMenus; - for (var j = 0; j < Menus[i].Size; ++j) - Menus[i].Values[j] = parser.ReadColumn(j * NumMenus + currentOffset); - break; - } - - Menus[i].Graphic = new byte[NumGraphics]; - currentOffset = 3 + (MaxNumValues + 7) * NumMenus + i; - for (var j = 0; j < NumGraphics; ++j) - Menus[i].Graphic[j] = parser.ReadColumn(j * NumMenus + currentOffset); - } - - currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus; - for (var i = 0; i < NumVoices; ++i) - Voices[i] = parser.ReadColumn(currentOffset++); - - for (var i = 0; i < NumFaces; ++i) - { - currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + i; - FacialFeatureByFace[i].Icons = new uint[NumFeatures]; - for (var j = 0; j < NumFeatures; ++j) - FacialFeatureByFace[i].Icons[j] = (uint)parser.ReadColumn(j * NumFaces + currentOffset); - } - - for (var i = 0; i < NumEquip; ++i) - { - currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7; - Equip[i] = new CharaMakeType.CharaMakeTypeUnkData3347Obj() - { - Helmet = parser.ReadColumn(currentOffset + 0), - Top = parser.ReadColumn(currentOffset + 1), - Gloves = parser.ReadColumn(currentOffset + 2), - Legs = parser.ReadColumn(currentOffset + 3), - Shoes = parser.ReadColumn(currentOffset + 4), - Weapon = parser.ReadColumn(currentOffset + 5), - SubWeapon = parser.ReadColumn(currentOffset + 6), - }; - } - } -} diff --git a/Glamourer.GameData/Customization/CmpFile.cs b/Glamourer.GameData/Customization/CmpFile.cs deleted file mode 100644 index d62e5da..0000000 --- a/Glamourer.GameData/Customization/CmpFile.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Logging; -using Dalamud.Plugin.Services; - -namespace Glamourer.Customization; - -// Convert the Human.Cmp file into color sets. -// If the file can not be read due to TexTools corruption, create a 0-array of size MinSize. -internal class CmpFile -{ - private readonly Lumina.Data.FileResource? _file; - private readonly uint[] _rgbaColors; - - // No error checking since only called internally. - public IEnumerable GetSlice(int offset, int count) - => _rgbaColors.Length >= offset + count ? _rgbaColors.Skip(offset).Take(count) : Enumerable.Repeat(0u, count); - - public bool Valid - => _file != null; - - public CmpFile(IDataManager gameData, IPluginLog log) - { - try - { - _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) - { - _rgbaColors[i >> 2] = _file.Data[i] - | (uint)(_file.Data[i + 1] << 8) - | (uint)(_file.Data[i + 2] << 16) - | (uint)(_file.Data[i + 3] << 24); - } - } - catch (Exception e) - { - log.Error("READ THIS\n======== Could not obtain the human.cmp file which is necessary for color sets.\n" - + "======== This usually indicates an error with your index files caused by TexTools modifications.\n" - + "======== If you have used TexTools before, you will probably need to start over in it to use Glamourer.", e); - _file = null; - _rgbaColors = Array.Empty(); - } - } -} diff --git a/Glamourer.GameData/Customization/CustomName.cs b/Glamourer.GameData/Customization/CustomName.cs deleted file mode 100644 index 6bff835..0000000 --- a/Glamourer.GameData/Customization/CustomName.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace Glamourer.Customization; - -// Localization from the game files directly. -public enum CustomName -{ - Clan = 0, - Gender, - Reverse, - OddEyes, - IrisSmall, - IrisLarge, - IrisSize, - MidlanderM, - HighlanderM, - WildwoodM, - DuskwightM, - PlainsfolkM, - DunesfolkM, - SeekerOfTheSunM, - KeeperOfTheMoonM, - SeawolfM, - HellsguardM, - RaenM, - XaelaM, - HelionM, - LostM, - RavaM, - VeenaM, - MidlanderF, - HighlanderF, - WildwoodF, - DuskwightF, - PlainsfolkF, - DunesfolkF, - SeekerOfTheSunF, - KeeperOfTheMoonF, - SeawolfF, - HellsguardF, - RaenF, - XaelaF, - HelionF, - LostF, - RavaF, - VeenaF, -} diff --git a/Glamourer.GameData/Customization/CustomizationManager.cs b/Glamourer.GameData/Customization/CustomizationManager.cs deleted file mode 100644 index 9221838..0000000 --- a/Glamourer.GameData/Customization/CustomizationManager.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Services; -using Penumbra.GameData.Enums; - -namespace Glamourer.Customization; - -public class CustomizationManager : ICustomizationManager -{ - private static CustomizationOptions? _options; - - private CustomizationManager() - { } - - public static ICustomizationManager Create(ITextureProvider textures, IDataManager gameData, IPluginLog log) - { - _options ??= new CustomizationOptions(textures, gameData, log); - return new CustomizationManager(); - } - - public IReadOnlyList Races - => CustomizationOptions.Races; - - public IReadOnlyList Clans - => CustomizationOptions.Clans; - - public IReadOnlyList Genders - => CustomizationOptions.Genders; - - public CustomizationSet GetList(SubRace clan, Gender gender) - => _options!.GetList(clan, gender); - - public IDalamudTextureWrap GetIcon(uint iconId) - => _options!.GetIcon(iconId); - - public string GetName(CustomName name) - => _options!.GetName(name); -} diff --git a/Glamourer.GameData/Customization/CustomizationNpcOptions.cs b/Glamourer.GameData/Customization/CustomizationNpcOptions.cs deleted file mode 100644 index 3cd988b..0000000 --- a/Glamourer.GameData/Customization/CustomizationNpcOptions.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Lumina.Excel; -using Lumina.Excel.GeneratedSheets; -using OtterGui; -using Penumbra.GameData.Enums; - -namespace Glamourer.Customization; - -public static class CustomizationNpcOptions -{ - public static Dictionary<(SubRace, Gender), IReadOnlyList<(CustomizeIndex, CustomizeValue)>> CreateNpcData(CustomizationSet[] sets, - ExcelSheet bNpc, ExcelSheet eNpc) - { - var customizes = bNpc.SelectWhere(FromBnpcCustomize) - .Concat(eNpc.SelectWhere(FromEnpcBase)).ToList(); - - var dict = new Dictionary<(SubRace, Gender), HashSet<(CustomizeIndex, CustomizeValue)>>(); - var customizeIndices = new[] - { - CustomizeIndex.Face, - CustomizeIndex.Hairstyle, - CustomizeIndex.LipColor, - CustomizeIndex.SkinColor, - CustomizeIndex.FacePaintColor, - CustomizeIndex.HighlightsColor, - CustomizeIndex.HairColor, - CustomizeIndex.FacePaint, - CustomizeIndex.TattooColor, - CustomizeIndex.EyeColorLeft, - CustomizeIndex.EyeColorRight, - }; - - foreach (var customize in customizes) - { - var set = sets[CustomizationOptions.ToIndex(customize.Clan, customize.Gender)]; - foreach (var customizeIndex in customizeIndices) - { - var value = customize[customizeIndex]; - if (value == CustomizeValue.Zero) - continue; - - if (set.DataByValue(customizeIndex, value, out _, customize.Face) >= 0) - continue; - - if (!dict.TryGetValue((set.Clan, set.Gender), out var npcSet)) - { - npcSet = new HashSet<(CustomizeIndex, CustomizeValue)> { (customizeIndex, value) }; - dict.Add((set.Clan, set.Gender), npcSet); - } - else - { - npcSet.Add((customizeIndex, value)); - } - } - } - - return dict.ToDictionary(kvp => kvp.Key, - kvp => (IReadOnlyList<(CustomizeIndex, CustomizeValue)>)kvp.Value.OrderBy(p => p.Item1).ThenBy(p => p.Item2.Value).ToArray()); - } - - private static (bool, Customize) FromBnpcCustomize(BNpcCustomize bnpcCustomize) - { - var customize = new Customize(); - customize.Data.Set(0, (byte)bnpcCustomize.Race.Row); - customize.Data.Set(1, bnpcCustomize.Gender); - customize.Data.Set(2, bnpcCustomize.BodyType); - customize.Data.Set(3, bnpcCustomize.Height); - customize.Data.Set(4, (byte)bnpcCustomize.Tribe.Row); - customize.Data.Set(5, bnpcCustomize.Face); - customize.Data.Set(6, bnpcCustomize.HairStyle); - customize.Data.Set(7, bnpcCustomize.HairHighlight); - customize.Data.Set(8, bnpcCustomize.SkinColor); - customize.Data.Set(9, bnpcCustomize.EyeHeterochromia); - customize.Data.Set(10, bnpcCustomize.HairColor); - customize.Data.Set(11, bnpcCustomize.HairHighlightColor); - customize.Data.Set(12, bnpcCustomize.FacialFeature); - customize.Data.Set(13, bnpcCustomize.FacialFeatureColor); - customize.Data.Set(14, bnpcCustomize.Eyebrows); - customize.Data.Set(15, bnpcCustomize.EyeColor); - customize.Data.Set(16, bnpcCustomize.EyeShape); - customize.Data.Set(17, bnpcCustomize.Nose); - customize.Data.Set(18, bnpcCustomize.Jaw); - customize.Data.Set(19, bnpcCustomize.Mouth); - customize.Data.Set(20, bnpcCustomize.LipColor); - customize.Data.Set(21, bnpcCustomize.BustOrTone1); - customize.Data.Set(22, bnpcCustomize.ExtraFeature1); - customize.Data.Set(23, bnpcCustomize.ExtraFeature2OrBust); - customize.Data.Set(24, bnpcCustomize.FacePaint); - customize.Data.Set(25, bnpcCustomize.FacePaintColor); - - if (customize.BodyType.Value != 1 - || !CustomizationOptions.Races.Contains(customize.Race) - || !CustomizationOptions.Clans.Contains(customize.Clan) - || !CustomizationOptions.Genders.Contains(customize.Gender)) - return (false, Customize.Default); - - return (true, customize); - } - - private static (bool, Customize) FromEnpcBase(ENpcBase enpcBase) - { - if (enpcBase.ModelChara.Value?.Type != 1) - return (false, Customize.Default); - - var customize = new Customize(); - customize.Data.Set(0, (byte)enpcBase.Race.Row); - customize.Data.Set(1, enpcBase.Gender); - customize.Data.Set(2, enpcBase.BodyType); - customize.Data.Set(3, enpcBase.Height); - customize.Data.Set(4, (byte)enpcBase.Tribe.Row); - customize.Data.Set(5, enpcBase.Face); - customize.Data.Set(6, enpcBase.HairStyle); - customize.Data.Set(7, enpcBase.HairHighlight); - customize.Data.Set(8, enpcBase.SkinColor); - customize.Data.Set(9, enpcBase.EyeHeterochromia); - customize.Data.Set(10, enpcBase.HairColor); - customize.Data.Set(11, enpcBase.HairHighlightColor); - customize.Data.Set(12, enpcBase.FacialFeature); - customize.Data.Set(13, enpcBase.FacialFeatureColor); - customize.Data.Set(14, enpcBase.Eyebrows); - customize.Data.Set(15, enpcBase.EyeColor); - customize.Data.Set(16, enpcBase.EyeShape); - customize.Data.Set(17, enpcBase.Nose); - customize.Data.Set(18, enpcBase.Jaw); - customize.Data.Set(19, enpcBase.Mouth); - customize.Data.Set(20, enpcBase.LipColor); - customize.Data.Set(21, enpcBase.BustOrTone1); - customize.Data.Set(22, enpcBase.ExtraFeature1); - customize.Data.Set(23, enpcBase.ExtraFeature2OrBust); - customize.Data.Set(24, enpcBase.FacePaint); - customize.Data.Set(25, enpcBase.FacePaintColor); - - if (customize.BodyType.Value != 1 - || !CustomizationOptions.Races.Contains(customize.Race) - || !CustomizationOptions.Clans.Contains(customize.Clan) - || !CustomizationOptions.Genders.Contains(customize.Gender)) - return (false, Customize.Default); - - return (true, customize); - } -} diff --git a/Glamourer.GameData/Customization/CustomizationOptions.cs b/Glamourer.GameData/Customization/CustomizationOptions.cs deleted file mode 100644 index a3e04f5..0000000 --- a/Glamourer.GameData/Customization/CustomizationOptions.cs +++ /dev/null @@ -1,527 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Dalamud; -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Services; -using Dalamud.Utility; -using Lumina.Excel; -using Lumina.Excel.GeneratedSheets; -using OtterGui.Classes; -using Penumbra.GameData.Enums; -using Race = Penumbra.GameData.Enums.Race; - -namespace Glamourer.Customization; - -// Generate everything about customization per tribe and gender. -public partial class CustomizationOptions -{ - // All races except for Unknown - internal static readonly Race[] Races = ((Race[])Enum.GetValues(typeof(Race))).Skip(1).ToArray(); - - // All tribes except for Unknown - internal static readonly SubRace[] Clans = ((SubRace[])Enum.GetValues(typeof(SubRace))).Skip(1).ToArray(); - - // Two genders. - internal static readonly Gender[] Genders = - { - Gender.Male, - Gender.Female, - }; - - // Every tribe and gender has a separate set of available customizations. - internal CustomizationSet GetList(SubRace race, Gender gender) - => _customizationSets[ToIndex(race, gender)]; - - // Get specific icons. - internal IDalamudTextureWrap GetIcon(uint id) - => _icons.LoadIcon(id)!; - - private readonly IconStorage _icons; - - private static readonly int ListSize = Clans.Length * Genders.Length; - private readonly CustomizationSet[] _customizationSets = new CustomizationSet[ListSize]; - - - // Get the index for the given pair of tribe and gender. - internal static int ToIndex(SubRace race, Gender gender) - { - var idx = ((int)race - 1) * Genders.Length + (gender == Gender.Female ? 1 : 0); - if (idx < 0 || idx >= ListSize) - ThrowException(race, gender); - return idx; - } - - private static void ThrowException(SubRace race, Gender gender) - => throw new Exception($"Invalid customization requested for {race} {gender}."); -} - -public partial class CustomizationOptions -{ - public string GetName(CustomName name) - => _names[(int)name]; - - internal CustomizationOptions(ITextureProvider textures, IDataManager gameData, IPluginLog log) - { - var tmp = new TemporaryData(gameData, this, log); - _icons = new IconStorage(textures, gameData); - SetNames(gameData, tmp); - foreach (var race in Clans) - { - foreach (var gender in Genders) - _customizationSets[ToIndex(race, gender)] = tmp.GetSet(race, gender); - } - tmp.SetNpcData(_customizationSets); - } - - // Obtain localized names of customization options and race names from the game data. - private readonly string[] _names = new string[Enum.GetValues().Length]; - - private void SetNames(IDataManager gameData, TemporaryData tmp) - { - var subRace = gameData.GetExcelSheet()!; - - void Set(CustomName id, Lumina.Text.SeString? s, string def) - => _names[(int)id] = s?.ToDalamudString().TextValue ?? def; - - Set(CustomName.Clan, tmp.Lobby.GetRow(102)?.Text, "Clan"); - Set(CustomName.Gender, tmp.Lobby.GetRow(103)?.Text, "Gender"); - Set(CustomName.Reverse, tmp.Lobby.GetRow(2135)?.Text, "Reverse"); - Set(CustomName.OddEyes, tmp.Lobby.GetRow(2125)?.Text, "Odd Eyes"); - Set(CustomName.IrisSmall, tmp.Lobby.GetRow(1076)?.Text, "Small"); - Set(CustomName.IrisLarge, tmp.Lobby.GetRow(1075)?.Text, "Large"); - Set(CustomName.IrisSize, tmp.Lobby.GetRow(244)?.Text, "Iris Size"); - Set(CustomName.MidlanderM, subRace.GetRow((int)SubRace.Midlander)?.Masculine, SubRace.Midlander.ToName()); - Set(CustomName.MidlanderF, subRace.GetRow((int)SubRace.Midlander)?.Feminine, SubRace.Midlander.ToName()); - Set(CustomName.HighlanderM, subRace.GetRow((int)SubRace.Highlander)?.Masculine, SubRace.Highlander.ToName()); - Set(CustomName.HighlanderF, subRace.GetRow((int)SubRace.Highlander)?.Feminine, SubRace.Highlander.ToName()); - Set(CustomName.WildwoodM, subRace.GetRow((int)SubRace.Wildwood)?.Masculine, SubRace.Wildwood.ToName()); - Set(CustomName.WildwoodF, subRace.GetRow((int)SubRace.Wildwood)?.Feminine, SubRace.Wildwood.ToName()); - Set(CustomName.DuskwightM, subRace.GetRow((int)SubRace.Duskwight)?.Masculine, SubRace.Duskwight.ToName()); - Set(CustomName.DuskwightF, subRace.GetRow((int)SubRace.Duskwight)?.Feminine, SubRace.Duskwight.ToName()); - Set(CustomName.PlainsfolkM, subRace.GetRow((int)SubRace.Plainsfolk)?.Masculine, SubRace.Plainsfolk.ToName()); - Set(CustomName.PlainsfolkF, subRace.GetRow((int)SubRace.Plainsfolk)?.Feminine, SubRace.Plainsfolk.ToName()); - Set(CustomName.DunesfolkM, subRace.GetRow((int)SubRace.Dunesfolk)?.Masculine, SubRace.Dunesfolk.ToName()); - Set(CustomName.DunesfolkF, subRace.GetRow((int)SubRace.Dunesfolk)?.Feminine, SubRace.Dunesfolk.ToName()); - Set(CustomName.SeekerOfTheSunM, subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Masculine, SubRace.SeekerOfTheSun.ToName()); - Set(CustomName.SeekerOfTheSunF, subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Feminine, SubRace.SeekerOfTheSun.ToName()); - Set(CustomName.KeeperOfTheMoonM, subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Masculine, SubRace.KeeperOfTheMoon.ToName()); - Set(CustomName.KeeperOfTheMoonF, subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Feminine, SubRace.KeeperOfTheMoon.ToName()); - Set(CustomName.SeawolfM, subRace.GetRow((int)SubRace.Seawolf)?.Masculine, SubRace.Seawolf.ToName()); - Set(CustomName.SeawolfF, subRace.GetRow((int)SubRace.Seawolf)?.Feminine, SubRace.Seawolf.ToName()); - Set(CustomName.HellsguardM, subRace.GetRow((int)SubRace.Hellsguard)?.Masculine, SubRace.Hellsguard.ToName()); - Set(CustomName.HellsguardF, subRace.GetRow((int)SubRace.Hellsguard)?.Feminine, SubRace.Hellsguard.ToName()); - Set(CustomName.RaenM, subRace.GetRow((int)SubRace.Raen)?.Masculine, SubRace.Raen.ToName()); - Set(CustomName.RaenF, subRace.GetRow((int)SubRace.Raen)?.Feminine, SubRace.Raen.ToName()); - Set(CustomName.XaelaM, subRace.GetRow((int)SubRace.Xaela)?.Masculine, SubRace.Xaela.ToName()); - Set(CustomName.XaelaF, subRace.GetRow((int)SubRace.Xaela)?.Feminine, SubRace.Xaela.ToName()); - Set(CustomName.HelionM, subRace.GetRow((int)SubRace.Helion)?.Masculine, SubRace.Helion.ToName()); - Set(CustomName.HelionF, subRace.GetRow((int)SubRace.Helion)?.Feminine, SubRace.Helion.ToName()); - Set(CustomName.LostM, subRace.GetRow((int)SubRace.Lost)?.Masculine, SubRace.Lost.ToName()); - Set(CustomName.LostF, subRace.GetRow((int)SubRace.Lost)?.Feminine, SubRace.Lost.ToName()); - Set(CustomName.RavaM, subRace.GetRow((int)SubRace.Rava)?.Masculine, SubRace.Rava.ToName()); - Set(CustomName.RavaF, subRace.GetRow((int)SubRace.Rava)?.Feminine, SubRace.Rava.ToName()); - Set(CustomName.VeenaM, subRace.GetRow((int)SubRace.Veena)?.Masculine, SubRace.Veena.ToName()); - Set(CustomName.VeenaF, subRace.GetRow((int)SubRace.Veena)?.Feminine, SubRace.Veena.ToName()); - } - - private class TemporaryData - { - public bool Valid - => _cmpFile.Valid; - - public CustomizationSet GetSet(SubRace race, Gender gender) - { - var (skin, hair) = GetColors(race, gender); - var row = _listSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; - var hrothgar = race.ToRace() == Race.Hrothgar; - // Create the initial set with all the easily accessible parameters available for anyone. - var set = new CustomizationSet(race, gender) - { - Voices = row.Voices, - HairStyles = GetHairStyles(race, gender), - HairColors = hair, - SkinColors = skin, - EyeColors = _eyeColorPicker, - HighlightColors = _highlightPicker, - TattooColors = _tattooColorPicker, - LipColorsDark = hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark, - LipColorsLight = hrothgar ? Array.Empty() : _lipColorPickerLight, - FacePaintColorsDark = _facePaintColorPickerDark, - FacePaintColorsLight = _facePaintColorPickerLight, - Faces = GetFaces(row), - NumEyebrows = GetListSize(row, CustomizeIndex.Eyebrows), - NumEyeShapes = GetListSize(row, CustomizeIndex.EyeShape), - NumNoseShapes = GetListSize(row, CustomizeIndex.Nose), - NumJawShapes = GetListSize(row, CustomizeIndex.Jaw), - NumMouthShapes = GetListSize(row, CustomizeIndex.Mouth), - FacePaints = GetFacePaints(race, gender), - TailEarShapes = GetTailEarShapes(row), - }; - - SetAvailability(set, row); - SetFacialFeatures(set, row); - SetHairByFace(set); - SetMenuTypes(set, row); - SetNames(set, row); - - return set; - } - - public void SetNpcData(CustomizationSet[] sets) - { - var data = CustomizationNpcOptions.CreateNpcData(sets, _bnpcCustomize, _enpcBase); - foreach (var set in sets) - { - if (data.TryGetValue((set.Clan, set.Gender), out var npcData)) - set.NpcOptions = npcData.ToArray(); - } - } - - - public TemporaryData(IDataManager gameData, CustomizationOptions options, IPluginLog log) - { - _options = options; - _cmpFile = new CmpFile(gameData, log); - _customizeSheet = gameData.GetExcelSheet()!; - _bnpcCustomize = gameData.GetExcelSheet()!; - _enpcBase = 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", - gameData.Language.ToLumina(), - null, - }) as ExcelSheet; - _listSheet = tmp!; - _hairSheet = gameData.GetExcelSheet()!; - _highlightPicker = CreateColorPicker(CustomizeIndex.HighlightsColor, 256, 192); - _lipColorPickerDark = CreateColorPicker(CustomizeIndex.LipColor, 512, 96); - _lipColorPickerLight = CreateColorPicker(CustomizeIndex.LipColor, 1024, 96, true); - _eyeColorPicker = CreateColorPicker(CustomizeIndex.EyeColorLeft, 0, 192); - _facePaintColorPickerDark = CreateColorPicker(CustomizeIndex.FacePaintColor, 640, 96); - _facePaintColorPickerLight = CreateColorPicker(CustomizeIndex.FacePaintColor, 1152, 96, true); - _tattooColorPicker = CreateColorPicker(CustomizeIndex.TattooColor, 0, 192); - } - - // Required sheets. - private readonly ExcelSheet _customizeSheet; - private readonly ExcelSheet _listSheet; - private readonly ExcelSheet _hairSheet; - private readonly ExcelSheet _bnpcCustomize; - private readonly ExcelSheet _enpcBase; - public readonly ExcelSheet Lobby; - private readonly CmpFile _cmpFile; - - // Those values are shared between all races. - private readonly CustomizeData[] _highlightPicker; - private readonly CustomizeData[] _eyeColorPicker; - private readonly CustomizeData[] _facePaintColorPickerDark; - private readonly CustomizeData[] _facePaintColorPickerLight; - private readonly CustomizeData[] _lipColorPickerDark; - private readonly CustomizeData[] _lipColorPickerLight; - private readonly CustomizeData[] _tattooColorPicker; - - private readonly CustomizationOptions _options; - - - private CustomizeData[] CreateColorPicker(CustomizeIndex index, int offset, int num, bool light = false) - => _cmpFile.GetSlice(offset, num) - .Select((c, i) => new CustomizeData(index, (CustomizeValue)(light ? 128 + i : 0 + i), c, (ushort)(offset + i))) - .ToArray(); - - - private void SetHairByFace(CustomizationSet set) - { - if (set.Race != Race.Hrothgar) - { - set.HairByFace = Enumerable.Repeat(set.HairStyles, set.Faces.Count + 1).ToArray(); - return; - } - - var tmp = new IReadOnlyList[set.Faces.Count + 1]; - tmp[0] = set.HairStyles; - - for (var i = 1; i <= set.Faces.Count; ++i) - { - bool Valid(CustomizeData c) - { - var data = _customizeSheet.GetRow(c.CustomizeId)?.Unknown6 ?? 0; - return data == 0 || data == i + set.Faces.Count; - } - - tmp[i] = set.HairStyles.Where(Valid).ToArray(); - } - - set.HairByFace = tmp; - } - - private static void SetMenuTypes(CustomizationSet set, CharaMakeParams row) - { - // Set up the menu types for all customizations. - set.Types = Enum.GetValues().Select(c => - { - // Those types are not correctly given in the menu, so special case them to color pickers. - switch (c) - { - case CustomizeIndex.HighlightsColor: - case CustomizeIndex.EyeColorLeft: - case CustomizeIndex.EyeColorRight: - case CustomizeIndex.FacePaintColor: - return CharaMakeParams.MenuType.ColorPicker; - case CustomizeIndex.BodyType: return CharaMakeParams.MenuType.Nothing; - case CustomizeIndex.FacePaintReversed: - case CustomizeIndex.Highlights: - case CustomizeIndex.SmallIris: - case CustomizeIndex.Lipstick: - return CharaMakeParams.MenuType.Checkmark; - case CustomizeIndex.FacialFeature1: - case CustomizeIndex.FacialFeature2: - case CustomizeIndex.FacialFeature3: - case CustomizeIndex.FacialFeature4: - case CustomizeIndex.FacialFeature5: - case CustomizeIndex.FacialFeature6: - case CustomizeIndex.FacialFeature7: - case CustomizeIndex.LegacyTattoo: - return CharaMakeParams.MenuType.IconCheckmark; - } - - var gameId = c.ToByteAndMask().ByteIdx; - // Otherwise find the first menu corresponding to the id. - // If there is none, assume a list. - var menu = row.Menus - .Cast() - .FirstOrDefault(m => m!.Value.Customize == gameId); - var ret = menu?.Type ?? CharaMakeParams.MenuType.ListSelector; - if (c is CustomizeIndex.TailShape && ret is CharaMakeParams.MenuType.ListSelector) - ret = CharaMakeParams.MenuType.List1Selector; - return ret; - }).ToArray(); - set.Order = CustomizationSet.ComputeOrder(set); - } - - // Set customizations available if they have any options. - private static void SetAvailability(CustomizationSet set, CharaMakeParams row) - { - if (set.Race == Race.Hrothgar && set.Gender == Gender.Female) - return; - - void Set(bool available, CustomizeIndex flag) - { - if (available) - set.SetAvailable(flag); - } - - Set(true, CustomizeIndex.Height); - Set(set.Faces.Count > 0, CustomizeIndex.Face); - Set(true, CustomizeIndex.Hairstyle); - Set(true, CustomizeIndex.Highlights); - Set(true, CustomizeIndex.SkinColor); - Set(true, CustomizeIndex.EyeColorRight); - Set(true, CustomizeIndex.HairColor); - Set(true, CustomizeIndex.HighlightsColor); - Set(true, CustomizeIndex.TattooColor); - Set(set.NumEyebrows > 0, CustomizeIndex.Eyebrows); - Set(true, CustomizeIndex.EyeColorLeft); - Set(set.NumEyeShapes > 0, CustomizeIndex.EyeShape); - Set(set.NumNoseShapes > 0, CustomizeIndex.Nose); - Set(set.NumJawShapes > 0, CustomizeIndex.Jaw); - Set(set.NumMouthShapes > 0, CustomizeIndex.Mouth); - Set(set.LipColorsDark.Count > 0, CustomizeIndex.LipColor); - Set(GetListSize(row, CustomizeIndex.MuscleMass) > 0, CustomizeIndex.MuscleMass); - Set(set.TailEarShapes.Count > 0, CustomizeIndex.TailShape); - Set(GetListSize(row, CustomizeIndex.BustSize) > 0, CustomizeIndex.BustSize); - Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaint); - Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaintColor); - Set(true, CustomizeIndex.FacialFeature1); - Set(true, CustomizeIndex.FacialFeature2); - Set(true, CustomizeIndex.FacialFeature3); - Set(true, CustomizeIndex.FacialFeature4); - Set(true, CustomizeIndex.FacialFeature5); - Set(true, CustomizeIndex.FacialFeature6); - Set(true, CustomizeIndex.FacialFeature7); - Set(true, CustomizeIndex.LegacyTattoo); - Set(true, CustomizeIndex.SmallIris); - Set(set.Race != Race.Hrothgar, CustomizeIndex.Lipstick); - Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaintReversed); - } - - // Create a list of lists of facial features and the legacy tattoo. - private static void SetFacialFeatures(CustomizationSet set, CharaMakeParams row) - { - var count = set.Faces.Count; - set.FacialFeature1 = new List<(CustomizeData, CustomizeData)>(count); - - static (CustomizeData, CustomizeData) Create(CustomizeIndex i, uint data) - => (new CustomizeData(i, CustomizeValue.Zero, data, 0), new CustomizeData(i, CustomizeValue.Max, data, 1)); - - set.LegacyTattoo = Create(CustomizeIndex.LegacyTattoo, 137905); - - var tmp = Enumerable.Repeat(0, 7).Select(_ => new (CustomizeData, CustomizeData)[count + 1]).ToArray(); - for (var i = 0; i < count; ++i) - { - var data = row.FacialFeatureByFace[i].Icons; - tmp[0][i + 1] = Create(CustomizeIndex.FacialFeature1, data[0]); - tmp[1][i + 1] = Create(CustomizeIndex.FacialFeature2, data[1]); - tmp[2][i + 1] = Create(CustomizeIndex.FacialFeature3, data[2]); - tmp[3][i + 1] = Create(CustomizeIndex.FacialFeature4, data[3]); - tmp[4][i + 1] = Create(CustomizeIndex.FacialFeature5, data[4]); - tmp[5][i + 1] = Create(CustomizeIndex.FacialFeature6, data[5]); - tmp[6][i + 1] = Create(CustomizeIndex.FacialFeature7, data[6]); - } - - set.FacialFeature1 = tmp[0]; - set.FacialFeature2 = tmp[1]; - set.FacialFeature3 = tmp[2]; - set.FacialFeature4 = tmp[3]; - set.FacialFeature5 = tmp[4]; - set.FacialFeature6 = tmp[5]; - set.FacialFeature7 = tmp[6]; - } - - // Set the names for the given set of parameters. - private void SetNames(CustomizationSet set, CharaMakeParams row) - { - var nameArray = Enum.GetValues().Select(c => - { - // Find the first menu that corresponds to the Id. - var byteId = c.ToByteAndMask().ByteIdx; - var menu = row.Menus - .Cast() - .FirstOrDefault(m => m!.Value.Customize == byteId); - if (menu == null) - { - // If none exists and the id corresponds to highlights, set the Highlights name. - if (c == CustomizeIndex.Highlights) - return Lobby.GetRow(237)?.Text.ToDalamudString().ToString() ?? "Highlights"; - - // Otherwise there is an error and we use the default name. - return c.ToDefaultName(); - } - - // Facial Features and Tattoos is created by combining two strings. - if (c is >= CustomizeIndex.FacialFeature1 and <= CustomizeIndex.LegacyTattoo) - return - $"{Lobby.GetRow(1741)?.Text.ToDalamudString().ToString() ?? "Facial Features"} & {Lobby.GetRow(1742)?.Text.ToDalamudString().ToString() ?? "Tattoos"}"; - - // Otherwise all is normal, get the menu name or if it does not work the default name. - var textRow = Lobby.GetRow(menu.Value.Id); - return textRow?.Text.ToDalamudString().ToString() ?? c.ToDefaultName(); - }).ToArray(); - - // Add names for both eye colors. - nameArray[(int)CustomizeIndex.EyeColorLeft] = nameArray[(int)CustomizeIndex.EyeColorRight]; - nameArray[(int)CustomizeIndex.EyeColorRight] = _options.GetName(CustomName.OddEyes); - - set.OptionName = nameArray; - } - - // Obtain available skin and hair colors for the given subrace and gender. - private (CustomizeData[], CustomizeData[]) GetColors(SubRace race, Gender gender) - { - if (race is > SubRace.Veena or SubRace.Unknown) - throw new ArgumentOutOfRangeException(nameof(race), race, null); - - var gv = gender == Gender.Male ? 0 : 1; - var idx = ((int)race * 2 + gv) * 5 + 3; - - return (CreateColorPicker(CustomizeIndex.SkinColor, idx << 8, 192), - CreateColorPicker(CustomizeIndex.HairColor, (idx + 1) << 8, 192)); - } - - // Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender. - private CustomizeData[] GetHairStyles(SubRace race, Gender gender) - { - var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; - // Unknown30 is the number of available hairstyles. - var hairList = new List(row.Unknown30); - // Hairstyles can be found starting at Unknown66. - for (var i = 0; i < row.Unknown30; ++i) - { - var name = $"Unknown{66 + i * 9}"; - var customizeIdx = (uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row) - ?? uint.MaxValue; - if (customizeIdx == uint.MaxValue) - continue; - - // Hair Row from CustomizeSheet might not be set in case of unlockable hair. - var hairRow = _customizeSheet.GetRow(customizeIdx); - if (hairRow == null) - hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)i, customizeIdx)); - else if (_options._icons.IconExists(hairRow.Icon)) - hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon, - (ushort)hairRow.RowId)); - } - - return hairList.OrderBy(h => h.Value.Value).ToArray(); - } - - // Get Features. - private CustomizeData FromValueAndIndex(CustomizeIndex id, uint value, int index) - { - var row = _customizeSheet.GetRow(value); - return row == null - ? new CustomizeData(id, (CustomizeValue)(index + 1), value) - : new CustomizeData(id, (CustomizeValue)row.FeatureID, row.Icon, (ushort)row.RowId); - } - - // Get List sizes. - private static int GetListSize(CharaMakeParams row, CustomizeIndex index) - { - var gameId = index.ToByteAndMask().ByteIdx; - var menu = row.Menus.Cast().FirstOrDefault(m => m!.Value.Customize == gameId); - return menu?.Size ?? 0; - } - - // Get face paints from the hair sheet via reflection. - private CustomizeData[] GetFacePaints(SubRace race, Gender gender) - { - var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; - var paintList = new List(row.Unknown37); - // Number of available face paints is at Unknown37. - for (var i = 0; i < row.Unknown37; ++i) - { - // Face paints start at Unknown73. - var name = $"Unknown{73 + i * 9}"; - var customizeIdx = - (uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row) - ?? uint.MaxValue; - if (customizeIdx == uint.MaxValue) - continue; - - var paintRow = _customizeSheet.GetRow(customizeIdx); - // Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints. - if (paintRow != null) - paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon, - (ushort)paintRow.RowId)); - else - paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)i, customizeIdx)); - } - - return paintList.OrderBy(p => p.Value.Value).ToArray(); - } - - // Specific icons for tails or ears. - private CustomizeData[] GetTailEarShapes(CharaMakeParams row) - => row.Menus.Cast() - .FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.TailShape.ToByteAndMask().ByteIdx)?.Values - .Select((v, i) => FromValueAndIndex(CustomizeIndex.TailShape, v, i)).ToArray() - ?? Array.Empty(); - - // Specific icons for faces. - private CustomizeData[] GetFaces(CharaMakeParams row) - => row.Menus.Cast().FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.Face.ToByteAndMask().ByteIdx) - ?.Values - .Select((v, i) => FromValueAndIndex(CustomizeIndex.Face, v, i)).ToArray() - ?? Array.Empty(); - - // Specific icons for Hrothgar patterns. - private CustomizeData[] HrothgarFurPattern(CharaMakeParams row) - => row.Menus.Cast() - .FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.LipColor.ToByteAndMask().ByteIdx)?.Values - .Select((v, i) => FromValueAndIndex(CustomizeIndex.LipColor, v, i)).ToArray() - ?? Array.Empty(); - } -} diff --git a/Glamourer.GameData/Customization/Customize.cs b/Glamourer.GameData/Customization/Customize.cs deleted file mode 100644 index 90dac63..0000000 --- a/Glamourer.GameData/Customization/Customize.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using Penumbra.GameData.Enums; - -namespace Glamourer.Customization; - -public unsafe struct Customize -{ - public Penumbra.GameData.Structs.CustomizeData Data; - - public Customize(in Penumbra.GameData.Structs.CustomizeData data) - { - Data = data.Clone(); - } - - public Race Race - { - get => (Race)Data.Get(CustomizeIndex.Race).Value; - set => Data.Set(CustomizeIndex.Race, (CustomizeValue)(byte)value); - } - - public Gender Gender - { - get => (Gender)Data.Get(CustomizeIndex.Gender).Value + 1; - set => Data.Set(CustomizeIndex.Gender, (CustomizeValue)(byte)value - 1); - } - - public CustomizeValue BodyType - { - get => Data.Get(CustomizeIndex.BodyType); - set => Data.Set(CustomizeIndex.BodyType, value); - } - - public SubRace Clan - { - get => (SubRace)Data.Get(CustomizeIndex.Clan).Value; - set => Data.Set(CustomizeIndex.Clan, (CustomizeValue)(byte)value); - } - - public CustomizeValue Face - { - get => Data.Get(CustomizeIndex.Face); - set => Data.Set(CustomizeIndex.Face, value); - } - - - public static readonly Customize Default = GenerateDefault(); - public static readonly Customize Empty = new(); - - public CustomizeValue Get(CustomizeIndex index) - => Data.Get(index); - - public bool Set(CustomizeIndex flag, CustomizeValue index) - => Data.Set(flag, index); - - public bool Equals(Customize other) - => Equals(Data, other.Data); - - public CustomizeValue this[CustomizeIndex index] - { - get => Get(index); - set => Set(index, value); - } - - private static Customize GenerateDefault() - { - var ret = new Customize - { - Race = Race.Hyur, - Clan = SubRace.Midlander, - Gender = Gender.Male, - }; - ret.Set(CustomizeIndex.BodyType, (CustomizeValue)1); - ret.Set(CustomizeIndex.Height, (CustomizeValue)50); - ret.Set(CustomizeIndex.Face, (CustomizeValue)1); - ret.Set(CustomizeIndex.Hairstyle, (CustomizeValue)1); - ret.Set(CustomizeIndex.SkinColor, (CustomizeValue)1); - ret.Set(CustomizeIndex.EyeColorRight, (CustomizeValue)1); - ret.Set(CustomizeIndex.HighlightsColor, (CustomizeValue)1); - ret.Set(CustomizeIndex.TattooColor, (CustomizeValue)1); - ret.Set(CustomizeIndex.Eyebrows, (CustomizeValue)1); - ret.Set(CustomizeIndex.EyeColorLeft, (CustomizeValue)1); - ret.Set(CustomizeIndex.EyeShape, (CustomizeValue)1); - ret.Set(CustomizeIndex.Nose, (CustomizeValue)1); - ret.Set(CustomizeIndex.Jaw, (CustomizeValue)1); - ret.Set(CustomizeIndex.Mouth, (CustomizeValue)1); - ret.Set(CustomizeIndex.LipColor, (CustomizeValue)1); - ret.Set(CustomizeIndex.MuscleMass, (CustomizeValue)50); - ret.Set(CustomizeIndex.TailShape, (CustomizeValue)1); - ret.Set(CustomizeIndex.BustSize, (CustomizeValue)50); - ret.Set(CustomizeIndex.FacePaint, (CustomizeValue)1); - ret.Set(CustomizeIndex.FacePaintColor, (CustomizeValue)1); - return ret; - } - - public void Load(Customize other) - => Data.Read(&other.Data); - - public readonly void Write(nint target) - => Data.Write((void*)target); - - public bool LoadBase64(string data) - => Data.LoadBase64(data); - - public readonly string WriteBase64() - => Data.WriteBase64(); - - public static CustomizeFlag Compare(Customize lhs, Customize rhs) - { - CustomizeFlag ret = 0; - foreach (var idx in Enum.GetValues()) - { - var l = lhs[idx]; - var r = rhs[idx]; - if (l.Value != r.Value) - ret |= idx.ToFlag(); - } - - return ret; - } - - public override string ToString() - => Data.ToString(); -} diff --git a/Glamourer.GameData/Customization/CustomizeFlag.cs b/Glamourer.GameData/Customization/CustomizeFlag.cs deleted file mode 100644 index 44b6cee..0000000 --- a/Glamourer.GameData/Customization/CustomizeFlag.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -namespace Glamourer.Customization; - -[Flags] -public enum CustomizeFlag : ulong -{ - Invalid = 0, - Race = 1ul << CustomizeIndex.Race, - Gender = 1ul << CustomizeIndex.Gender, - BodyType = 1ul << CustomizeIndex.BodyType, - Height = 1ul << CustomizeIndex.Height, - Clan = 1ul << CustomizeIndex.Clan, - Face = 1ul << CustomizeIndex.Face, - Hairstyle = 1ul << CustomizeIndex.Hairstyle, - Highlights = 1ul << CustomizeIndex.Highlights, - SkinColor = 1ul << CustomizeIndex.SkinColor, - EyeColorRight = 1ul << CustomizeIndex.EyeColorRight, - HairColor = 1ul << CustomizeIndex.HairColor, - HighlightsColor = 1ul << CustomizeIndex.HighlightsColor, - FacialFeature1 = 1ul << CustomizeIndex.FacialFeature1, - FacialFeature2 = 1ul << CustomizeIndex.FacialFeature2, - FacialFeature3 = 1ul << CustomizeIndex.FacialFeature3, - FacialFeature4 = 1ul << CustomizeIndex.FacialFeature4, - FacialFeature5 = 1ul << CustomizeIndex.FacialFeature5, - FacialFeature6 = 1ul << CustomizeIndex.FacialFeature6, - FacialFeature7 = 1ul << CustomizeIndex.FacialFeature7, - LegacyTattoo = 1ul << CustomizeIndex.LegacyTattoo, - TattooColor = 1ul << CustomizeIndex.TattooColor, - Eyebrows = 1ul << CustomizeIndex.Eyebrows, - EyeColorLeft = 1ul << CustomizeIndex.EyeColorLeft, - EyeShape = 1ul << CustomizeIndex.EyeShape, - SmallIris = 1ul << CustomizeIndex.SmallIris, - Nose = 1ul << CustomizeIndex.Nose, - Jaw = 1ul << CustomizeIndex.Jaw, - Mouth = 1ul << CustomizeIndex.Mouth, - Lipstick = 1ul << CustomizeIndex.Lipstick, - LipColor = 1ul << CustomizeIndex.LipColor, - MuscleMass = 1ul << CustomizeIndex.MuscleMass, - TailShape = 1ul << CustomizeIndex.TailShape, - BustSize = 1ul << CustomizeIndex.BustSize, - FacePaint = 1ul << CustomizeIndex.FacePaint, - FacePaintReversed = 1ul << CustomizeIndex.FacePaintReversed, - FacePaintColor = 1ul << CustomizeIndex.FacePaintColor, -} - -public static class CustomizeFlagExtensions -{ - public const CustomizeFlag All = (CustomizeFlag)(((ulong)CustomizeFlag.FacePaintColor << 1) - 1ul); - public const CustomizeFlag AllRelevant = All & ~CustomizeFlag.BodyType & ~CustomizeFlag.Race; - - public const CustomizeFlag RedrawRequired = - CustomizeFlag.Race | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.Face | CustomizeFlag.BodyType; - - public static CustomizeFlag FixApplication(this CustomizeFlag flag, CustomizationSet set) - => flag & (set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender); - - public static bool RequiresRedraw(this CustomizeFlag flags) - => (flags & RedrawRequired) != 0; - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static CustomizeIndex ToIndex(this CustomizeFlag flag) - => flag switch - { - CustomizeFlag.Race => CustomizeIndex.Race, - CustomizeFlag.Gender => CustomizeIndex.Gender, - CustomizeFlag.BodyType => CustomizeIndex.BodyType, - CustomizeFlag.Height => CustomizeIndex.Height, - CustomizeFlag.Clan => CustomizeIndex.Clan, - CustomizeFlag.Face => CustomizeIndex.Face, - CustomizeFlag.Hairstyle => CustomizeIndex.Hairstyle, - CustomizeFlag.Highlights => CustomizeIndex.Highlights, - CustomizeFlag.SkinColor => CustomizeIndex.SkinColor, - CustomizeFlag.EyeColorRight => CustomizeIndex.EyeColorRight, - CustomizeFlag.HairColor => CustomizeIndex.HairColor, - CustomizeFlag.HighlightsColor => CustomizeIndex.HighlightsColor, - CustomizeFlag.FacialFeature1 => CustomizeIndex.FacialFeature1, - CustomizeFlag.FacialFeature2 => CustomizeIndex.FacialFeature2, - CustomizeFlag.FacialFeature3 => CustomizeIndex.FacialFeature3, - CustomizeFlag.FacialFeature4 => CustomizeIndex.FacialFeature4, - CustomizeFlag.FacialFeature5 => CustomizeIndex.FacialFeature5, - CustomizeFlag.FacialFeature6 => CustomizeIndex.FacialFeature6, - CustomizeFlag.FacialFeature7 => CustomizeIndex.FacialFeature7, - CustomizeFlag.LegacyTattoo => CustomizeIndex.LegacyTattoo, - CustomizeFlag.TattooColor => CustomizeIndex.TattooColor, - CustomizeFlag.Eyebrows => CustomizeIndex.Eyebrows, - CustomizeFlag.EyeColorLeft => CustomizeIndex.EyeColorLeft, - CustomizeFlag.EyeShape => CustomizeIndex.EyeShape, - CustomizeFlag.SmallIris => CustomizeIndex.SmallIris, - CustomizeFlag.Nose => CustomizeIndex.Nose, - CustomizeFlag.Jaw => CustomizeIndex.Jaw, - CustomizeFlag.Mouth => CustomizeIndex.Mouth, - CustomizeFlag.Lipstick => CustomizeIndex.Lipstick, - CustomizeFlag.LipColor => CustomizeIndex.LipColor, - CustomizeFlag.MuscleMass => CustomizeIndex.MuscleMass, - CustomizeFlag.TailShape => CustomizeIndex.TailShape, - CustomizeFlag.BustSize => CustomizeIndex.BustSize, - CustomizeFlag.FacePaint => CustomizeIndex.FacePaint, - CustomizeFlag.FacePaintReversed => CustomizeIndex.FacePaintReversed, - CustomizeFlag.FacePaintColor => CustomizeIndex.FacePaintColor, - _ => (CustomizeIndex)byte.MaxValue, - }; - - public static bool SetIfDifferent(ref this CustomizeFlag flags, CustomizeFlag flag, bool value) - { - var newValue = value ? flags | flag : flags & ~flag; - if (newValue == flags) - return false; - - flags = newValue; - return true; - } -} diff --git a/Glamourer.GameData/Customization/CustomizeIndex.cs b/Glamourer.GameData/Customization/CustomizeIndex.cs deleted file mode 100644 index 1c03dd1..0000000 --- a/Glamourer.GameData/Customization/CustomizeIndex.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Linq; -using System.Runtime.CompilerServices; - -namespace Glamourer.Customization; - -public enum CustomizeIndex : byte -{ - Race, - Gender, - BodyType, - Height, - Clan, - Face, - Hairstyle, - Highlights, - SkinColor, - EyeColorRight, - HairColor, - HighlightsColor, - FacialFeature1, - FacialFeature2, - FacialFeature3, - FacialFeature4, - FacialFeature5, - FacialFeature6, - FacialFeature7, - LegacyTattoo, - TattooColor, - Eyebrows, - EyeColorLeft, - EyeShape, - SmallIris, - Nose, - Jaw, - Mouth, - Lipstick, - LipColor, - MuscleMass, - TailShape, - BustSize, - FacePaint, - FacePaintReversed, - FacePaintColor, -} - -public static class CustomizationExtensions -{ - public const int NumIndices = (int)CustomizeIndex.FacePaintColor + 1; - - public static readonly CustomizeIndex[] All = Enum.GetValues() - .Where(v => v is not CustomizeIndex.Race and not CustomizeIndex.BodyType).ToArray(); - - public static readonly CustomizeIndex[] AllBasic = All - .Where(v => v is not CustomizeIndex.Gender and not CustomizeIndex.Clan).ToArray(); - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static (int ByteIdx, byte Mask) ToByteAndMask(this CustomizeIndex index) - => index switch - { - CustomizeIndex.Race => (0, 0xFF), - CustomizeIndex.Gender => (1, 0xFF), - CustomizeIndex.BodyType => (2, 0xFF), - CustomizeIndex.Height => (3, 0xFF), - CustomizeIndex.Clan => (4, 0xFF), - CustomizeIndex.Face => (5, 0xFF), - CustomizeIndex.Hairstyle => (6, 0xFF), - CustomizeIndex.Highlights => (7, 0x80), - CustomizeIndex.SkinColor => (8, 0xFF), - CustomizeIndex.EyeColorRight => (9, 0xFF), - CustomizeIndex.HairColor => (10, 0xFF), - CustomizeIndex.HighlightsColor => (11, 0xFF), - CustomizeIndex.FacialFeature1 => (12, 0x01), - CustomizeIndex.FacialFeature2 => (12, 0x02), - CustomizeIndex.FacialFeature3 => (12, 0x04), - CustomizeIndex.FacialFeature4 => (12, 0x08), - CustomizeIndex.FacialFeature5 => (12, 0x10), - CustomizeIndex.FacialFeature6 => (12, 0x20), - CustomizeIndex.FacialFeature7 => (12, 0x40), - CustomizeIndex.LegacyTattoo => (12, 0x80), - CustomizeIndex.TattooColor => (13, 0xFF), - CustomizeIndex.Eyebrows => (14, 0xFF), - CustomizeIndex.EyeColorLeft => (15, 0xFF), - CustomizeIndex.EyeShape => (16, 0x7F), - CustomizeIndex.SmallIris => (16, 0x80), - CustomizeIndex.Nose => (17, 0xFF), - CustomizeIndex.Jaw => (18, 0xFF), - CustomizeIndex.Mouth => (19, 0x7F), - CustomizeIndex.Lipstick => (19, 0x80), - CustomizeIndex.LipColor => (20, 0xFF), - CustomizeIndex.MuscleMass => (21, 0xFF), - CustomizeIndex.TailShape => (22, 0xFF), - CustomizeIndex.BustSize => (23, 0xFF), - CustomizeIndex.FacePaint => (24, 0x7F), - CustomizeIndex.FacePaintReversed => (24, 0x80), - CustomizeIndex.FacePaintColor => (25, 0xFF), - _ => (0, 0x00), - }; - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static CustomizeFlag ToFlag(this CustomizeIndex index) - => (CustomizeFlag)(1ul << (int)index); - - public static string ToDefaultName(this CustomizeIndex customizeIndex) - => customizeIndex switch - { - CustomizeIndex.Race => "Race", - CustomizeIndex.Gender => "Gender", - CustomizeIndex.BodyType => "Body Type", - CustomizeIndex.Height => "Height", - CustomizeIndex.Clan => "Clan", - CustomizeIndex.Face => "Head Style", - CustomizeIndex.Hairstyle => "Hair Style", - CustomizeIndex.Highlights => "Highlights", - CustomizeIndex.SkinColor => "Skin Color", - CustomizeIndex.EyeColorRight => "Right Eye Color", - CustomizeIndex.HairColor => "Hair Color", - CustomizeIndex.HighlightsColor => "Highlights Color", - CustomizeIndex.TattooColor => "Tattoo Color", - CustomizeIndex.Eyebrows => "Eyebrow Style", - CustomizeIndex.EyeColorLeft => "Left Eye Color", - CustomizeIndex.EyeShape => "Small Pupils", - CustomizeIndex.Nose => "Nose Style", - CustomizeIndex.Jaw => "Jaw Style", - CustomizeIndex.Mouth => "Mouth Style", - CustomizeIndex.MuscleMass => "Muscle Tone", - CustomizeIndex.TailShape => "Tail Shape", - CustomizeIndex.BustSize => "Bust Size", - CustomizeIndex.FacePaint => "Face Paint", - CustomizeIndex.FacePaintColor => "Face Paint Color", - CustomizeIndex.LipColor => "Lip Color", - CustomizeIndex.FacialFeature1 => "Facial Feature 1", - CustomizeIndex.FacialFeature2 => "Facial Feature 2", - CustomizeIndex.FacialFeature3 => "Facial Feature 3", - CustomizeIndex.FacialFeature4 => "Facial Feature 4", - CustomizeIndex.FacialFeature5 => "Facial Feature 5", - CustomizeIndex.FacialFeature6 => "Facial Feature 6", - CustomizeIndex.FacialFeature7 => "Facial Feature 7", - CustomizeIndex.LegacyTattoo => "Legacy Tattoo", - CustomizeIndex.SmallIris => "Small Iris", - CustomizeIndex.Lipstick => "Enable Lipstick", - CustomizeIndex.FacePaintReversed => "Reverse Face Paint", - _ => string.Empty, - }; - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static unsafe CustomizeValue Get(this in Penumbra.GameData.Structs.CustomizeData data, CustomizeIndex index) - { - var (offset, mask) = index.ToByteAndMask(); - return (CustomizeValue)(data.Data[offset] & mask); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static unsafe bool Set(this ref Penumbra.GameData.Structs.CustomizeData data, CustomizeIndex index, CustomizeValue value) - { - var (offset, mask) = index.ToByteAndMask(); - return mask != 0xFF - ? SetIfDifferentMasked(ref data.Data[offset], value, mask) - : SetIfDifferent(ref data.Data[offset], value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static bool SetIfDifferentMasked(ref byte oldValue, CustomizeValue newValue, byte mask) - { - var tmp = (byte)(newValue.Value & mask); - tmp = (byte)(tmp | (oldValue & ~mask)); - if (oldValue == tmp) - return false; - - oldValue = tmp; - return true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static bool SetIfDifferent(ref byte oldValue, CustomizeValue newValue) - { - if (oldValue == newValue.Value) - return false; - - oldValue = newValue.Value; - return true; - } -} diff --git a/Glamourer.GameData/Customization/CustomizeValue.cs b/Glamourer.GameData/Customization/CustomizeValue.cs deleted file mode 100644 index 25b3406..0000000 --- a/Glamourer.GameData/Customization/CustomizeValue.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace Glamourer.Customization; - -public record struct CustomizeValue(byte Value) -{ - public static readonly CustomizeValue Zero = new(0); - public static readonly CustomizeValue Max = new(0xFF); - - public static CustomizeValue Bool(bool b) - => b ? Max : Zero; - - public static explicit operator CustomizeValue(byte value) - => new(value); - - public static CustomizeValue operator ++(CustomizeValue v) - => new(++v.Value); - - public static CustomizeValue operator --(CustomizeValue v) - => new(--v.Value); - - public static bool operator <(CustomizeValue v, int count) - => v.Value < count; - - public static bool operator >(CustomizeValue v, int count) - => v.Value > count; - - public static CustomizeValue operator +(CustomizeValue v, int rhs) - => new((byte)(v.Value + rhs)); - - public static CustomizeValue operator -(CustomizeValue v, int rhs) - => new((byte)(v.Value - rhs)); - - public override string ToString() - => Value.ToString(); -} diff --git a/Glamourer.GameData/Customization/DatCharacterFile.cs b/Glamourer.GameData/Customization/DatCharacterFile.cs deleted file mode 100644 index 304334a..0000000 --- a/Glamourer.GameData/Customization/DatCharacterFile.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using Dalamud.Memory; - -namespace Glamourer.Customization; - -[StructLayout(LayoutKind.Explicit, Size = Size)] -public unsafe struct DatCharacterFile -{ - public const int Size = 4 + 4 + 4 + 4 + Penumbra.GameData.Structs.CustomizeData.Size + 2 + 4 + 41 * 4; // 212 - - [FieldOffset(0)] - private fixed byte _data[Size]; - - [FieldOffset(0)] - public readonly uint Magic = 0x2013FF14; - - [FieldOffset(4)] - public readonly uint Version = 0x05; - - [FieldOffset(8)] - private uint _checksum; - - [FieldOffset(12)] - private readonly uint _padding = 0; - - [FieldOffset(16)] - private Penumbra.GameData.Structs.CustomizeData _customize; - - [FieldOffset(16 + Penumbra.GameData.Structs.CustomizeData.Size)] - private ushort _voice; - - [FieldOffset(16 + Penumbra.GameData.Structs.CustomizeData.Size + 2)] - private uint _timeStamp; - - [FieldOffset(Size - 41 * 4)] - private fixed byte _description[41 * 4]; - - public readonly void Write(Stream stream) - { - for (var i = 0; i < Size; ++i) - stream.WriteByte(_data[i]); - } - - public static bool Read(Stream stream, out DatCharacterFile file) - { - if (stream.Length - stream.Position != Size) - { - file = default; - return false; - } - - file = new DatCharacterFile(stream); - return true; - } - - private DatCharacterFile(Stream stream) - { - for (var i = 0; i < Size; ++i) - _data[i] = (byte)stream.ReadByte(); - } - - public DatCharacterFile(in Customize customize, byte voice, string text) - { - SetCustomize(customize); - SetVoice(voice); - SetTime(DateTimeOffset.UtcNow); - SetDescription(text); - _checksum = CalculateChecksum(); - } - - public readonly uint CalculateChecksum() - { - var ret = 0u; - for (var i = 16; i < Size; i++) - ret ^= (uint)(_data[i] << ((i - 16) % 24)); - return ret; - } - - public readonly uint Checksum - => _checksum; - - public Customize Customize - { - readonly get => new(_customize); - set - { - SetCustomize(value); - _checksum = CalculateChecksum(); - } - } - - public ushort Voice - { - readonly get => _voice; - set - { - SetVoice(value); - _checksum = CalculateChecksum(); - } - } - - public string Description - { - readonly get - { - fixed (byte* ptr = _description) - { - return MemoryHelper.ReadStringNullTerminated((nint)ptr); - } - } - set - { - SetDescription(value); - _checksum = CalculateChecksum(); - } - } - - public DateTimeOffset Time - { - readonly get => DateTimeOffset.FromUnixTimeSeconds(_timeStamp); - set - { - SetTime(value); - _checksum = CalculateChecksum(); - } - } - - private void SetTime(DateTimeOffset time) - => _timeStamp = (uint)time.ToUnixTimeSeconds(); - - private void SetCustomize(in Customize customize) - => _customize = customize.Data.Clone(); - - private void SetVoice(ushort voice) - => _voice = voice; - - private void SetDescription(string text) - { - fixed (byte* ptr = _description) - { - var span = new Span(ptr, 41 * 4); - Encoding.UTF8.GetBytes(text.AsSpan(0, Math.Min(40, text.Length)), span); - } - } -} diff --git a/Glamourer.GameData/Customization/ICustomizationManager.cs b/Glamourer.GameData/Customization/ICustomizationManager.cs deleted file mode 100644 index e946e3d..0000000 --- a/Glamourer.GameData/Customization/ICustomizationManager.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using Dalamud.Interface.Internal; -using Penumbra.GameData.Enums; - -namespace Glamourer.Customization; - -public interface ICustomizationManager -{ - public IReadOnlyList Races { get; } - public IReadOnlyList Clans { get; } - public IReadOnlyList Genders { get; } - - public CustomizationSet GetList(SubRace race, Gender gender); - - public IDalamudTextureWrap GetIcon(uint iconId); - public string GetName(CustomName name); -} diff --git a/Glamourer.GameData/GameData.cs b/Glamourer.GameData/GameData.cs deleted file mode 100644 index d5638b0..0000000 --- a/Glamourer.GameData/GameData.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Dalamud; -using Dalamud.Plugin.Services; -using Glamourer.Structs; -using Lumina.Excel.GeneratedSheets; - -namespace Glamourer; - -public static class GameData -{ - private static Dictionary? _jobs; - private static Dictionary? _jobGroups; - private static JobGroup[]? _allJobGroups; - - public static IReadOnlyDictionary Jobs(IDataManager dataManager) - { - if (_jobs != null) - return _jobs; - - var sheet = dataManager.GetExcelSheet()!; - _jobs = sheet.Where(j => j.Abbreviation.RawData.Length > 0).ToDictionary(j => (byte)j.RowId, j => new Job(j)); - return _jobs; - } - - public static IReadOnlyList AllJobGroups(IDataManager dataManager) - { - if (_allJobGroups != null) - return _allJobGroups; - - var sheet = dataManager.GetExcelSheet()!; - var jobs = dataManager.GetExcelSheet(ClientLanguage.English)!; - _allJobGroups = sheet.Select(j => new JobGroup(j, jobs)).ToArray(); - return _allJobGroups; - } - - public static IReadOnlyDictionary JobGroups(IDataManager dataManager) - { - if (_jobGroups != null) - return _jobGroups; - - static bool ValidIndex(uint idx) - { - if (idx is > 0 and < 36) - return true; - - return idx switch - { - // Single jobs and big groups - 91 => true, - 92 => true, - 96 => true, - 98 => true, - 99 => true, - 111 => true, - 112 => true, - 129 => true, - 149 => true, - 150 => true, - 156 => true, - 157 => true, - 158 => true, - 159 => true, - 180 => true, - 181 => true, - 188 => true, - 189 => true, - - // Class + Job - 38 => true, - 41 => true, - 44 => true, - 47 => true, - 50 => true, - 53 => true, - 55 => true, - 69 => true, - 68 => true, - 93 => true, - _ => false, - }; - } - - _jobGroups = AllJobGroups(dataManager).Where(j => ValidIndex(j.Id)) - .ToDictionary(j => (ushort) j.Id, j => j); - return _jobGroups; - } -} diff --git a/Glamourer.GameData/Glamourer - Backup.GameData.csproj b/Glamourer.GameData/Glamourer - Backup.GameData.csproj deleted file mode 100644 index 74d1588..0000000 --- a/Glamourer.GameData/Glamourer - Backup.GameData.csproj +++ /dev/null @@ -1,61 +0,0 @@ - - - net472 - preview - Glamourer - Glamourer.GameData - 1.0.0.0 - 1.0.0.0 - SoftOtter - Glamourer - Copyright © 2020 - true - Library - 4 - true - enable - bin\$(Configuration)\ - $(MSBuildWarningsAsMessages);MSB3277 - - - - full - DEBUG;TRACE - - - - pdbonly - - - - $(MSBuildWarningsAsMessages);MSB3277 - - - - - $(DALAMUD_ROOT)\Dalamud.dll - ..\libs\Dalamud.dll - $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll - False - - - $(DALAMUD_ROOT)\Lumina.dll - ..\libs\Lumina.dll - $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll - False - - - $(DALAMUD_ROOT)\Lumina.Excel.dll - ..\libs\Lumina.Excel.dll - $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll - False - - - ..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.GameData.dll - - - - - - - diff --git a/Glamourer.GameData/Glamourer.GameData.csproj b/Glamourer.GameData/Glamourer.GameData.csproj deleted file mode 100644 index 92bf301..0000000 --- a/Glamourer.GameData/Glamourer.GameData.csproj +++ /dev/null @@ -1,72 +0,0 @@ - - - net7.0-windows - preview - x64 - Glamourer - Glamourer.GameData - 1.0.0.0 - 1.0.0.0 - SoftOtter - Glamourer - Copyright © 2020 - true - Library - 4 - true - enable - bin\$(Configuration)\ - $(MSBuildWarningsAsMessages);MSB3277 - false - false - - - - full - DEBUG;TRACE - - - - pdbonly - - - - $(MSBuildWarningsAsMessages);MSB3277 - - - - $(AppData)\XIVLauncher\addon\Hooks\dev\ - - - - - $(DalamudLibPath)Dalamud.dll - False - - - $(DalamudLibPath)FFXIVClientStructs.dll - False - - - $(DalamudLibPath)Lumina.dll - False - - - $(DalamudLibPath)Lumina.Excel.dll - False - - - $(DalamudLibPath)Newtonsoft.Json.dll - False - - - $(DalamudLibPath)ImGuiScene.dll - False - - - - - - - - \ No newline at end of file diff --git a/Glamourer.GameData/Offsets.cs b/Glamourer.GameData/Offsets.cs deleted file mode 100644 index a6b5d1f..0000000 --- a/Glamourer.GameData/Offsets.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Glamourer; - -public static class Offsets -{ - public static class Character - { - public const int ClassJobContainer = 0x1A8; - } - - public const byte DrawObjectVisorStateFlag = 0x40; - public const byte DrawObjectVisorToggleFlag = 0x80; -} - -public static class Sigs -{ - public const string ChangeJob = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 80 61"; - public const string FlagSlotForUpdate = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A"; - public const string ChangeCustomize = "E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86"; -} diff --git a/Glamourer.GameData/Structs/EquipFlag.cs b/Glamourer.GameData/Structs/EquipFlag.cs deleted file mode 100644 index eaacbac..0000000 --- a/Glamourer.GameData/Structs/EquipFlag.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using Penumbra.GameData.Enums; - -namespace Glamourer.Structs; - -[Flags] -public enum EquipFlag : uint -{ - Head = 0x00000001, - Body = 0x00000002, - Hands = 0x00000004, - Legs = 0x00000008, - Feet = 0x00000010, - Ears = 0x00000020, - Neck = 0x00000040, - Wrist = 0x00000080, - RFinger = 0x00000100, - LFinger = 0x00000200, - Mainhand = 0x00000400, - Offhand = 0x00000800, - HeadStain = 0x00001000, - BodyStain = 0x00002000, - HandsStain = 0x00004000, - LegsStain = 0x00008000, - FeetStain = 0x00010000, - EarsStain = 0x00020000, - NeckStain = 0x00040000, - WristStain = 0x00080000, - RFingerStain = 0x00100000, - LFingerStain = 0x00200000, - MainhandStain = 0x00400000, - OffhandStain = 0x00800000, -} - -public static class EquipFlagExtensions -{ - public const EquipFlag All = (EquipFlag)(((uint)EquipFlag.OffhandStain << 1) - 1); - public const int NumEquipFlags = 24; - - public static EquipFlag ToFlag(this EquipSlot slot) - => slot switch - { - EquipSlot.MainHand => EquipFlag.Mainhand, - EquipSlot.OffHand => EquipFlag.Offhand, - EquipSlot.Head => EquipFlag.Head, - EquipSlot.Body => EquipFlag.Body, - EquipSlot.Hands => EquipFlag.Hands, - EquipSlot.Legs => EquipFlag.Legs, - EquipSlot.Feet => EquipFlag.Feet, - EquipSlot.Ears => EquipFlag.Ears, - EquipSlot.Neck => EquipFlag.Neck, - EquipSlot.Wrists => EquipFlag.Wrist, - EquipSlot.RFinger => EquipFlag.RFinger, - EquipSlot.LFinger => EquipFlag.LFinger, - _ => 0, - }; - - public static EquipFlag ToStainFlag(this EquipSlot slot) - => slot switch - { - EquipSlot.MainHand => EquipFlag.MainhandStain, - EquipSlot.OffHand => EquipFlag.OffhandStain, - EquipSlot.Head => EquipFlag.HeadStain, - EquipSlot.Body => EquipFlag.BodyStain, - EquipSlot.Hands => EquipFlag.HandsStain, - EquipSlot.Legs => EquipFlag.LegsStain, - EquipSlot.Feet => EquipFlag.FeetStain, - EquipSlot.Ears => EquipFlag.EarsStain, - EquipSlot.Neck => EquipFlag.NeckStain, - EquipSlot.Wrists => EquipFlag.WristStain, - EquipSlot.RFinger => EquipFlag.RFingerStain, - EquipSlot.LFinger => EquipFlag.LFingerStain, - _ => 0, - }; - - public static EquipFlag ToBothFlags(this EquipSlot slot) - => slot switch - { - EquipSlot.MainHand => EquipFlag.Mainhand | EquipFlag.MainhandStain, - EquipSlot.OffHand => EquipFlag.Offhand | EquipFlag.OffhandStain, - EquipSlot.Head => EquipFlag.Head | EquipFlag.HeadStain, - EquipSlot.Body => EquipFlag.Body | EquipFlag.BodyStain, - EquipSlot.Hands => EquipFlag.Hands | EquipFlag.HandsStain, - EquipSlot.Legs => EquipFlag.Legs | EquipFlag.LegsStain, - EquipSlot.Feet => EquipFlag.Feet | EquipFlag.FeetStain, - EquipSlot.Ears => EquipFlag.Ears | EquipFlag.EarsStain, - EquipSlot.Neck => EquipFlag.Neck | EquipFlag.NeckStain, - EquipSlot.Wrists => EquipFlag.Wrist | EquipFlag.WristStain, - EquipSlot.RFinger => EquipFlag.RFinger | EquipFlag.RFingerStain, - EquipSlot.LFinger => EquipFlag.LFinger | EquipFlag.LFingerStain, - _ => 0, - }; -} diff --git a/Glamourer.GameData/Structs/Job.cs b/Glamourer.GameData/Structs/Job.cs deleted file mode 100644 index 1d2873d..0000000 --- a/Glamourer.GameData/Structs/Job.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Dalamud.Utility; -using Lumina.Excel.GeneratedSheets; - -namespace Glamourer.Structs; - -// A struct containing the different jobs the game supports. -// Also contains the jobs Name and Abbreviation as strings. -public readonly struct Job -{ - public readonly string Name; - public readonly string Abbreviation; - public readonly ClassJob Base; - - public uint Id - => Base.RowId; - - public JobFlag Flag - => (JobFlag)(1ul << (int)Base.RowId); - - public Job(ClassJob job) - { - Base = job; - Name = job.Name.ToDalamudString().ToString(); - Abbreviation = job.Abbreviation.ToDalamudString().ToString(); - } - - public override string ToString() - => Name; -} diff --git a/Glamourer.GameData/Structs/JobGroup.cs b/Glamourer.GameData/Structs/JobGroup.cs deleted file mode 100644 index 5471e7f..0000000 --- a/Glamourer.GameData/Structs/JobGroup.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Diagnostics; -using Lumina.Excel; -using Lumina.Excel.GeneratedSheets; - -namespace Glamourer.Structs; - -[Flags] -public enum JobFlag : ulong -{ } - -// The game specifies different job groups that can contain specific jobs or not. -public readonly struct JobGroup -{ - public readonly string Name; - public readonly int Count; - public readonly uint Id; - private readonly JobFlag _flags; - - // Create a job group from a given category and the ClassJob sheet. - // It looks up the different jobs contained in the category and sets the flags appropriately. - public JobGroup(ClassJobCategory group, ExcelSheet jobs) - { - Count = 0; - _flags = 0ul; - Id = group.RowId; - Name = group.Name.ToString(); - - Debug.Assert(jobs.RowCount < 64, $"Number of Jobs exceeded 63 ({jobs.RowCount})."); - foreach (var job in jobs) - { - var abbr = job.Abbreviation.ToString(); - if (abbr.Length == 0) - continue; - - var prop = group.GetType().GetProperty(abbr); - Debug.Assert(prop != null, $"Could not get job abbreviation {abbr} property."); - - if (!(bool)prop.GetValue(group)!) - continue; - - ++Count; - _flags |= (JobFlag)(1ul << (int)job.RowId); - } - } - - // Check if a job is contained inside this group. - public bool Fits(Job job) - => _flags.HasFlag(job.Flag); - - // Check if any of the jobs in the given flags fit this group. - public bool Fits(JobFlag flag) - => (_flags & flag) != 0; - - // Check if a job is contained inside this group. - public bool Fits(uint jobId) - { - var flag = (JobFlag)(1ul << (int)jobId); - return _flags.HasFlag(flag); - } -} diff --git a/Glamourer.sln b/Glamourer.sln index 5f555ca..e2915d5 100644 --- a/Glamourer.sln +++ b/Glamourer.sln @@ -6,11 +6,12 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{383AEE76-D423-431C-893A-7AB3DEA13630}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + .github\workflows\release.yml = .github\workflows\release.yml + Glamourer\Glamourer.json = Glamourer\Glamourer.json repo.json = repo.json + .github\workflows\test_release.yml = .github\workflows\test_release.yml EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer.GameData", "Glamourer.GameData\Glamourer.GameData.csproj", "{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer", "Glamourer\Glamourer.csproj", "{01EB903D-871F-4285-A8CF-6486561D5B5B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "Penumbra.Api\Penumbra.Api.csproj", "{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}" @@ -21,36 +22,38 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "OtterGui\OtterGui.csproj", "{EF233CE2-F243-449E-BE05-72B9D110E419}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer.Api", "Glamourer.Api\Glamourer.Api.csproj", "{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|Any CPU.Build.0 = Release|Any CPU - {01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.Build.0 = Release|Any CPU - {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.Build.0 = Release|Any CPU - {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.Build.0 = Release|Any CPU - {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.Build.0 = Release|Any CPU - {EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.Build.0 = Release|Any CPU + {01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.ActiveCfg = Debug|x64 + {01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.Build.0 = Debug|x64 + {01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.ActiveCfg = Release|x64 + {01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.Build.0 = Release|x64 + {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.ActiveCfg = Debug|x64 + {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.Build.0 = Debug|x64 + {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.ActiveCfg = Release|x64 + {29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.Build.0 = Release|x64 + {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.ActiveCfg = Debug|x64 + {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.Build.0 = Debug|x64 + {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.ActiveCfg = Release|x64 + {C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.Build.0 = Release|x64 + {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.ActiveCfg = Debug|x64 + {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.Build.0 = Debug|x64 + {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.ActiveCfg = Release|x64 + {AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.Build.0 = Release|x64 + {EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.ActiveCfg = Debug|x64 + {EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.Build.0 = Debug|x64 + {EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.ActiveCfg = Release|x64 + {EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.Build.0 = Release|x64 + {9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Debug|Any CPU.ActiveCfg = Debug|x64 + {9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Debug|Any CPU.Build.0 = Debug|x64 + {9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Release|Any CPU.ActiveCfg = Release|x64 + {9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Release|Any CPU.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Glamourer/Api/ApiHelpers.cs b/Glamourer/Api/ApiHelpers.cs new file mode 100644 index 0000000..14cff3b --- /dev/null +++ b/Glamourer/Api/ApiHelpers.cs @@ -0,0 +1,133 @@ +using Glamourer.Api.Enums; +using Glamourer.Designs; +using Glamourer.State; +using OtterGui.Extensions; +using OtterGui.Log; +using OtterGui.Services; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; +using Penumbra.String; + +namespace Glamourer.Api; + +public class ApiHelpers(ActorObjectManager objects, StateManager stateManager, ActorManager actors) : IApiService +{ + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal IEnumerable FindExistingStates(string actorName, ushort worldId = ushort.MaxValue) + { + if (actorName.Length == 0 || !ByteString.FromString(actorName, out var byteString)) + yield break; + + if (worldId == WorldId.AnyWorld.Id) + { + foreach (var state in stateManager.Values.Where(state + => state.Identifier.Type is IdentifierType.Player && state.Identifier.PlayerName == byteString)) + yield return state; + } + else + { + var identifier = actors.CreatePlayer(byteString, worldId); + if (stateManager.TryGetValue(identifier, out var state)) + yield return state; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal GlamourerApiEc FindExistingState(int objectIndex, out ActorState? state) + { + var actor = objects.Objects[objectIndex]; + var identifier = actor.GetIdentifier(actors); + if (!identifier.IsValid) + { + state = null; + return GlamourerApiEc.ActorNotFound; + } + + stateManager.TryGetValue(identifier, out state); + return GlamourerApiEc.Success; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal ActorState? FindState(int objectIndex) + { + var actor = objects.Objects[objectIndex]; + var identifier = actor.GetIdentifier(actors); + if (identifier.IsValid && stateManager.GetOrCreate(identifier, actor, out var state)) + return state; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static DesignBase.FlagRestrictionResetter Restrict(DesignBase design, ApplyFlag flags) + => (flags & (ApplyFlag.Equipment | ApplyFlag.Customization)) switch + { + ApplyFlag.Equipment => design.TemporarilyRestrictApplication(ApplicationCollection.Equipment), + ApplyFlag.Customization => design.TemporarilyRestrictApplication(ApplicationCollection.Customizations), + ApplyFlag.Equipment | ApplyFlag.Customization => design.TemporarilyRestrictApplication(ApplicationCollection.All), + _ => design.TemporarilyRestrictApplication(ApplicationCollection.None), + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static void Lock(ActorState state, uint key, ApplyFlag flags) + { + if ((flags & ApplyFlag.Lock) != 0 && key != 0) + state.Lock(key); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal IEnumerable FindStates(string objectName) + { + if (objectName.Length == 0 || !ByteString.FromString(objectName, out var byteString)) + return []; + + return stateManager.Values.Where(state => state.Identifier.Type is IdentifierType.Player && state.Identifier.PlayerName == byteString) + .Concat(objects + .Where(kvp => kvp.Key is { IsValid: true, Type: IdentifierType.Player } && kvp.Key.PlayerName == byteString) + .SelectWhere(kvp => + { + if (stateManager.ContainsKey(kvp.Key)) + return (false, null); + + var ret = stateManager.GetOrCreate(kvp.Key, kvp.Value.Objects[0], out var state); + return (ret, state); + })); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static GlamourerApiEc Return(GlamourerApiEc ec, LazyString args, [CallerMemberName] string name = "Unknown") + { + if (ec is GlamourerApiEc.Success or GlamourerApiEc.NothingDone) + Glamourer.Log.Verbose($"[{name}] Called with {args}, returned {ec}."); + else + Glamourer.Log.Debug($"[{name}] Called with {args}, returned {ec}."); + return ec; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static LazyString Args(params object[] arguments) + { + if (arguments.Length == 0) + return new LazyString(() => "no arguments"); + + return new LazyString(() => + { + var sb = new StringBuilder(); + for (var i = 0; i < arguments.Length / 2; ++i) + { + sb.Append(arguments[2 * i]); + sb.Append(" = "); + if (arguments[2 * i + 1] is IEnumerable e) + sb.Append($"[{string.Join(',', e)}]"); + else + sb.Append(arguments[2 * i + 1]); + sb.Append(", "); + } + + return sb.ToString(0, sb.Length - 2); + }); + } +} diff --git a/Glamourer/Api/DesignsApi.cs b/Glamourer/Api/DesignsApi.cs new file mode 100644 index 0000000..9b48ade --- /dev/null +++ b/Glamourer/Api/DesignsApi.cs @@ -0,0 +1,138 @@ +using Glamourer.Api.Api; +using Glamourer.Api.Enums; +using Glamourer.Designs; +using Glamourer.State; +using Newtonsoft.Json.Linq; +using OtterGui.Services; + +namespace Glamourer.Api; + +public class DesignsApi( + ApiHelpers helpers, + DesignManager designs, + StateManager stateManager, + DesignFileSystem fileSystem, + DesignColors color, + DesignConverter converter) + : IGlamourerApiDesigns, IApiService +{ + public Dictionary GetDesignList() + => designs.Designs.ToDictionary(d => d.Identifier, d => d.Name.Text); + + public Dictionary GetDesignListExtended() + => fileSystem.ToDictionary(kvp => kvp.Key.Identifier, + kvp => (kvp.Key.Name.Text, kvp.Value.FullName(), color.GetColor(kvp.Key), kvp.Key.QuickDesign)); + + public (string DisplayName, string FullPath, uint DisplayColor, bool ShowInQdb) GetExtendedDesignData(Guid designId) + => designs.Designs.ByIdentifier(designId) is { } d + ? (d.Name.Text, fileSystem.TryGetValue(d, out var leaf) ? leaf.FullName() : d.Name.Text, color.GetColor(d), d.QuickDesign) + : (string.Empty, string.Empty, 0, false); + + public GlamourerApiEc ApplyDesign(Guid designId, int objectIndex, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Design", designId, "Index", objectIndex, "Key", key, "Flags", flags); + var design = designs.Designs.ByIdentifier(designId); + if (design == null) + return ApiHelpers.Return(GlamourerApiEc.DesignNotFound, args); + + if (helpers.FindState(objectIndex) is not { } state) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (!state.CanUnlock(key)) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + ApplyDesign(state, design, key, flags); + ApiHelpers.Lock(state, key, flags); + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + private void ApplyDesign(ActorState state, Design design, uint key, ApplyFlag flags) + { + var once = (flags & ApplyFlag.Once) != 0; + var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true, + ResetMaterials: !once && key != 0, IsFinal: true); + + using var restrict = ApiHelpers.Restrict(design, flags); + stateManager.ApplyDesign(state, design, settings); + } + + public GlamourerApiEc ApplyDesignName(Guid designId, string playerName, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Design", designId, "Name", playerName, "Key", key, "Flags", flags); + var design = designs.Designs.ByIdentifier(designId); + if (design == null) + return ApiHelpers.Return(GlamourerApiEc.DesignNotFound, args); + + var any = false; + var anyUnlocked = false; + foreach (var state in helpers.FindStates(playerName)) + { + any = true; + if (!state.CanUnlock(key)) + continue; + + anyUnlocked = true; + ApplyDesign(state, design, key, flags); + ApiHelpers.Lock(state, key, flags); + } + + if (!any) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + if (!anyUnlocked) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public (GlamourerApiEc, Guid) AddDesign(string designInput, string name) + { + var args = ApiHelpers.Args("DesignData", designInput, "Name", name); + + if (converter.FromBase64(designInput, true, true, out _) is not { } designBase) + try + { + var jObj = JObject.Parse(designInput); + designBase = converter.FromJObject(jObj, true, true); + if (designBase is null) + return (ApiHelpers.Return(GlamourerApiEc.CouldNotParse, args), Guid.Empty); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Failure parsing data for AddDesign due to\n{ex}"); + return (ApiHelpers.Return(GlamourerApiEc.CouldNotParse, args), Guid.Empty); + } + + try + { + var design = designBase is Design d + ? designs.CreateClone(d, name, true) + : designs.CreateClone(designBase, name, true); + return (ApiHelpers.Return(GlamourerApiEc.Success, args), design.Identifier); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Unknown error creating design via IPC:\n{ex}"); + return (ApiHelpers.Return(GlamourerApiEc.UnknownError, args), Guid.Empty); + } + } + + public GlamourerApiEc DeleteDesign(Guid designId) + { + var args = ApiHelpers.Args("DesignId", designId); + if (designs.Designs.ByIdentifier(designId) is not { } design) + return ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + designs.Delete(design); + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public string? GetDesignBase64(Guid designId) + => designs.Designs.ByIdentifier(designId) is { } design + ? converter.ShareBase64(design) + : null; + + public JObject? GetDesignJObject(Guid designId) + => designs.Designs.ByIdentifier(designId) is { } design + ? converter.ShareJObject(design) + : null; +} diff --git a/Glamourer/Api/GlamourerApi.cs b/Glamourer/Api/GlamourerApi.cs new file mode 100644 index 0000000..85f873a --- /dev/null +++ b/Glamourer/Api/GlamourerApi.cs @@ -0,0 +1,25 @@ +using Glamourer.Api.Api; +using OtterGui.Services; + +namespace Glamourer.Api; + +public class GlamourerApi(Configuration config, DesignsApi designs, StateApi state, ItemsApi items) : IGlamourerApi, IApiService +{ + public const int CurrentApiVersionMajor = 1; + public const int CurrentApiVersionMinor = 7; + + public (int Major, int Minor) ApiVersion + => (CurrentApiVersionMajor, CurrentApiVersionMinor); + + public bool AutoReloadGearEnabled + => config.AutoRedrawEquipOnChanges; + + public IGlamourerApiDesigns Designs + => designs; + + public IGlamourerApiItems Items + => items; + + public IGlamourerApiState State + => state; +} diff --git a/Glamourer/Api/GlamourerIpc.ApiVersions.cs b/Glamourer/Api/GlamourerIpc.ApiVersions.cs deleted file mode 100644 index 7c74d62..0000000 --- a/Glamourer/Api/GlamourerIpc.ApiVersions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using Dalamud.Plugin; -using Penumbra.Api.Helpers; - -namespace Glamourer.Api; - -public partial class GlamourerIpc -{ - public const string LabelApiVersion = "Glamourer.ApiVersion"; - public const string LabelApiVersions = "Glamourer.ApiVersions"; - - private readonly FuncProvider _apiVersionProvider; - private readonly FuncProvider<(int Major, int Minor)> _apiVersionsProvider; - - public static FuncSubscriber ApiVersionSubscriber(DalamudPluginInterface pi) - => new(pi, LabelApiVersion); - - public static FuncSubscriber<(int Major, int Minor)> ApiVersionsSubscriber(DalamudPluginInterface pi) - => new(pi, LabelApiVersions); - - public int ApiVersion() - => CurrentApiVersionMajor; - - public (int Major, int Minor) ApiVersions() - => (CurrentApiVersionMajor, CurrentApiVersionMinor); -} diff --git a/Glamourer/Api/GlamourerIpc.Apply.cs b/Glamourer/Api/GlamourerIpc.Apply.cs deleted file mode 100644 index da8e33e..0000000 --- a/Glamourer/Api/GlamourerIpc.Apply.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System.Collections.Generic; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Plugin; -using Glamourer.Designs; -using Glamourer.Events; -using Glamourer.Interop.Structs; -using Penumbra.Api.Helpers; -using Penumbra.GameData.Actors; - -namespace Glamourer.Api; - -public partial class GlamourerIpc -{ - public const string LabelApplyAll = "Glamourer.ApplyAll"; - public const string LabelApplyAllToCharacter = "Glamourer.ApplyAllToCharacter"; - public const string LabelApplyOnlyEquipment = "Glamourer.ApplyOnlyEquipment"; - public const string LabelApplyOnlyEquipmentToCharacter = "Glamourer.ApplyOnlyEquipmentToCharacter"; - public const string LabelApplyOnlyCustomization = "Glamourer.ApplyOnlyCustomization"; - public const string LabelApplyOnlyCustomizationToCharacter = "Glamourer.ApplyOnlyCustomizationToCharacter"; - - public const string LabelApplyAllLock = "Glamourer.ApplyAllLock"; - public const string LabelApplyAllToCharacterLock = "Glamourer.ApplyAllToCharacterLock"; - public const string LabelApplyOnlyEquipmentLock = "Glamourer.ApplyOnlyEquipmentLock"; - public const string LabelApplyOnlyEquipmentToCharacterLock = "Glamourer.ApplyOnlyEquipmentToCharacterLock"; - public const string LabelApplyOnlyCustomizationLock = "Glamourer.ApplyOnlyCustomizationLock"; - public const string LabelApplyOnlyCustomizationToCharacterLock = "Glamourer.ApplyOnlyCustomizationToCharacterLock"; - - private readonly ActionProvider _applyAllProvider; - private readonly ActionProvider _applyAllToCharacterProvider; - private readonly ActionProvider _applyOnlyEquipmentProvider; - private readonly ActionProvider _applyOnlyEquipmentToCharacterProvider; - private readonly ActionProvider _applyOnlyCustomizationProvider; - private readonly ActionProvider _applyOnlyCustomizationToCharacterProvider; - - private readonly ActionProvider _applyAllProviderLock; - private readonly ActionProvider _applyAllToCharacterProviderLock; - private readonly ActionProvider _applyOnlyEquipmentProviderLock; - private readonly ActionProvider _applyOnlyEquipmentToCharacterProviderLock; - private readonly ActionProvider _applyOnlyCustomizationProviderLock; - private readonly ActionProvider _applyOnlyCustomizationToCharacterProviderLock; - - public static ActionSubscriber ApplyAllSubscriber(DalamudPluginInterface pi) - => new(pi, LabelApplyAll); - - public static ActionSubscriber ApplyAllToCharacterSubscriber(DalamudPluginInterface pi) - => new(pi, LabelApplyAllToCharacter); - - public static ActionSubscriber ApplyOnlyEquipmentSubscriber(DalamudPluginInterface pi) - => new(pi, LabelApplyOnlyEquipment); - - public static ActionSubscriber ApplyOnlyEquipmentToCharacterSubscriber(DalamudPluginInterface pi) - => new(pi, LabelApplyOnlyEquipmentToCharacter); - - public static ActionSubscriber ApplyOnlyCustomizationSubscriber(DalamudPluginInterface pi) - => new(pi, LabelApplyOnlyCustomization); - - public static ActionSubscriber ApplyOnlyCustomizationToCharacterSubscriber(DalamudPluginInterface pi) - => new(pi, LabelApplyOnlyCustomizationToCharacter); - - public void ApplyAll(string base64, string characterName) - => ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(characterName), version, 0); - - public void ApplyAllToCharacter(string base64, Character? character) - => ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(character), version, 0); - - public void ApplyOnlyEquipment(string base64, string characterName) - => ApplyDesign(_designConverter.FromBase64(base64, false, true, out var version), FindActors(characterName), version, 0); - - public void ApplyOnlyEquipmentToCharacter(string base64, Character? character) - => ApplyDesign(_designConverter.FromBase64(base64, false, true, out var version), FindActors(character), version, 0); - - public void ApplyOnlyCustomization(string base64, string characterName) - => ApplyDesign(_designConverter.FromBase64(base64, true, false, out var version), FindActors(characterName), version, 0); - - public void ApplyOnlyCustomizationToCharacter(string base64, Character? character) - => ApplyDesign(_designConverter.FromBase64(base64, true, false, out var version), FindActors(character), version, 0); - - - public void ApplyAllLock(string base64, string characterName, uint lockCode) - => ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(characterName), version, lockCode); - - public void ApplyAllToCharacterLock(string base64, Character? character, uint lockCode) - => ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(character), version, lockCode); - - public void ApplyOnlyEquipmentLock(string base64, string characterName, uint lockCode) - => ApplyDesign(_designConverter.FromBase64(base64, false, true, out var version), FindActors(characterName), version, lockCode); - - public void ApplyOnlyEquipmentToCharacterLock(string base64, Character? character, uint lockCode) - => ApplyDesign(_designConverter.FromBase64(base64, false, true, out var version), FindActors(character), version, lockCode); - - public void ApplyOnlyCustomizationLock(string base64, string characterName, uint lockCode) - => ApplyDesign(_designConverter.FromBase64(base64, true, false, out var version), FindActors(characterName), version, lockCode); - - public void ApplyOnlyCustomizationToCharacterLock(string base64, Character? character, uint lockCode) - => ApplyDesign(_designConverter.FromBase64(base64, true, false, out var version), FindActors(character), version, lockCode); - - - private void ApplyDesign(DesignBase? design, IEnumerable actors, byte version, uint lockCode) - { - if (design == null) - return; - - var hasModelId = version >= 3; - _objects.Update(); - foreach (var id in actors) - { - if (!_stateManager.TryGetValue(id, out var state)) - { - var data = _objects.TryGetValue(id, out var d) ? d : ActorData.Invalid; - if (!data.Valid || !_stateManager.GetOrCreate(id, data.Objects[0], out state)) - continue; - } - - if ((hasModelId || state.ModelData.ModelId == 0) && state.CanUnlock(lockCode)) - { - _stateManager.ApplyDesign(design, state, StateChanged.Source.Ipc, lockCode); - state.Lock(lockCode); - } - } - } -} diff --git a/Glamourer/Api/GlamourerIpc.Events.cs b/Glamourer/Api/GlamourerIpc.Events.cs deleted file mode 100644 index 0186f6d..0000000 --- a/Glamourer/Api/GlamourerIpc.Events.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Glamourer.Events; -using Glamourer.Interop.Structs; -using Glamourer.State; -using Penumbra.Api.Helpers; - -namespace Glamourer.Api; - -public partial class GlamourerIpc -{ - public const string LabelStateChanged = "Glamourer.StateChanged"; - public const string LabelGPoseChanged = "Glamourer.GPoseChanged"; - - private readonly GPoseService _gPose; - private readonly StateChanged _stateChangedEvent; - private readonly EventProvider> _stateChangedProvider; - private readonly EventProvider _gPoseChangedProvider; - - private void OnStateChanged(StateChanged.Type type, StateChanged.Source source, ActorState state, ActorData actors, object? data = null) - { - foreach (var actor in actors.Objects) - _stateChangedProvider.Invoke(type, actor.Address, new Lazy(() => _designConverter.ShareBase64(state))); - } - - private void OnGPoseChanged(bool value) - => _gPoseChangedProvider.Invoke(value); -} diff --git a/Glamourer/Api/GlamourerIpc.GetCustomization.cs b/Glamourer/Api/GlamourerIpc.GetCustomization.cs deleted file mode 100644 index 3e8794c..0000000 --- a/Glamourer/Api/GlamourerIpc.GetCustomization.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Buffers.Text; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Plugin; -using Glamourer.Customization; -using Glamourer.Designs; -using Glamourer.Structs; -using Penumbra.Api.Helpers; -using Penumbra.GameData.Actors; - -namespace Glamourer.Api; - -public partial class GlamourerIpc -{ - public const string LabelGetAllCustomization = "Glamourer.GetAllCustomization"; - public const string LabelGetAllCustomizationFromCharacter = "Glamourer.GetAllCustomizationFromCharacter"; - - private readonly FuncProvider _getAllCustomizationProvider; - private readonly FuncProvider _getAllCustomizationFromCharacterProvider; - - public static FuncSubscriber GetAllCustomizationSubscriber(DalamudPluginInterface pi) - => new(pi, LabelGetAllCustomization); - - public static FuncSubscriber GetAllCustomizationFromCharacterSubscriber(DalamudPluginInterface pi) - => new(pi, LabelGetAllCustomizationFromCharacter); - - public string? GetAllCustomization(string characterName) - => GetCustomization(FindActors(characterName)); - - public string? GetAllCustomizationFromCharacter(Character? character) - => GetCustomization(FindActors(character)); - - private string? GetCustomization(IEnumerable actors) - { - var actor = actors.FirstOrDefault(ActorIdentifier.Invalid); - if (!actor.IsValid) - return null; - - if (!_stateManager.TryGetValue(actor, out var state)) - { - _objects.Update(); - if (!_objects.TryGetValue(actor, out var data) || !data.Valid) - return null; - if (!_stateManager.GetOrCreate(actor, data.Objects[0], out state)) - return null; - } - - return _designConverter.ShareBase64(state); - } -} diff --git a/Glamourer/Api/GlamourerIpc.Revert.cs b/Glamourer/Api/GlamourerIpc.Revert.cs deleted file mode 100644 index 9d486ae..0000000 --- a/Glamourer/Api/GlamourerIpc.Revert.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System.Collections.Generic; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Plugin; -using Glamourer.Events; -using Penumbra.Api.Helpers; -using Penumbra.GameData.Actors; - -namespace Glamourer.Api; - -public partial class GlamourerIpc -{ - public const string LabelRevert = "Glamourer.Revert"; - public const string LabelRevertCharacter = "Glamourer.RevertCharacter"; - public const string LabelRevertLock = "Glamourer.RevertLock"; - public const string LabelRevertCharacterLock = "Glamourer.RevertCharacterLock"; - public const string LabelRevertToAutomation = "Glamourer.RevertToAutomation"; - public const string LabelRevertToAutomationCharacter = "Glamourer.RevertToAutomationCharacter"; - public const string LabelUnlock = "Glamourer.Unlock"; - public const string LabelUnlockName = "Glamourer.UnlockName"; - - private readonly ActionProvider _revertProvider; - private readonly ActionProvider _revertCharacterProvider; - - private readonly ActionProvider _revertProviderLock; - private readonly ActionProvider _revertCharacterProviderLock; - - private readonly FuncProvider _revertToAutomationProvider; - private readonly FuncProvider _revertToAutomationCharacterProvider; - - private readonly FuncProvider _unlockNameProvider; - private readonly FuncProvider _unlockProvider; - - public static ActionSubscriber RevertSubscriber(DalamudPluginInterface pi) - => new(pi, LabelRevert); - - public static ActionSubscriber RevertCharacterSubscriber(DalamudPluginInterface pi) - => new(pi, LabelRevertCharacter); - - public static ActionSubscriber RevertLockSubscriber(DalamudPluginInterface pi) - => new(pi, LabelRevertLock); - - public static ActionSubscriber RevertCharacterLockSubscriber(DalamudPluginInterface pi) - => new(pi, LabelRevertCharacterLock); - - public static FuncSubscriber UnlockNameSubscriber(DalamudPluginInterface pi) - => new(pi, LabelUnlockName); - - public static FuncSubscriber UnlockSubscriber(DalamudPluginInterface pi) - => new(pi, LabelUnlock); - - public static FuncSubscriber RevertToAutomationSubscriber(DalamudPluginInterface pi) - => new(pi, LabelRevertToAutomation); - - public static FuncSubscriber RevertToAutomationCharacterSubscriber(DalamudPluginInterface pi) - => new(pi, LabelRevertToAutomationCharacter); - - public void Revert(string characterName) - => Revert(FindActorsRevert(characterName), 0); - - public void RevertCharacter(Character? character) - => Revert(FindActors(character), 0); - - public void RevertLock(string characterName, uint lockCode) - => Revert(FindActorsRevert(characterName), lockCode); - - public void RevertCharacterLock(Character? character, uint lockCode) - => Revert(FindActors(character), lockCode); - - public bool Unlock(string characterName, uint lockCode) - => Unlock(FindActorsRevert(characterName), lockCode); - - public bool Unlock(Character? character, uint lockCode) - => Unlock(FindActors(character), lockCode); - - public bool RevertToAutomation(string characterName, uint lockCode) - => RevertToAutomation(FindActorsRevert(characterName), lockCode); - - public bool RevertToAutomation(Character? character, uint lockCode) - => RevertToAutomation(FindActors(character), lockCode); - - private void Revert(IEnumerable actors, uint lockCode) - { - foreach (var id in actors) - { - if (_stateManager.TryGetValue(id, out var state)) - _stateManager.ResetState(state, StateChanged.Source.Ipc, lockCode); - } - } - - private bool Unlock(IEnumerable actors, uint lockCode) - { - var ret = false; - foreach (var id in actors) - { - if (_stateManager.TryGetValue(id, out var state)) - ret |= state.Unlock(lockCode); - } - - return ret; - } - - private bool RevertToAutomation(IEnumerable actors, uint lockCode) - { - var ret = false; - foreach (var id in actors) - { - if (_stateManager.TryGetValue(id, out var state)) - { - ret |= state.Unlock(lockCode); - if (_objects.TryGetValue(id, out var data)) - foreach (var obj in data.Objects) - { - _autoDesignApplier.ReapplyAutomation(obj, state.Identifier, state); - _stateManager.ReapplyState(obj); - } - } - } - - return ret; - } -} diff --git a/Glamourer/Api/GlamourerIpc.cs b/Glamourer/Api/GlamourerIpc.cs deleted file mode 100644 index 398cdf2..0000000 --- a/Glamourer/Api/GlamourerIpc.cs +++ /dev/null @@ -1,151 +0,0 @@ -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Plugin; -using System; -using System.Collections.Generic; -using System.Linq; -using Glamourer.Automation; -using Glamourer.Designs; -using Glamourer.Events; -using Glamourer.Interop; -using Glamourer.Services; -using Glamourer.State; -using Penumbra.Api.Helpers; -using Penumbra.GameData.Actors; -using Penumbra.String; - -namespace Glamourer.Api; - -public partial class GlamourerIpc : IDisposable -{ - public const int CurrentApiVersionMajor = 0; - public const int CurrentApiVersionMinor = 4; - - private readonly StateManager _stateManager; - private readonly ObjectManager _objects; - private readonly ActorService _actors; - private readonly DesignConverter _designConverter; - private readonly AutoDesignApplier _autoDesignApplier; - - public GlamourerIpc(DalamudPluginInterface pi, StateManager stateManager, ObjectManager objects, ActorService actors, - DesignConverter designConverter, StateChanged stateChangedEvent, GPoseService gPose, AutoDesignApplier autoDesignApplier) - { - _stateManager = stateManager; - _objects = objects; - _actors = actors; - _designConverter = designConverter; - _autoDesignApplier = autoDesignApplier; - _gPose = gPose; - _stateChangedEvent = stateChangedEvent; - _apiVersionProvider = new FuncProvider(pi, LabelApiVersion, ApiVersion); - _apiVersionsProvider = new FuncProvider<(int Major, int Minor)>(pi, LabelApiVersions, ApiVersions); - - _getAllCustomizationProvider = new FuncProvider(pi, LabelGetAllCustomization, GetAllCustomization); - _getAllCustomizationFromCharacterProvider = - new FuncProvider(pi, LabelGetAllCustomizationFromCharacter, GetAllCustomizationFromCharacter); - - _applyAllProvider = new ActionProvider(pi, LabelApplyAll, ApplyAll); - _applyAllToCharacterProvider = new ActionProvider(pi, LabelApplyAllToCharacter, ApplyAllToCharacter); - _applyOnlyEquipmentProvider = new ActionProvider(pi, LabelApplyOnlyEquipment, ApplyOnlyEquipment); - _applyOnlyEquipmentToCharacterProvider = - new ActionProvider(pi, LabelApplyOnlyEquipmentToCharacter, ApplyOnlyEquipmentToCharacter); - _applyOnlyCustomizationProvider = new ActionProvider(pi, LabelApplyOnlyCustomization, ApplyOnlyCustomization); - _applyOnlyCustomizationToCharacterProvider = - new ActionProvider(pi, LabelApplyOnlyCustomizationToCharacter, ApplyOnlyCustomizationToCharacter); - - _applyAllProviderLock = new ActionProvider(pi, LabelApplyAllLock, ApplyAllLock); - _applyAllToCharacterProviderLock = - new ActionProvider(pi, LabelApplyAllToCharacterLock, ApplyAllToCharacterLock); - _applyOnlyEquipmentProviderLock = new ActionProvider(pi, LabelApplyOnlyEquipmentLock, ApplyOnlyEquipmentLock); - _applyOnlyEquipmentToCharacterProviderLock = - new ActionProvider(pi, LabelApplyOnlyEquipmentToCharacterLock, ApplyOnlyEquipmentToCharacterLock); - _applyOnlyCustomizationProviderLock = - new ActionProvider(pi, LabelApplyOnlyCustomizationLock, ApplyOnlyCustomizationLock); - _applyOnlyCustomizationToCharacterProviderLock = - new ActionProvider(pi, LabelApplyOnlyCustomizationToCharacterLock, ApplyOnlyCustomizationToCharacterLock); - - _revertProvider = new ActionProvider(pi, LabelRevert, Revert); - _revertCharacterProvider = new ActionProvider(pi, LabelRevertCharacter, RevertCharacter); - _revertProviderLock = new ActionProvider(pi, LabelRevertLock, RevertLock); - _revertCharacterProviderLock = new ActionProvider(pi, LabelRevertCharacterLock, RevertCharacterLock); - _unlockNameProvider = new FuncProvider(pi, LabelUnlockName, Unlock); - _unlockProvider = new FuncProvider(pi, LabelUnlock, Unlock); - _revertToAutomationProvider = new FuncProvider(pi, LabelRevertToAutomation, RevertToAutomation); - _revertToAutomationCharacterProvider = - new FuncProvider(pi, LabelRevertToAutomationCharacter, RevertToAutomation); - - _stateChangedProvider = new EventProvider>(pi, LabelStateChanged); - _gPoseChangedProvider = new EventProvider(pi, LabelGPoseChanged); - - _stateChangedEvent.Subscribe(OnStateChanged, StateChanged.Priority.GlamourerIpc); - _gPose.Subscribe(OnGPoseChanged, GPoseService.Priority.GlamourerIpc); - } - - public void Dispose() - { - _apiVersionProvider.Dispose(); - _apiVersionsProvider.Dispose(); - - _getAllCustomizationProvider.Dispose(); - _getAllCustomizationFromCharacterProvider.Dispose(); - - _applyAllProvider.Dispose(); - _applyAllToCharacterProvider.Dispose(); - _applyOnlyEquipmentProvider.Dispose(); - _applyOnlyEquipmentToCharacterProvider.Dispose(); - _applyOnlyCustomizationProvider.Dispose(); - _applyOnlyCustomizationToCharacterProvider.Dispose(); - _applyAllProviderLock.Dispose(); - _applyAllToCharacterProviderLock.Dispose(); - _applyOnlyEquipmentProviderLock.Dispose(); - _applyOnlyEquipmentToCharacterProviderLock.Dispose(); - _applyOnlyCustomizationProviderLock.Dispose(); - _applyOnlyCustomizationToCharacterProviderLock.Dispose(); - - _revertProvider.Dispose(); - _revertCharacterProvider.Dispose(); - _revertProviderLock.Dispose(); - _revertCharacterProviderLock.Dispose(); - _unlockNameProvider.Dispose(); - _unlockProvider.Dispose(); - _revertToAutomationProvider.Dispose(); - _revertToAutomationCharacterProvider.Dispose(); - - _stateChangedEvent.Unsubscribe(OnStateChanged); - _stateChangedProvider.Dispose(); - _gPose.Unsubscribe(OnGPoseChanged); - _gPoseChangedProvider.Dispose(); - } - - private IEnumerable FindActors(string actorName) - { - if (actorName.Length == 0 || !ByteString.FromString(actorName, out var byteString)) - return Array.Empty(); - - _objects.Update(); - return _objects.Where(i => i.Key is { IsValid: true, Type: IdentifierType.Player } && i.Key.PlayerName == byteString) - .Select(i => i.Key); - } - - private IEnumerable FindActorsRevert(string actorName) - { - if (actorName.Length == 0 || !ByteString.FromString(actorName, out var byteString)) - yield break; - - _objects.Update(); - foreach (var id in _objects.Where(i => i.Key is { IsValid: true, Type: IdentifierType.Player } && i.Key.PlayerName == byteString) - .Select(i => i.Key)) - yield return id; - - foreach (var id in _stateManager.Keys.Where(s => s.Type is IdentifierType.Player && s.PlayerName == byteString)) - yield return id; - } - - private IEnumerable FindActors(Character? character) - { - var id = _actors.AwaitedService.FromObject(character, true, true, false); - if (!id.IsValid) - yield break; - - yield return id; - } -} diff --git a/Glamourer/Api/IpcProviders.cs b/Glamourer/Api/IpcProviders.cs new file mode 100644 index 0000000..f120db3 --- /dev/null +++ b/Glamourer/Api/IpcProviders.cs @@ -0,0 +1,85 @@ +using Dalamud.Plugin; +using Glamourer.Api.Api; +using Glamourer.Api.Helpers; +using OtterGui.Services; +using Glamourer.Api.Enums; + +namespace Glamourer.Api; + +public sealed class IpcProviders : IDisposable, IApiService +{ + private readonly List _providers; + + private readonly EventProvider _disposedProvider; + private readonly EventProvider _initializedProvider; + + public IpcProviders(IDalamudPluginInterface pi, IGlamourerApi api) + { + _disposedProvider = IpcSubscribers.Disposed.Provider(pi); + _initializedProvider = IpcSubscribers.Initialized.Provider(pi); + _providers = + [ + new FuncProvider<(int Major, int Minor)>(pi, "Glamourer.ApiVersions", () => api.ApiVersion), // backward compatibility + new FuncProvider(pi, "Glamourer.ApiVersion", () => api.ApiVersion.Major), // backward compatibility + IpcSubscribers.ApiVersion.Provider(pi, api), + IpcSubscribers.AutoReloadGearEnabled.Provider(pi, api), + + IpcSubscribers.GetDesignList.Provider(pi, api.Designs), + IpcSubscribers.GetDesignListExtended.Provider(pi, api.Designs), + IpcSubscribers.GetExtendedDesignData.Provider(pi, api.Designs), + IpcSubscribers.ApplyDesign.Provider(pi, api.Designs), + IpcSubscribers.ApplyDesignName.Provider(pi, api.Designs), + IpcSubscribers.AddDesign.Provider(pi, api.Designs), + IpcSubscribers.DeleteDesign.Provider(pi, api.Designs), + IpcSubscribers.GetDesignBase64.Provider(pi, api.Designs), + IpcSubscribers.GetDesignJObject.Provider(pi, api.Designs), + + IpcSubscribers.SetItem.Provider(pi, api.Items), + IpcSubscribers.SetItemName.Provider(pi, api.Items), + // backward compatibility + new FuncProvider(pi, IpcSubscribers.Legacy.SetItemV2.Label, + (a, b, c, d, e, f) => (int)api.Items.SetItem(a, (ApiEquipSlot)b, c, [d], e, (ApplyFlag)f)), + new FuncProvider(pi, IpcSubscribers.Legacy.SetItemName.Label, + (a, b, c, d, e, f) => (int)api.Items.SetItemName(a, (ApiEquipSlot)b, c, [d], e, (ApplyFlag)f)), + IpcSubscribers.SetBonusItem.Provider(pi, api.Items), + IpcSubscribers.SetBonusItemName.Provider(pi, api.Items), + IpcSubscribers.SetMetaState.Provider(pi, api.Items), + IpcSubscribers.SetMetaStateName.Provider(pi, api.Items), + IpcSubscribers.GetState.Provider(pi, api.State), + IpcSubscribers.GetStateName.Provider(pi, api.State), + IpcSubscribers.GetStateBase64.Provider(pi, api.State), + IpcSubscribers.GetStateBase64Name.Provider(pi, api.State), + IpcSubscribers.ApplyState.Provider(pi, api.State), + IpcSubscribers.ApplyStateName.Provider(pi, api.State), + IpcSubscribers.ReapplyState.Provider(pi, api.State), + IpcSubscribers.ReapplyStateName.Provider(pi, api.State), + IpcSubscribers.RevertState.Provider(pi, api.State), + IpcSubscribers.RevertStateName.Provider(pi, api.State), + IpcSubscribers.UnlockState.Provider(pi, api.State), + IpcSubscribers.CanUnlock.Provider(pi, api.State), + IpcSubscribers.UnlockStateName.Provider(pi, api.State), + IpcSubscribers.DeletePlayerState.Provider(pi, api.State), + IpcSubscribers.UnlockAll.Provider(pi, api.State), + IpcSubscribers.RevertToAutomation.Provider(pi, api.State), + IpcSubscribers.RevertToAutomationName.Provider(pi, api.State), + IpcSubscribers.AutoReloadGearChanged.Provider(pi, api.State), + IpcSubscribers.StateChanged.Provider(pi, api.State), + IpcSubscribers.StateChangedWithType.Provider(pi, api.State), + IpcSubscribers.StateFinalized.Provider(pi, api.State), + IpcSubscribers.GPoseChanged.Provider(pi, api.State), + ]; + _initializedProvider.Invoke(); + } + + public void Dispose() + { + foreach (var provider in _providers) + provider.Dispose(); + _providers.Clear(); + _initializedProvider.Dispose(); + _disposedProvider.Invoke(); + _disposedProvider.Dispose(); + } +} + + diff --git a/Glamourer/Api/ItemsApi.cs b/Glamourer/Api/ItemsApi.cs new file mode 100644 index 0000000..ac971c9 --- /dev/null +++ b/Glamourer/Api/ItemsApi.cs @@ -0,0 +1,217 @@ +using Glamourer.Api.Api; +using Glamourer.Api.Enums; +using Glamourer.Designs; +using Glamourer.Services; +using Glamourer.State; +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Api; + +public class ItemsApi(ApiHelpers helpers, ItemManager itemManager, StateManager stateManager) : IGlamourerApiItems, IApiService +{ + public GlamourerApiEc SetItem(int objectIndex, ApiEquipSlot slot, ulong itemId, IReadOnlyList stains, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Index", objectIndex, "Slot", slot, "ID", itemId, "Stains", stains, "Key", key, "Flags", flags); + if (!ResolveItem(slot, itemId, out var item)) + return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args); + + if (helpers.FindState(objectIndex) is not { } state) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (!state.ModelData.IsHuman) + return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args); + + if (!state.CanUnlock(key)) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key); + stateManager.ChangeEquip(state, (EquipSlot)slot, item, new StainIds(stains), settings); + ApiHelpers.Lock(state, key, flags); + return GlamourerApiEc.Success; + } + + public GlamourerApiEc SetItemName(string playerName, ApiEquipSlot slot, ulong itemId, IReadOnlyList stains, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Name", playerName, "Slot", slot, "ID", itemId, "Stains", stains, "Key", key, "Flags", flags); + if (!ResolveItem(slot, itemId, out var item)) + return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args); + + var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key); + var anyHuman = false; + var anyFound = false; + var anyUnlocked = false; + foreach (var state in helpers.FindStates(playerName)) + { + anyFound = true; + if (!state.ModelData.IsHuman) + continue; + + anyHuman = true; + if (!state.CanUnlock(key)) + continue; + + anyUnlocked = true; + stateManager.ChangeEquip(state, (EquipSlot)slot, item, new StainIds(stains), settings); + ApiHelpers.Lock(state, key, flags); + } + + if (!anyFound) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (!anyHuman) + return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args); + + if (!anyUnlocked) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc SetBonusItem(int objectIndex, ApiBonusSlot slot, ulong bonusItemId, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Index", objectIndex, "Slot", slot, "ID", bonusItemId, "Key", key, "Flags", flags); + if (!ResolveBonusItem(slot, bonusItemId, out var item)) + return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args); + + if (helpers.FindState(objectIndex) is not { } state) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (!state.ModelData.IsHuman) + return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args); + + if (!state.CanUnlock(key)) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key); + stateManager.ChangeBonusItem(state, item.Type.ToBonus(), item, settings); + ApiHelpers.Lock(state, key, flags); + return GlamourerApiEc.Success; + } + + public GlamourerApiEc SetBonusItemName(string playerName, ApiBonusSlot slot, ulong bonusItemId, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Name", playerName, "Slot", slot, "ID", bonusItemId, "Key", key, "Flags", flags); + if (!ResolveBonusItem(slot, bonusItemId, out var item)) + return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args); + + var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key); + var anyHuman = false; + var anyFound = false; + var anyUnlocked = false; + foreach (var state in helpers.FindStates(playerName)) + { + anyFound = true; + if (!state.ModelData.IsHuman) + continue; + + anyHuman = true; + if (!state.CanUnlock(key)) + continue; + + anyUnlocked = true; + stateManager.ChangeBonusItem(state, item.Type.ToBonus(), item, settings); + ApiHelpers.Lock(state, key, flags); + } + + if (!anyFound) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (!anyHuman) + return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args); + + if (!anyUnlocked) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc SetMetaState(int objectIndex, MetaFlag types, bool newValue, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Index", objectIndex, "MetaTypes", types, "NewValue", newValue, "Key", key, "ApplyFlags", flags); + if (types == 0) + return ApiHelpers.Return(GlamourerApiEc.InvalidState, args); + + if (helpers.FindState(objectIndex) is not { } state) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (!state.ModelData.IsHuman) + return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args); + + if (!state.CanUnlock(key)) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + // Grab MetaIndices from attached flags, and update the states. + var indices = types.ToIndices(); + foreach (var index in indices) + { + stateManager.ChangeMetaState(state, index, newValue, ApplySettings.Manual); + ApiHelpers.Lock(state, key, flags); + } + + return GlamourerApiEc.Success; + } + + public GlamourerApiEc SetMetaStateName(string playerName, MetaFlag types, bool newValue, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Name", playerName, "MetaTypes", types, "NewValue", newValue, "Key", key, "ApplyFlags", flags); + if (types == 0) + return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args); + + var anyHuman = false; + var anyFound = false; + var anyUnlocked = false; + foreach (var state in helpers.FindStates(playerName)) + { + anyFound = true; + if (!state.ModelData.IsHuman) + continue; + + anyHuman = true; + if (!state.CanUnlock(key)) + continue; + + anyUnlocked = true; + // update all MetaStates for this ActorState + foreach (var index in types.ToIndices()) + { + stateManager.ChangeMetaState(state, index, newValue, ApplySettings.Manual); + ApiHelpers.Lock(state, key, flags); + } + } + + if (!anyFound) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (!anyHuman) + return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args); + + if (!anyUnlocked) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + private bool ResolveItem(ApiEquipSlot apiSlot, ulong itemId, out EquipItem item) + { + var id = (CustomItemId)itemId; + var slot = (EquipSlot)apiSlot; + if (id.Id == 0) + id = ItemManager.NothingId(slot); + + item = itemManager.Resolve(slot, id); + return item.Valid; + } + + private bool ResolveBonusItem(ApiBonusSlot apiSlot, ulong itemId, out EquipItem item) + { + var slot = apiSlot switch + { + ApiBonusSlot.Glasses => BonusItemFlag.Glasses, + _ => BonusItemFlag.Unknown, + }; + + return itemManager.IsBonusItemValid(slot, (BonusItemId)itemId, out item); + } +} diff --git a/Glamourer/Api/StateApi.cs b/Glamourer/Api/StateApi.cs new file mode 100644 index 0000000..4ce9c01 --- /dev/null +++ b/Glamourer/Api/StateApi.cs @@ -0,0 +1,452 @@ +using Glamourer.Api.Api; +using Glamourer.Api.Enums; +using Glamourer.Automation; +using Glamourer.Designs; +using Glamourer.Designs.History; +using Glamourer.Events; +using Glamourer.State; +using Newtonsoft.Json.Linq; +using OtterGui.Services; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; +using StateChanged = Glamourer.Events.StateChanged; + +namespace Glamourer.Api; + +public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable +{ + private readonly ApiHelpers _helpers; + private readonly StateManager _stateManager; + private readonly DesignConverter _converter; + private readonly AutoDesignApplier _autoDesigns; + private readonly ActorObjectManager _objects; + private readonly AutoRedrawChanged _autoRedraw; + private readonly StateChanged _stateChanged; + private readonly StateFinalized _stateFinalized; + private readonly GPoseService _gPose; + + public StateApi(ApiHelpers helpers, + StateManager stateManager, + DesignConverter converter, + AutoDesignApplier autoDesigns, + ActorObjectManager objects, + AutoRedrawChanged autoRedraw, + StateChanged stateChanged, + StateFinalized stateFinalized, + GPoseService gPose) + { + _helpers = helpers; + _stateManager = stateManager; + _converter = converter; + _autoDesigns = autoDesigns; + _objects = objects; + _autoRedraw = autoRedraw; + _stateChanged = stateChanged; + _stateFinalized = stateFinalized; + _gPose = gPose; + _autoRedraw.Subscribe(OnAutoRedrawChange, AutoRedrawChanged.Priority.StateApi); + _stateChanged.Subscribe(OnStateChanged, Events.StateChanged.Priority.GlamourerIpc); + _stateFinalized.Subscribe(OnStateFinalized, Events.StateFinalized.Priority.StateApi); + _gPose.Subscribe(OnGPoseChange, GPoseService.Priority.StateApi); + } + + public void Dispose() + { + _autoRedraw.Unsubscribe(OnAutoRedrawChange); + _stateChanged.Unsubscribe(OnStateChanged); + _stateFinalized.Unsubscribe(OnStateFinalized); + _gPose.Unsubscribe(OnGPoseChange); + } + + public (GlamourerApiEc, JObject?) GetState(int objectIndex, uint key) + => Convert(_helpers.FindState(objectIndex), key); + + public (GlamourerApiEc, JObject?) GetStateName(string playerName, uint key) + => Convert(_helpers.FindStates(playerName).FirstOrDefault(), key); + + public (GlamourerApiEc, string?) GetStateBase64(int objectIndex, uint key) + => ConvertBase64(_helpers.FindState(objectIndex), key); + + public (GlamourerApiEc, string?) GetStateBase64Name(string objectName, uint key) + => ConvertBase64(_helpers.FindStates(objectName).FirstOrDefault(), key); + + public GlamourerApiEc ApplyState(object applyState, int objectIndex, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Index", objectIndex, "Key", key, "Flags", flags); + if (Convert(applyState, flags, out var version) is not { } design) + return ApiHelpers.Return(GlamourerApiEc.InvalidState, args); + + if (_helpers.FindState(objectIndex) is not { } state) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (!state.CanUnlock(key)) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + if (version < 3 && state.ModelData.ModelId != 0) + return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args); + + ApplyDesign(state, design, key, flags); + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc ApplyStateName(object applyState, string playerName, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Name", playerName, "Key", key, "Flags", flags); + if (Convert(applyState, flags, out var version) is not { } design) + return ApiHelpers.Return(GlamourerApiEc.InvalidState, args); + + var states = _helpers.FindExistingStates(playerName); + + var any = false; + var anyUnlocked = false; + var anyHuman = false; + foreach (var state in states) + { + any = true; + if (!state.CanUnlock(key)) + continue; + + anyUnlocked = true; + if (version < 3 && state.ModelData.ModelId != 0) + continue; + + anyHuman = true; + ApplyDesign(state, design, key, flags); + } + + if (any) + ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + if (!anyUnlocked) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + if (!anyHuman) + return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args); + + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc ReapplyState(int objectIndex, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Index", objectIndex, "Key", key, "Flags", flags); + if (_helpers.FindExistingState(objectIndex, out var state) is not GlamourerApiEc.Success) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (state is null) + return ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + if (!state.CanUnlock(key)) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + Reapply(_objects.Objects[objectIndex], state, key, flags); + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc ReapplyStateName(string playerName, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Name", playerName, "Key", key, "Flags", flags); + var states = _helpers.FindExistingStates(playerName); + + var any = false; + var anyReapplied = false; + foreach (var state in states) + { + any = true; + if (!state.CanUnlock(key)) + continue; + + anyReapplied = true; + anyReapplied |= Reapply(state, key, flags) is GlamourerApiEc.Success; + } + + if (any) + ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + if (!anyReapplied) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc RevertState(int objectIndex, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Index", objectIndex, "Key", key, "Flags", flags); + if (_helpers.FindExistingState(objectIndex, out var state) != GlamourerApiEc.Success) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (state == null) + return ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + if (!state.CanUnlock(key)) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + Revert(state, key, flags); + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc RevertStateName(string playerName, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Name", playerName, "Key", key, "Flags", flags); + var states = _helpers.FindExistingStates(playerName); + + var any = false; + var anyUnlocked = false; + foreach (var state in states) + { + any = true; + if (!state.CanUnlock(key)) + continue; + + anyUnlocked = true; + Revert(state, key, flags); + } + + if (any) + ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + if (!anyUnlocked) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc UnlockState(int objectIndex, uint key) + { + var args = ApiHelpers.Args("Index", objectIndex, "Key", key); + if (_helpers.FindExistingState(objectIndex, out var state) != GlamourerApiEc.Success) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (state == null) + return ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + if (!state.Unlock(key)) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc CanUnlock(int objectIndex, uint key, out bool isLocked, out bool canUnlock) + { + var args = ApiHelpers.Args("Index", objectIndex, "Key", key); + isLocked = false; + canUnlock = true; + if (_helpers.FindExistingState(objectIndex, out var state) is not GlamourerApiEc.Success) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + if (state is null) + return ApiHelpers.Return(GlamourerApiEc.Success, args); + isLocked = state.IsLocked; + canUnlock = state.CanUnlock(key); + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc UnlockStateName(string playerName, uint key) + { + var args = ApiHelpers.Args("Name", playerName, "Key", key); + var states = _helpers.FindExistingStates(playerName); + + var any = false; + var anyUnlocked = false; + foreach (var state in states) + { + any = true; + anyUnlocked |= state.Unlock(key); + } + + if (any) + ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + if (!anyUnlocked) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc DeletePlayerState(string playerName, ushort worldId, uint key) + { + var args = ApiHelpers.Args("Name", playerName, "World", worldId, "Key", key); + var states = _helpers.FindExistingStates(playerName).ToList(); + if (states.Count is 0) + return ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + var anyLocked = false; + foreach (var state in states) + { + if (state.CanUnlock(key)) + _stateManager.DeleteState(state.Identifier); + else + anyLocked = true; + } + + return ApiHelpers.Return(anyLocked + ? GlamourerApiEc.InvalidKey + : GlamourerApiEc.Success, args); + } + + public int UnlockAll(uint key) + => _stateManager.Values.Count(state => state.Unlock(key)); + + public GlamourerApiEc RevertToAutomation(int objectIndex, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Index", objectIndex, "Key", key, "Flags", flags); + if (_helpers.FindExistingState(objectIndex, out var state) != GlamourerApiEc.Success) + return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (state == null) + return ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + if (!state.CanUnlock(key)) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + RevertToAutomation(_objects.Objects[objectIndex], state, key, flags); + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public GlamourerApiEc RevertToAutomationName(string playerName, uint key, ApplyFlag flags) + { + var args = ApiHelpers.Args("Name", playerName, "Key", key, "Flags", flags); + var states = _helpers.FindExistingStates(playerName); + + var any = false; + var anyUnlocked = false; + var anyReverted = false; + foreach (var state in states) + { + any = true; + if (!state.CanUnlock(key)) + continue; + + anyUnlocked = true; + anyReverted |= RevertToAutomation(state, key, flags) is GlamourerApiEc.Success; + } + + if (any) + ApiHelpers.Return(GlamourerApiEc.NothingDone, args); + + if (!anyReverted) + ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args); + + if (!anyUnlocked) + return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args); + + return ApiHelpers.Return(GlamourerApiEc.Success, args); + } + + public event Action? AutoReloadGearChanged; + public event Action? StateChanged; + public event Action? StateChangedWithType; + public event Action? StateFinalized; + public event Action? GPoseChanged; + + private void ApplyDesign(ActorState state, DesignBase design, uint key, ApplyFlag flags) + { + var once = (flags & ApplyFlag.Once) != 0; + var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true, + ResetMaterials: !once && key != 0, IsFinal: true); + _stateManager.ApplyDesign(state, design, settings); + ApiHelpers.Lock(state, key, flags); + } + + private GlamourerApiEc Reapply(ActorState state, uint key, ApplyFlag flags) + { + if (!_objects.TryGetValue(state.Identifier, out var actors) || !actors.Valid) + return GlamourerApiEc.ActorNotFound; + + foreach (var actor in actors.Objects) + Reapply(actor, state, key, flags); + + return GlamourerApiEc.Success; + } + + private void Reapply(Actor actor, ActorState state, uint key, ApplyFlag flags) + { + var source = flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcFixed : StateSource.IpcManual; + _stateManager.ReapplyState(actor, state, false, source, true); + ApiHelpers.Lock(state, key, flags); + } + + private void Revert(ActorState state, uint key, ApplyFlag flags) + { + var source = flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcFixed : StateSource.IpcManual; + switch (flags & (ApplyFlag.Equipment | ApplyFlag.Customization)) + { + case ApplyFlag.Equipment: _stateManager.ResetEquip(state, source, key); break; + case ApplyFlag.Customization: _stateManager.ResetCustomize(state, source, key); break; + case ApplyFlag.Equipment | ApplyFlag.Customization: _stateManager.ResetState(state, source, key, true); break; + } + + ApiHelpers.Lock(state, key, flags); + } + + private GlamourerApiEc RevertToAutomation(ActorState state, uint key, ApplyFlag flags) + { + if (!_objects.TryGetValue(state.Identifier, out var actors) || !actors.Valid) + return GlamourerApiEc.ActorNotFound; + + foreach (var actor in actors.Objects) + RevertToAutomation(actor, state, key, flags); + + return GlamourerApiEc.Success; + } + + private void RevertToAutomation(Actor actor, ActorState state, uint key, ApplyFlag flags) + { + var source = (flags & ApplyFlag.Once) != 0 ? StateSource.IpcManual : StateSource.IpcFixed; + _autoDesigns.ReapplyAutomation(actor, state.Identifier, state, true, false, out var forcedRedraw); + _stateManager.ReapplyAutomationState(actor, state, forcedRedraw, true, source); + ApiHelpers.Lock(state, key, flags); + } + + private (GlamourerApiEc, JObject?) Convert(ActorState? state, uint key) + { + if (state == null) + return (GlamourerApiEc.ActorNotFound, null); + + if (!state.CanUnlock(key)) + return (GlamourerApiEc.InvalidKey, null); + + return (GlamourerApiEc.Success, _converter.ShareJObject(state, ApplicationRules.All)); + } + + private (GlamourerApiEc, string?) ConvertBase64(ActorState? state, uint key) + { + var (ec, jObj) = Convert(state, key); + return (ec, jObj != null ? DesignConverter.ToBase64(jObj) : null); + } + + private DesignBase? Convert(object? state, ApplyFlag flags, out byte version) + { + version = DesignConverter.Version; + return state switch + { + string s => _converter.FromBase64(s, (flags & ApplyFlag.Customization) != 0, (flags & ApplyFlag.Equipment) != 0, out version), + JObject j => _converter.FromJObject(j, (flags & ApplyFlag.Customization) != 0, (flags & ApplyFlag.Equipment) != 0), + _ => null, + }; + } + + private void OnAutoRedrawChange(bool autoReload) + => AutoReloadGearChanged?.Invoke(autoReload); + + private void OnStateChanged(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, ITransaction? _5) + { + Glamourer.Log.Excessive($"[OnStateChanged] State Changed with Type {type} [Affecting {actors.ToLazyString("nothing")}.]"); + if (StateChanged != null) + foreach (var actor in actors.Objects) + StateChanged.Invoke(actor.Address); + + if (StateChangedWithType != null) + foreach (var actor in actors.Objects) + StateChangedWithType.Invoke(actor.Address, type); + } + + private void OnStateFinalized(StateFinalizationType type, ActorData actors) + { + Glamourer.Log.Verbose($"[OnStateUpdated] State Updated with Type {type}. [Affecting {actors.ToLazyString("nothing")}.]"); + if (StateFinalized != null) + foreach (var actor in actors.Objects) + StateFinalized.Invoke(actor.Address, type); + } + + private void OnGPoseChange(bool gPose) + => GPoseChanged?.Invoke(gPose); +} diff --git a/Glamourer/Automation/ApplicationType.cs b/Glamourer/Automation/ApplicationType.cs new file mode 100644 index 0000000..f72c93f --- /dev/null +++ b/Glamourer/Automation/ApplicationType.cs @@ -0,0 +1,74 @@ +using Glamourer.Api.Enums; +using Glamourer.Designs; +using Glamourer.GameData; +using Penumbra.GameData.Enums; + +namespace Glamourer.Automation; + +[Flags] +public enum ApplicationType : byte +{ + Armor = 0x01, + Customizations = 0x02, + Weapons = 0x04, + GearCustomization = 0x08, + Accessories = 0x10, + + All = Armor | Accessories | Customizations | Weapons | GearCustomization, +} + +public static class ApplicationTypeExtensions +{ + public static readonly IReadOnlyList<(ApplicationType, string)> Types = + [ + (ApplicationType.Customizations, + "Apply all customization changes that are enabled in this design and that are valid in a fixed design and for the given race and gender."), + (ApplicationType.Armor, "Apply all armor piece changes that are enabled in this design and that are valid in a fixed design."), + (ApplicationType.Accessories, "Apply all accessory changes that are enabled in this design and that are valid in a fixed design."), + (ApplicationType.GearCustomization, "Apply all dye and crest changes that are enabled in this design."), + (ApplicationType.Weapons, "Apply all weapon changes that are enabled in this design and that are valid with the current weapon worn."), + ]; + + public static ApplicationCollection Collection(this ApplicationType type) + { + var equipFlags = (type.HasFlag(ApplicationType.Weapons) ? WeaponFlags : 0) + | (type.HasFlag(ApplicationType.Armor) ? ArmorFlags : 0) + | (type.HasFlag(ApplicationType.Accessories) ? AccessoryFlags : 0) + | (type.HasFlag(ApplicationType.GearCustomization) ? StainFlags : 0); + var customizeFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeFlagExtensions.All : 0; + var parameterFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeParameterExtensions.All : 0; + var crestFlags = type.HasFlag(ApplicationType.GearCustomization) ? CrestExtensions.AllRelevant : 0; + var metaFlags = (type.HasFlag(ApplicationType.Armor) ? MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.EarState : 0) + | (type.HasFlag(ApplicationType.Weapons) ? MetaFlag.WeaponState : 0) + | (type.HasFlag(ApplicationType.Customizations) ? MetaFlag.Wetness : 0); + var bonusFlags = type.HasFlag(ApplicationType.Armor) ? BonusExtensions.All : 0; + + return new ApplicationCollection(equipFlags, bonusFlags, customizeFlags, crestFlags, parameterFlags, metaFlags); + } + + public static ApplicationCollection ApplyWhat(this ApplicationType type, IDesignStandIn designStandIn) + { + if(designStandIn is not DesignBase design) + return type.Collection(); + var ret = type.Collection().Restrict(design.Application); + ret.CustomizeRaw = ret.CustomizeRaw.FixApplication(design.CustomizeSet); + return ret; + } + + public const EquipFlag WeaponFlags = EquipFlag.Mainhand | EquipFlag.Offhand; + public const EquipFlag ArmorFlags = EquipFlag.Head | EquipFlag.Body | EquipFlag.Hands | EquipFlag.Legs | EquipFlag.Feet; + public const EquipFlag AccessoryFlags = EquipFlag.Ears | EquipFlag.Neck | EquipFlag.Wrist | EquipFlag.RFinger | EquipFlag.LFinger; + + public const EquipFlag StainFlags = EquipFlag.MainhandStain + | EquipFlag.OffhandStain + | EquipFlag.HeadStain + | EquipFlag.BodyStain + | EquipFlag.HandsStain + | EquipFlag.LegsStain + | EquipFlag.FeetStain + | EquipFlag.EarsStain + | EquipFlag.NeckStain + | EquipFlag.WristStain + | EquipFlag.RFingerStain + | EquipFlag.LFingerStain; +} diff --git a/Glamourer/Automation/AutoDesign.cs b/Glamourer/Automation/AutoDesign.cs index 2004652..e31fb16 100644 --- a/Glamourer/Automation/AutoDesign.cs +++ b/Glamourer/Automation/AutoDesign.cs @@ -1,102 +1,66 @@ -using System; -using Glamourer.Customization; -using Glamourer.Designs; -using Glamourer.Interop.Structs; -using Glamourer.State; -using Glamourer.Structs; +using Glamourer.Designs; +using Glamourer.Designs.Special; +using Glamourer.GameData; using Newtonsoft.Json.Linq; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; namespace Glamourer.Automation; public class AutoDesign { - public const string RevertName = "Revert"; - - [Flags] - public enum Type : byte - { - Armor = 0x01, - Customizations = 0x02, - Weapons = 0x04, - Stains = 0x08, - Accessories = 0x10, - - All = Armor | Accessories | Customizations | Weapons | Stains, - } - - public Design? Design; - public JobGroup Jobs; - public Type ApplicationType; - - public string Name(bool incognito) - => Revert ? RevertName : incognito ? Design!.Incognito : Design!.Name.Text; - - public ref DesignData GetDesignData(ActorState state) - => ref Design == null ? ref state.BaseData : ref Design.DesignData; - - public bool Revert - => Design == null; + public IDesignStandIn Design = new RevertDesign(); + public JobGroup Jobs; + public ApplicationType Type; + public short GearsetIndex = -1; public AutoDesign Clone() => new() { - Design = Design, - ApplicationType = ApplicationType, - Jobs = Jobs, + Design = Design, + Type = Type, + Jobs = Jobs, + GearsetIndex = GearsetIndex, }; public unsafe bool IsActive(Actor actor) - => actor.IsCharacter && Jobs.Fits(actor.AsCharacter->CharacterData.ClassJob); - - public JObject Serialize() - => new() - { - ["Design"] = Design?.Identifier.ToString(), - ["ApplicationType"] = (uint)ApplicationType, - ["Conditions"] = CreateConditionObject(), - }; - - private JObject CreateConditionObject() { - var ret = new JObject(); - if (Jobs.Id != 0) - ret["JobGroup"] = Jobs.Id; + if (!actor.IsCharacter) + return false; + + var ret = true; + if (GearsetIndex < 0) + ret &= Jobs.Fits(actor.AsCharacter->CharacterData.ClassJob); + else + ret &= AutoDesignApplier.CheckGearset(GearsetIndex); + return ret; } - public (EquipFlag Equip, CustomizeFlag Customize, bool ApplyHat, bool ApplyVisor, bool ApplyWeapon, bool ApplyWet) ApplyWhat() + public JObject Serialize() { - var equipFlags = (ApplicationType.HasFlag(Type.Weapons) ? WeaponFlags : 0) - | (ApplicationType.HasFlag(Type.Armor) ? ArmorFlags : 0) - | (ApplicationType.HasFlag(Type.Accessories) ? AccessoryFlags : 0) - | (ApplicationType.HasFlag(Type.Stains) ? StainFlags : 0); - var customizeFlags = ApplicationType.HasFlag(Type.Customizations) ? CustomizeFlagExtensions.All : 0; - - if (Revert) - return (equipFlags, customizeFlags, ApplicationType.HasFlag(Type.Armor), ApplicationType.HasFlag(Type.Armor), - ApplicationType.HasFlag(Type.Weapons), ApplicationType.HasFlag(Type.Customizations)); - - return (equipFlags & Design!.ApplyEquip, customizeFlags & Design.ApplyCustomize, - ApplicationType.HasFlag(Type.Armor) && Design.DoApplyHatVisible(), - ApplicationType.HasFlag(Type.Armor) && Design.DoApplyVisorToggle(), - ApplicationType.HasFlag(Type.Weapons) && Design.DoApplyWeaponVisible(), - ApplicationType.HasFlag(Type.Customizations) && Design.DoApplyWetness()); + var ret = new JObject + { + ["Design"] = Design.SerializeName(), + ["Type"] = (uint)Type, + ["Conditions"] = CreateConditionObject(), + }; + Design.AddData(ret); + return ret; } - public const EquipFlag WeaponFlags = EquipFlag.Mainhand | EquipFlag.Offhand; - public const EquipFlag ArmorFlags = EquipFlag.Head | EquipFlag.Body | EquipFlag.Hands | EquipFlag.Legs | EquipFlag.Feet; - public const EquipFlag AccessoryFlags = EquipFlag.Ears | EquipFlag.Neck | EquipFlag.Wrist | EquipFlag.RFinger | EquipFlag.LFinger; + private JObject CreateConditionObject() + { + var ret = new JObject + { + ["Gearset"] = GearsetIndex, + ["JobGroup"] = Jobs.Id.Id, + }; - public const EquipFlag StainFlags = EquipFlag.MainhandStain - | EquipFlag.OffhandStain - | EquipFlag.HeadStain - | EquipFlag.BodyStain - | EquipFlag.HandsStain - | EquipFlag.LegsStain - | EquipFlag.FeetStain - | EquipFlag.EarsStain - | EquipFlag.NeckStain - | EquipFlag.WristStain - | EquipFlag.RFingerStain - | EquipFlag.LFingerStain; + return ret; + } + + public ApplicationCollection ApplyWhat() + => Type.ApplyWhat(Design); } diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index f68c3c1..a61a004 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -1,114 +1,118 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Dalamud.Plugin.Services; -using Glamourer.Customization; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; using Glamourer.Designs; +using Glamourer.Designs.Links; using Glamourer.Events; using Glamourer.Interop; -using Glamourer.Interop.Structs; -using Glamourer.Services; +using Glamourer.Interop.Material; using Glamourer.State; -using Glamourer.Structs; -using Glamourer.Unlocks; -using OtterGui.Classes; using Penumbra.GameData.Actors; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.Automation; -public class AutoDesignApplier : IDisposable +public sealed class AutoDesignApplier : IDisposable { - private readonly Configuration _config; - private readonly AutoDesignManager _manager; - private readonly StateManager _state; - private readonly JobService _jobs; - private readonly ActorService _actors; - private readonly CustomizationService _customizations; - private readonly CustomizeUnlockManager _customizeUnlocks; - private readonly ItemUnlockManager _itemUnlocks; - private readonly AutomationChanged _event; - private readonly ObjectManager _objects; - private readonly WeaponLoading _weapons; - private readonly HumanModelList _humans; - private readonly IClientState _clientState; + private readonly Configuration _config; + private readonly AutoDesignManager _manager; + private readonly StateManager _state; + private readonly JobService _jobs; + private readonly EquippedGearset _equippedGearset; + private readonly ActorManager _actors; + private readonly AutomationChanged _event; + private readonly ActorObjectManager _objects; + private readonly WeaponLoading _weapons; + private readonly HumanModelList _humans; + private readonly DesignMerger _designMerger; + private readonly IClientState _clientState; - private ActorState? _jobChangeState; - private readonly Dictionary _jobChangeMainhand = new(); - private readonly Dictionary _jobChangeOffhand = new(); + private readonly JobChangeState _jobChangeState; - private void ResetJobChange() + public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, ActorManager actors, + AutomationChanged @event, ActorObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState, + EquippedGearset equippedGearset, DesignMerger designMerger, JobChangeState jobChangeState) { - _jobChangeState = null; - _jobChangeMainhand.Clear(); - _jobChangeOffhand.Clear(); - } - - public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, - CustomizationService customizations, ActorService actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks, - AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState) - { - _config = config; - _manager = manager; - _state = state; - _jobs = jobs; - _customizations = customizations; - _actors = actors; - _itemUnlocks = itemUnlocks; - _customizeUnlocks = customizeUnlocks; - _event = @event; - _objects = objects; - _weapons = weapons; - _humans = humans; - _clientState = clientState; - _jobs.JobChanged += OnJobChange; + _config = config; + _manager = manager; + _state = state; + _jobs = jobs; + _actors = actors; + _event = @event; + _objects = objects; + _weapons = weapons; + _humans = humans; + _clientState = clientState; + _equippedGearset = equippedGearset; + _designMerger = designMerger; + _jobChangeState = jobChangeState; + _jobs.JobChanged += OnJobChange; _event.Subscribe(OnAutomationChange, AutomationChanged.Priority.AutoDesignApplier); _weapons.Subscribe(OnWeaponLoading, WeaponLoading.Priority.AutoDesignApplier); + _equippedGearset.Subscribe(OnEquippedGearset, EquippedGearset.Priority.AutoDesignApplier); + } + + public void OnEnableAutoDesignsChanged(bool value) + { + if (value) + return; + + foreach (var state in _state.Values) + state.Sources.RemoveFixedDesignSources(); } public void Dispose() { _weapons.Unsubscribe(OnWeaponLoading); _event.Unsubscribe(OnAutomationChange); + _equippedGearset.Unsubscribe(OnEquippedGearset); _jobs.JobChanged -= OnJobChange; } - private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref weapon) + private void OnWeaponLoading(Actor actor, EquipSlot slot, ref CharacterWeapon weapon) { - if (_jobChangeState == null || !_config.EnableAutoDesigns) + if (!_jobChangeState.HasState || !_config.EnableAutoDesigns) return; - var id = actor.GetIdentifier(_actors.AwaitedService); + var id = actor.GetIdentifier(_actors); if (id == _jobChangeState.Identifier) { - var current = _jobChangeState.BaseData.Item(slot); - if (slot is EquipSlot.MainHand) + var state = _jobChangeState.State!; + var current = state.BaseData.Item(slot); + switch (slot) { - if (_jobChangeMainhand.TryGetValue(current.Type, out var data)) + case EquipSlot.MainHand: { - Glamourer.Log.Verbose($"Changing Mainhand from {_jobChangeState.ModelData.Weapon(EquipSlot.MainHand)} | {_jobChangeState.BaseData.Weapon(EquipSlot.MainHand)} to {data.Item1} for 0x{actor.Address:X}."); - _state.ChangeItem(_jobChangeState, EquipSlot.MainHand, data.Item1, data.Item2); - weapon.Value = _jobChangeState.ModelData.Weapon(EquipSlot.MainHand); - } - } - else if (slot is EquipSlot.OffHand && current.Type == _jobChangeState.BaseData.MainhandType.Offhand()) - { - if (_jobChangeOffhand.TryGetValue(current.Type, out var data)) - { - Glamourer.Log.Verbose($"Changing Offhand from {_jobChangeState.ModelData.Weapon(EquipSlot.OffHand)} | {_jobChangeState.BaseData.Weapon(EquipSlot.OffHand)} to {data.Item1} for 0x{actor.Address:X}."); - _state.ChangeItem(_jobChangeState, EquipSlot.OffHand, data.Item1, data.Item2); - weapon.Value = _jobChangeState.ModelData.Weapon(EquipSlot.OffHand); - } + if (_jobChangeState.TryGetValue(current.Type, actor.Job, false, out var data)) + { + Glamourer.Log.Verbose( + $"Changing Mainhand from {state.ModelData.Weapon(EquipSlot.MainHand)} | {state.BaseData.Weapon(EquipSlot.MainHand)} to {data.Item1} for 0x{actor.Address:X}."); + _state.ChangeItem(state, EquipSlot.MainHand, data.Item1, new ApplySettings(Source: data.Item2)); + weapon = state.ModelData.Weapon(EquipSlot.MainHand); + } - ResetJobChange(); + break; + } + case EquipSlot.OffHand when current.Type == state.BaseData.MainhandType.Offhand(): + { + if (_jobChangeState.TryGetValue(current.Type, actor.Job, false, out var data)) + { + Glamourer.Log.Verbose( + $"Changing Offhand from {state.ModelData.Weapon(EquipSlot.OffHand)} | {state.BaseData.Weapon(EquipSlot.OffHand)} to {data.Item1} for 0x{actor.Address:X}."); + _state.ChangeItem(state, EquipSlot.OffHand, data.Item1, new ApplySettings(Source: data.Item2)); + weapon = state.ModelData.Weapon(EquipSlot.OffHand); + } + + _jobChangeState.Reset(); + break; + } } } else { - ResetJobChange(); + _jobChangeState.Reset(); } } @@ -117,57 +121,6 @@ public class AutoDesignApplier : IDisposable if (!_config.EnableAutoDesigns || set == null) return; - void RemoveOld(ActorIdentifier[]? identifiers) - { - if (identifiers == null) - return; - - foreach (var id in identifiers) - { - if (id.Type is IdentifierType.Player && id.HomeWorld == WorldId.AnyWorld) - foreach (var state in _state.Where(kvp => kvp.Key.PlayerName == id.PlayerName).Select(kvp => kvp.Value)) - state.RemoveFixedDesignSources(); - else if (_state.TryGetValue(id, out var state)) - state.RemoveFixedDesignSources(); - } - } - - void ApplyNew(AutoDesignSet? newSet) - { - if (newSet is not { Enabled: true }) - return; - - _objects.Update(); - foreach (var id in newSet.Identifiers) - { - if (_objects.TryGetValue(id, out var data)) - { - if (_state.GetOrCreate(id, data.Objects[0], out var state)) - { - Reduce(data.Objects[0], state, newSet, false, false); - foreach (var actor in data.Objects) - _state.ReapplyState(actor); - } - } - else if (_objects.TryGetValueAllWorld(id, out data) || _objects.TryGetValueNonOwned(id, out data)) - { - foreach (var actor in data.Objects) - { - var specificId = actor.GetIdentifier(_actors.AwaitedService); - if (_state.GetOrCreate(specificId, actor, out var state)) - { - Reduce(actor, state, newSet, false, false); - _state.ReapplyState(actor); - } - } - } - else if (_state.TryGetValue(id, out var state)) - { - state.RemoveFixedDesignSources(); - } - } - } - switch (type) { case AutomationChanged.Type.ToggleSet when !set.Enabled: @@ -177,7 +130,7 @@ public class AutoDesignApplier : IDisposable break; case AutomationChanged.Type.ChangeIdentifier when set.Enabled: // Remove fixed state from the old identifiers assigned and the old enabled set, if any. - var (oldIds, _, oldSet) = ((ActorIdentifier[], ActorIdentifier, AutoDesignSet?))bonusData!; + var (oldIds, _, _) = ((ActorIdentifier[], ActorIdentifier, AutoDesignSet?))bonusData!; RemoveOld(oldIds); ApplyNew(set); // Does not need to disable oldSet because same identifiers. break; @@ -188,24 +141,77 @@ public class AutoDesignApplier : IDisposable case AutomationChanged.Type.ChangedDesign: case AutomationChanged.Type.ChangedConditions: case AutomationChanged.Type.ChangedType: + case AutomationChanged.Type.ChangedData: ApplyNew(set); break; } + + return; + + void ApplyNew(AutoDesignSet? newSet) + { + if (newSet is not { Enabled: true }) + return; + + foreach (var id in newSet.Identifiers) + { + if (_objects.TryGetValue(id, out var data)) + { + if (_state.GetOrCreate(id, data.Objects[0], out var state)) + { + Reduce(data.Objects[0], state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw); + foreach (var actor in data.Objects) + _state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed); + } + } + else if (_objects.TryGetValueAllWorld(id, out data) || _objects.TryGetValueNonOwned(id, out data)) + { + foreach (var actor in data.Objects) + { + var specificId = actor.GetIdentifier(_actors); + if (_state.GetOrCreate(specificId, actor, out var state)) + { + Reduce(actor, state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw); + _state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed); + } + } + } + else if (_state.TryGetValue(id, out var state)) + { + state.Sources.RemoveFixedDesignSources(); + } + } + } + + void RemoveOld(ActorIdentifier[]? identifiers) + { + if (identifiers == null) + return; + + foreach (var id in identifiers) + { + if (id.Type is IdentifierType.Player && id.HomeWorld == WorldId.AnyWorld) + foreach (var state in _state.Where(kvp => kvp.Key.PlayerName == id.PlayerName).Select(kvp => kvp.Value)) + state.Sources.RemoveFixedDesignSources(); + else if (_state.TryGetValue(id, out var state)) + state.Sources.RemoveFixedDesignSources(); + } + } } private void OnJobChange(Actor actor, Job oldJob, Job newJob) { - if (!_config.EnableAutoDesigns || !actor.Identifier(_actors.AwaitedService, out var id)) + if (!_config.EnableAutoDesigns || !actor.Identifier(_actors, out var id)) return; if (!GetPlayerSet(id, out var set)) { if (_state.TryGetValue(id, out var s)) - s.LastJob = (byte)newJob.Id; + s.LastJob = newJob.Id; return; } - if (!_state.TryGetValue(id, out var state)) + if (!_state.GetOrCreate(actor, out var state)) return; if (oldJob.Id == newJob.Id && state.LastJob == newJob.Id) @@ -213,19 +219,21 @@ public class AutoDesignApplier : IDisposable var respectManual = state.LastJob == newJob.Id; state.LastJob = actor.Job; - Reduce(actor, state, set, respectManual, true); - _state.ReapplyState(actor); + Reduce(actor, state, set, respectManual, true, true, out var forcedRedraw); + _state.ReapplyState(actor, forcedRedraw, StateSource.Fixed); } - public void ReapplyAutomation(Actor actor, ActorIdentifier identifier, ActorState state) + public void ReapplyAutomation(Actor actor, ActorIdentifier identifier, ActorState state, bool reset, bool forcedNew, out bool forcedRedraw) { + forcedRedraw = false; if (!_config.EnableAutoDesigns) return; - if (!GetPlayerSet(identifier, out var set)) - return; + if (reset) + _state.ResetState(state, StateSource.Game); - Reduce(actor, state, set, false, false); + if (GetPlayerSet(identifier, out var set)) + Reduce(actor, state, set, false, false, forcedNew, out forcedRedraw); } public bool Reduce(Actor actor, ActorIdentifier identifier, [NotNullWhen(true)] out ActorState? state) @@ -233,9 +241,6 @@ public class AutoDesignApplier : IDisposable AutoDesignSet set; if (!_state.TryGetValue(identifier, out state)) { - if (!_config.EnableAutoDesigns) - return false; - if (!GetPlayerSet(identifier, out set!)) return false; @@ -245,70 +250,91 @@ public class AutoDesignApplier : IDisposable else if (!GetPlayerSet(identifier, out set!)) { if (state.UpdateTerritory(_clientState.TerritoryType) && _config.RevertManualChangesOnZoneChange) - _state.ResetState(state, StateChanged.Source.Game); + _state.ResetState(state, StateSource.Game); return true; } var respectManual = !state.UpdateTerritory(_clientState.TerritoryType) || !_config.RevertManualChangesOnZoneChange; if (!respectManual) - _state.ResetState(state, StateChanged.Source.Game); - Reduce(actor, state, set, respectManual, false); + _state.ResetState(state, StateSource.Game); + Reduce(actor, state, set, respectManual, false, false, out _); return true; } - private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set, bool respectManual, bool fromJobChange) + private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set, bool respectManual, bool fromJobChange, bool newApplication, + out bool forcedRedraw) { - EquipFlag totalEquipFlags = 0; - CustomizeFlag totalCustomizeFlags = 0; - byte totalMetaFlags = 0; - if (set.BaseState == AutoDesignSet.Base.Game) - _state.ResetStateFixed(state); - else if (!respectManual) - state.RemoveFixedDesignSources(); - - if (!_humans.IsHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId)) - return; - - foreach (var design in set.Designs) + if (set.BaseState is AutoDesignSet.Base.Game) { - if (!design.IsActive(actor)) - continue; - - if (design.ApplicationType is 0) - continue; - - ref var data = ref design.GetDesignData(state); - var source = design.Revert ? StateChanged.Source.Game : StateChanged.Source.Fixed; - - if (!data.IsHuman) - continue; - - var (equipFlags, customizeFlags, applyHat, applyVisor, applyWeapon, applyWet) = design.ApplyWhat(); - ReduceMeta(state, data, applyHat, applyVisor, applyWeapon, applyWet, ref totalMetaFlags, respectManual, source); - ReduceCustomize(state, data, customizeFlags, ref totalCustomizeFlags, respectManual, source); - ReduceEquip(state, data, equipFlags, ref totalEquipFlags, respectManual, source, fromJobChange); + _state.ResetStateFixed(state, respectManual); + } + else if (!respectManual) + { + state.Sources.RemoveFixedDesignSources(); + for (var i = 0; i < state.Materials.Values.Count; ++i) + { + var (key, value) = state.Materials.Values[i]; + if (value.Source is StateSource.Fixed) + state.Materials.UpdateValue(key, new MaterialValueState(value.Game, value.Model, value.DrawData, StateSource.Manual), + out _); + } } - if (totalCustomizeFlags != 0) - state.ModelData.ModelId = 0; + forcedRedraw = false; + if (!_humans.IsHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId)) + return; + + if (actor.IsTransformed) + return; + + var mergedDesign = _designMerger.Merge( + set.Designs.Where(d => d.IsActive(actor)) + .SelectMany(d => d.Design.AllLinks(newApplication).Select(l => (l.Design, l.Flags & d.Type, d.Jobs.Flags))), + state.ModelData.Customize, state.BaseData, true, _config.AlwaysApplyAssociatedMods); + + if (_objects.IsInGPose && actor.IsGPoseOrCutscene) + { + mergedDesign.ResetTemporarySettings = false; + mergedDesign.AssociatedMods.Clear(); + } + else if (set.ResetTemporarySettings) + { + mergedDesign.ResetTemporarySettings = true; + } + + _state.ApplyDesign(state, mergedDesign, new ApplySettings(0, StateSource.Fixed, respectManual, fromJobChange, false, false, false)); + forcedRedraw = mergedDesign.ForcedRedraw; } - /// Get world-specific first and all-world afterwards. + /// Get world-specific first and all-world afterward. private bool GetPlayerSet(ActorIdentifier identifier, [NotNullWhen(true)] out AutoDesignSet? set) { + if (!_config.EnableAutoDesigns) + { + set = null; + return false; + } + switch (identifier.Type) { case IdentifierType.Player: if (_manager.EnabledSets.TryGetValue(identifier, out set)) return true; - identifier = _actors.AwaitedService.CreatePlayer(identifier.PlayerName, ushort.MaxValue); + identifier = _actors.CreatePlayer(identifier.PlayerName, WorldId.AnyWorld); return _manager.EnabledSets.TryGetValue(identifier, out set); case IdentifierType.Retainer: case IdentifierType.Npc: return _manager.EnabledSets.TryGetValue(identifier, out set); case IdentifierType.Owned: - identifier = _actors.AwaitedService.CreateNpc(identifier.Kind, identifier.DataId); + if (_manager.EnabledSets.TryGetValue(identifier, out set)) + return true; + + identifier = _actors.CreateOwned(identifier.PlayerName, WorldId.AnyWorld, identifier.Kind, identifier.DataId); + if (_manager.EnabledSets.TryGetValue(identifier, out set)) + return true; + + identifier = _actors.CreateNpc(identifier.Kind, identifier.DataId); return _manager.EnabledSets.TryGetValue(identifier, out set); default: set = null; @@ -316,182 +342,37 @@ public class AutoDesignApplier : IDisposable } } - private void ReduceEquip(ActorState state, in DesignData design, EquipFlag equipFlags, ref EquipFlag totalEquipFlags, bool respectManual, - StateChanged.Source source, bool fromJobChange) + internal static int NewGearsetId = -1; + + private void OnEquippedGearset(string name, int id, int prior, byte _, byte job) { - equipFlags &= ~totalEquipFlags; - if (equipFlags == 0) + if (!_config.EnableAutoDesigns) return; - foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - var flag = slot.ToFlag(); - if (equipFlags.HasFlag(flag)) - { - var item = design.Item(slot); - if (!_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _)) - { - if (!respectManual || state[slot, false] is not StateChanged.Source.Manual) - _state.ChangeItem(state, slot, item, source); - totalEquipFlags |= flag; - } - } + var (player, data) = _objects.PlayerData; + if (!player.IsValid) + return; - var stainFlag = slot.ToStainFlag(); - if (equipFlags.HasFlag(stainFlag)) - { - if (!respectManual || state[slot, true] is not StateChanged.Source.Manual) - _state.ChangeStain(state, slot, design.Stain(slot), source); - totalEquipFlags |= stainFlag; - } - } + if (!GetPlayerSet(player, out var set) || !_state.TryGetValue(player, out var state)) + return; - if (equipFlags.HasFlag(EquipFlag.Mainhand)) - { - var item = design.Item(EquipSlot.MainHand); - var checkUnlock = !_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _); - var checkState = !respectManual || state[EquipSlot.MainHand, false] is not StateChanged.Source.Manual; - if (checkUnlock && checkState) - { - if (fromJobChange) - { - _jobChangeMainhand.TryAdd(item.Type, (item, source)); - _jobChangeState = state; - } - else if (state.ModelData.Item(EquipSlot.MainHand).Type == item.Type) - { - _state.ChangeItem(state, EquipSlot.MainHand, item, source); - totalEquipFlags |= EquipFlag.Mainhand; - } - } - } - - if (equipFlags.HasFlag(EquipFlag.Offhand)) - { - var item = design.Item(EquipSlot.OffHand); - var checkUnlock = !_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _); - var checkState = !respectManual || state[EquipSlot.OffHand, false] is not StateChanged.Source.Manual; - if (checkUnlock && checkState) - { - if (fromJobChange) - { - _jobChangeOffhand.TryAdd(item.Type, (item, source)); - _jobChangeState = state; - } - else if (state.ModelData.Item(EquipSlot.OffHand).Type == item.Type) - { - _state.ChangeItem(state, EquipSlot.OffHand, item, source); - totalEquipFlags |= EquipFlag.Offhand; - } - } - } - - if (equipFlags.HasFlag(EquipFlag.MainhandStain)) - { - if (!respectManual || state[EquipSlot.MainHand, true] is not StateChanged.Source.Manual) - _state.ChangeStain(state, EquipSlot.MainHand, design.Stain(EquipSlot.MainHand), source); - totalEquipFlags |= EquipFlag.MainhandStain; - } - - if (equipFlags.HasFlag(EquipFlag.OffhandStain)) - { - if (!respectManual || state[EquipSlot.OffHand, true] is not StateChanged.Source.Manual) - _state.ChangeStain(state, EquipSlot.OffHand, design.Stain(EquipSlot.OffHand), source); - totalEquipFlags |= EquipFlag.OffhandStain; - } + var respectManual = prior == id; + NewGearsetId = id; + Reduce(data.Objects[0], state, set, respectManual, job != state.LastJob, prior == id, out var forcedRedraw); + NewGearsetId = -1; + foreach (var actor in data.Objects) + _state.ReapplyState(actor, forcedRedraw, StateSource.Fixed); } - private void ReduceCustomize(ActorState state, in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag totalCustomizeFlags, - bool respectManual, StateChanged.Source source) + public static unsafe bool CheckGearset(short check) { - customizeFlags &= ~totalCustomizeFlags; - if (customizeFlags == 0) - return; + if (NewGearsetId != -1) + return check == NewGearsetId; - var customize = state.ModelData.Customize; - CustomizeFlag fixFlags = 0; + var module = RaptureGearsetModule.Instance(); + if (module == null) + return false; - // Skip anything not human. - if (!state.ModelData.IsHuman || !design.IsHuman) - return; - - if (customizeFlags.HasFlag(CustomizeFlag.Clan)) - { - if (!respectManual || state[CustomizeIndex.Clan] is not StateChanged.Source.Manual) - fixFlags |= _customizations.ChangeClan(ref customize, design.Customize.Clan); - customizeFlags &= ~(CustomizeFlag.Clan | CustomizeFlag.Race); - totalCustomizeFlags |= CustomizeFlag.Clan | CustomizeFlag.Race; - } - - if (customizeFlags.HasFlag(CustomizeFlag.Gender)) - { - if (!respectManual || state[CustomizeIndex.Gender] is not StateChanged.Source.Manual) - fixFlags |= _customizations.ChangeGender(ref customize, design.Customize.Gender); - customizeFlags &= ~CustomizeFlag.Gender; - totalCustomizeFlags |= CustomizeFlag.Gender; - } - - if (fixFlags != 0) - _state.ChangeCustomize(state, customize, fixFlags, source); - - if (customizeFlags.HasFlag(CustomizeFlag.Face)) - { - if (!respectManual || state[CustomizeIndex.Face] is not StateChanged.Source.Manual) - _state.ChangeCustomize(state, CustomizeIndex.Face, design.Customize.Face, source); - customizeFlags &= ~CustomizeFlag.Face; - totalCustomizeFlags |= CustomizeFlag.Face; - } - - var set = _customizations.AwaitedService.GetList(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender); - var face = state.ModelData.Customize.Face; - foreach (var index in Enum.GetValues()) - { - var flag = index.ToFlag(); - if (!customizeFlags.HasFlag(flag)) - continue; - - var value = design.Customize[index]; - if (CustomizationService.IsCustomizationValid(set, face, index, value, out var data)) - { - if (data.HasValue && _config.UnlockedItemMode && !_customizeUnlocks.IsUnlocked(data.Value, out _)) - continue; - - if (!respectManual || state[index] is not StateChanged.Source.Manual) - _state.ChangeCustomize(state, index, value, source); - totalCustomizeFlags |= flag; - } - } - } - - private void ReduceMeta(ActorState state, in DesignData design, bool applyHat, bool applyVisor, bool applyWeapon, bool applyWet, - ref byte totalMetaFlags, bool respectManual, StateChanged.Source source) - { - if (applyHat && (totalMetaFlags & 0x01) == 0) - { - if (!respectManual || state[ActorState.MetaIndex.HatState] is not StateChanged.Source.Manual) - _state.ChangeHatState(state, design.IsHatVisible(), source); - totalMetaFlags |= 0x01; - } - - if (applyVisor && (totalMetaFlags & 0x02) == 0) - { - if (!respectManual || state[ActorState.MetaIndex.VisorState] is not StateChanged.Source.Manual) - _state.ChangeVisorState(state, design.IsVisorToggled(), source); - totalMetaFlags |= 0x02; - } - - if (applyWeapon && (totalMetaFlags & 0x04) == 0) - { - if (!respectManual || state[ActorState.MetaIndex.WeaponState] is not StateChanged.Source.Manual) - _state.ChangeWeaponState(state, design.IsWeaponVisible(), source); - totalMetaFlags |= 0x04; - } - - if (applyWet && (totalMetaFlags & 0x08) == 0) - { - if (!respectManual || state[ActorState.MetaIndex.Wetness] is not StateChanged.Source.Manual) - _state.ChangeWetness(state, design.IsWet(), source); - totalMetaFlags |= 0x08; - } + return check == module->CurrentGearsetIndex; } } diff --git a/Glamourer/Automation/AutoDesignManager.cs b/Glamourer/Automation/AutoDesignManager.cs index 8140084..7a4511b 100644 --- a/Glamourer/Automation/AutoDesignManager.cs +++ b/Glamourer/Automation/AutoDesignManager.cs @@ -1,22 +1,20 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Interface.ImGuiNotification; using Glamourer.Designs; +using Glamourer.Designs.History; +using Glamourer.Designs.Special; using Glamourer.Events; using Glamourer.Interop; using Glamourer.Services; -using Glamourer.Structs; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Filesystem; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Glamourer.Automation; @@ -26,27 +24,32 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos private readonly SaveService _saveService; - private readonly JobService _jobs; - private readonly DesignManager _designs; - private readonly ActorService _actors; - private readonly AutomationChanged _event; - private readonly DesignChanged _designEvent; + private readonly JobService _jobs; + private readonly DesignManager _designs; + private readonly ActorManager _actors; + private readonly AutomationChanged _event; + private readonly DesignChanged _designEvent; + private readonly RandomDesignGenerator _randomDesigns; + private readonly QuickSelectedDesign _quickSelectedDesign; - private readonly List _data = new(); - private readonly Dictionary _enabled = new(); + private readonly List _data = []; + private readonly Dictionary _enabled = []; public IReadOnlyDictionary EnabledSets => _enabled; - public AutoDesignManager(JobService jobs, ActorService actors, SaveService saveService, DesignManager designs, AutomationChanged @event, - FixedDesignMigrator migrator, DesignFileSystem fileSystem, DesignChanged designEvent) + public AutoDesignManager(JobService jobs, ActorManager actors, SaveService saveService, DesignManager designs, AutomationChanged @event, + FixedDesignMigrator migrator, DesignFileSystem fileSystem, DesignChanged designEvent, RandomDesignGenerator randomDesigns, + QuickSelectedDesign quickSelectedDesign) { - _jobs = jobs; - _actors = actors; - _saveService = saveService; - _designs = designs; - _event = @event; - _designEvent = designEvent; + _jobs = jobs; + _actors = actors; + _saveService = saveService; + _designs = designs; + _event = @event; + _designEvent = designEvent; + _randomDesigns = randomDesigns; + _quickSelectedDesign = quickSelectedDesign; _designEvent.Subscribe(OnDesignChange, DesignChanged.Priority.AutoDesignManager); Load(); migrator.ConsumeMigratedData(_actors, fileSystem, this); @@ -232,18 +235,34 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos _event.Invoke(AutomationChanged.Type.ChangedBase, set, (old, newBase)); } - public void AddDesign(AutoDesignSet set, Design? design) + public void ChangeResetSettings(int whichSet, bool newValue) + { + if (whichSet >= _data.Count || whichSet < 0) + return; + + var set = _data[whichSet]; + if (newValue == set.ResetTemporarySettings) + return; + + var old = set.ResetTemporarySettings; + set.ResetTemporarySettings = newValue; + Save(); + Glamourer.Log.Debug($"Changed resetting of temporary settings of set {whichSet + 1} from {old} to {newValue}."); + _event.Invoke(AutomationChanged.Type.ChangedTemporarySettingsReset, set, newValue); + } + + public void AddDesign(AutoDesignSet set, IDesignStandIn design) { var newDesign = new AutoDesign() { - Design = design, - ApplicationType = AutoDesign.Type.All, - Jobs = _jobs.JobGroups[1], + Design = design, + Type = ApplicationType.All, + Jobs = _jobs.JobGroups[1], }; set.Designs.Add(newDesign); Save(); Glamourer.Log.Debug( - $"Added new associated design {design?.Identifier.ToString() ?? "Reverter"} as design {set.Designs.Count} to design set."); + $"Added new associated design {design.ResolveName(true)} as design {set.Designs.Count} to design set."); _event.Invoke(AutomationChanged.Type.AddedDesign, set, set.Designs.Count - 1); } @@ -283,20 +302,20 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos _event.Invoke(AutomationChanged.Type.MovedDesign, set, (from, to)); } - public void ChangeDesign(AutoDesignSet set, int which, Design? newDesign) + public void ChangeDesign(AutoDesignSet set, int which, IDesignStandIn newDesign) { if (which >= set.Designs.Count || which < 0) return; var design = set.Designs[which]; - if (design.Design?.Identifier == newDesign?.Identifier) + if (design.Design.Equals(newDesign)) return; var old = design.Design; design.Design = newDesign; Save(); Glamourer.Log.Debug( - $"Changed linked design from {old?.Identifier.ToString() ?? "Reverter"} to {newDesign?.Identifier.ToString() ?? "Reverter"} for associated design {which + 1} in design set."); + $"Changed linked design from {old.ResolveName(true)} to {newDesign.ResolveName(true)} for associated design {which + 1} in design set."); _event.Invoke(AutomationChanged.Type.ChangedDesign, set, (which, old, newDesign)); } @@ -306,6 +325,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos return; var design = set.Designs[which]; + if (design.Jobs.Id == jobs.Id) return; @@ -316,21 +336,51 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos _event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, jobs)); } - public void ChangeApplicationType(AutoDesignSet set, int which, AutoDesign.Type type) + public void ChangeGearsetCondition(AutoDesignSet set, int which, short index) { if (which >= set.Designs.Count || which < 0) return; - type &= AutoDesign.Type.All; var design = set.Designs[which]; - if (design.ApplicationType == type) + if (design.GearsetIndex == index) return; - var old = design.ApplicationType; - design.ApplicationType = type; + var old = design.GearsetIndex; + design.GearsetIndex = index; Save(); - Glamourer.Log.Debug($"Changed application type from {old} to {type} for associated design {which + 1} in design set."); - _event.Invoke(AutomationChanged.Type.ChangedType, set, (which, old, type)); + Glamourer.Log.Debug($"Changed gearset condition from {old} to {index} for associated design {which + 1} in design set."); + _event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, index)); + } + + public void ChangeApplicationType(AutoDesignSet set, int which, ApplicationType applicationType) + { + if (which >= set.Designs.Count || which < 0) + return; + + applicationType &= ApplicationType.All; + var design = set.Designs[which]; + if (design.Type == applicationType) + return; + + var old = design.Type; + design.Type = applicationType; + Save(); + Glamourer.Log.Debug($"Changed application type from {old} to {applicationType} for associated design {which + 1} in design set."); + _event.Invoke(AutomationChanged.Type.ChangedType, set, (which, old, applicationType)); + } + + public void ChangeData(AutoDesignSet set, int which, object data) + { + if (which >= set.Designs.Count || which < 0) + return; + + var design = set.Designs[which]; + if (!design.Design.ChangeData(data)) + return; + + Save(); + Glamourer.Log.Debug($"Changed additional design data for associated design {which + 1} in design set."); + _event.Invoke(AutomationChanged.Type.ChangedData, set, (which, data)); } public string ToFilename(FilenameService fileNames) @@ -338,10 +388,8 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos public void Save(StreamWriter writer) { - using var j = new JsonTextWriter(writer) - { - Formatting = Formatting.Indented, - }; + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; Serialize().WriteTo(j); } @@ -404,7 +452,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos continue; } - var id = _actors.AwaitedService.FromJson(obj["Identifier"] as JObject); + var id = _actors.FromJson(obj["Identifier"] as JObject); if (!IdentifierValid(id, out var group)) { Glamourer.Messager.NotificationMessage("Skipped loading Automation Set: Invalid Identifier.", NotificationType.Warning); @@ -413,8 +461,9 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos var set = new AutoDesignSet(name, group) { - Enabled = obj["Enabled"]?.ToObject() ?? false, - BaseState = obj["BaseState"]?.ToObject() ?? AutoDesignSet.Base.Current, + Enabled = obj["Enabled"]?.ToObject() ?? false, + ResetTemporarySettings = obj["ResetTemporarySettings"]?.ToObject() ?? false, + BaseState = obj["BaseState"]?.ToObject() ?? AutoDesignSet.Base.Current, }; if (set.Enabled) @@ -440,8 +489,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos continue; } - var design = ToDesignObject(set.Name, j); - if (design != null) + if (ToDesignObject(set.Name, j) is { } design) set.Designs.Add(design); } } @@ -449,58 +497,85 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos private AutoDesign? ToDesignObject(string setName, JObject jObj) { - var designIdentifier = jObj["Design"]?.ToObject(); - Design? design = null; - // designIdentifier == null means Revert-Design. - if (designIdentifier != null) + var designIdentifier = jObj["Design"]?.ToObject(); + IDesignStandIn? design; + // designIdentifier == null means Revert-Design for backwards compatibility + if (designIdentifier is null or RevertDesign.SerializedName) + { + design = new RevertDesign(); + } + else if (designIdentifier is RandomDesign.SerializedName) + { + design = new RandomDesign(_randomDesigns); + } + else if (designIdentifier is QuickSelectedDesign.SerializedName) + { + design = _quickSelectedDesign; + } + else { if (designIdentifier.Length == 0) { - Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: No design specified.", NotificationType.Warning); + Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: No design specified.", + NotificationType.Warning); return null; } if (!Guid.TryParse(designIdentifier, out var guid)) { - Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: {designIdentifier} is not a valid GUID.", NotificationType.Warning); + Glamourer.Messager.NotificationMessage( + $"Error parsing automatically applied design for set {setName}: {designIdentifier} is not a valid GUID.", + NotificationType.Warning); return null; } - design = _designs.Designs.FirstOrDefault(d => d.Identifier == guid); - if (design == null) + if (!_designs.Designs.TryGetValue(guid, out var d)) { Glamourer.Messager.NotificationMessage( - $"Error parsing automatically applied design for set {setName}: The specified design {guid} does not exist.", NotificationType.Warning); + $"Error parsing automatically applied design for set {setName}: The specified design {guid} does not exist.", + NotificationType.Warning); return null; } + + design = d; } - var applicationType = (AutoDesign.Type)(jObj["ApplicationType"]?.ToObject() ?? 0); + design.ParseData(jObj); - var ret = new AutoDesign() + // ApplicationType is a migration from an older property name. + var applicationType = (ApplicationType)(jObj["Type"]?.ToObject() ?? jObj["ApplicationType"]?.ToObject() ?? 0); + + var ret = new AutoDesign { - Design = design, - ApplicationType = applicationType & AutoDesign.Type.All, + Design = design, + Type = applicationType & ApplicationType.All, }; + return ParseConditions(setName, jObj, ret) ? ret : null; + } + private bool ParseConditions(string setName, JObject jObj, AutoDesign ret) + { var conditions = jObj["Conditions"]; if (conditions == null) - return ret; + return true; var jobs = conditions["JobGroup"]?.ToObject() ?? -1; if (jobs >= 0) { - if (!_jobs.JobGroups.TryGetValue((ushort)jobs, out var jobGroup)) + if (!_jobs.JobGroups.TryGetValue((JobGroupId)jobs, out var jobGroup)) { - Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: The job condition {jobs} does not exist.", NotificationType.Warning); - return null; + Glamourer.Messager.NotificationMessage( + $"Error parsing automatically applied design for set {setName}: The job condition {jobs} does not exist.", + NotificationType.Warning); + return false; } ret.Jobs = jobGroup; } - return ret; + ret.GearsetIndex = conditions["Gearset"]?.ToObject() ?? -1; + return true; } private void Save() @@ -513,12 +588,13 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos IdentifierType.Player => true, IdentifierType.Retainer => true, IdentifierType.Npc => true, + IdentifierType.Owned => true, _ => false, }; if (!validType) { - group = Array.Empty(); + group = []; return false; } @@ -529,42 +605,42 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos private ActorIdentifier[] GetGroup(ActorIdentifier identifier) { if (!identifier.IsValid) - return Array.Empty(); + return []; + + return identifier.Type switch + { + IdentifierType.Player => + [ + identifier.CreatePermanent(), + ], + IdentifierType.Retainer => + [ + _actors.CreateRetainer(identifier.PlayerName, + identifier.Retainer == ActorIdentifier.RetainerType.Mannequin + ? ActorIdentifier.RetainerType.Mannequin + : ActorIdentifier.RetainerType.Bell).CreatePermanent(), + ], + IdentifierType.Npc => CreateNpcs(_actors, identifier), + IdentifierType.Owned => CreateNpcs(_actors, identifier), + _ => [], + }; static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier) { var name = manager.Data.ToName(identifier.Kind, identifier.DataId); var table = identifier.Kind switch { - ObjectKind.BattleNpc => manager.Data.BNpcs, + ObjectKind.BattleNpc => (IReadOnlyDictionary)manager.Data.BNpcs, ObjectKind.EventNpc => manager.Data.ENpcs, - _ => new Dictionary(), + _ => new Dictionary(), }; return table.Where(kvp => kvp.Value == name) .Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id, - identifier.Kind, - kvp.Key)).ToArray(); + identifier.Kind, kvp.Key)).ToArray(); } - - return identifier.Type switch - { - IdentifierType.Player => new[] - { - identifier.CreatePermanent(), - }, - IdentifierType.Retainer => new[] - { - _actors.AwaitedService.CreateRetainer(identifier.PlayerName, - identifier.Retainer == ActorIdentifier.RetainerType.Mannequin - ? ActorIdentifier.RetainerType.Mannequin - : ActorIdentifier.RetainerType.Bell).CreatePermanent(), - }, - IdentifierType.Npc => CreateNpcs(_actors.AwaitedService, identifier), - _ => Array.Empty(), - }; } - private void OnDesignChange(DesignChanged.Type type, Design design, object? data) + private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? _) { if (type is not DesignChanged.Type.Deleted) return; diff --git a/Glamourer/Automation/AutoDesignSet.cs b/Glamourer/Automation/AutoDesignSet.cs index 8657ad7..f8987af 100644 --- a/Glamourer/Automation/AutoDesignSet.cs +++ b/Glamourer/Automation/AutoDesignSet.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using Penumbra.GameData.Actors; namespace Glamourer.Automation; -public class AutoDesignSet +public class AutoDesignSet(string name, ActorIdentifier[] identifiers, List designs) { - public readonly List Designs; + public readonly List Designs = designs; - public string Name; - public ActorIdentifier[] Identifiers; + public string Name = name; + public ActorIdentifier[] Identifiers = identifiers; public bool Enabled; - public Base BaseState = Base.Current; + public Base BaseState = Base.Current; + public bool ResetTemporarySettings = false; public JObject Serialize() { @@ -21,25 +21,19 @@ public class AutoDesignSet return new JObject() { - ["Name"] = Name, - ["Identifier"] = Identifiers[0].ToJson(), - ["Enabled"] = Enabled, - ["BaseState"] = BaseState.ToString(), - ["Designs"] = list, + ["Name"] = Name, + ["Identifier"] = Identifiers[0].ToJson(), + ["Enabled"] = Enabled, + ["BaseState"] = BaseState.ToString(), + ["ResetTemporarySettings"] = ResetTemporarySettings.ToString(), + ["Designs"] = list, }; } public AutoDesignSet(string name, params ActorIdentifier[] identifiers) - : this(name, identifiers, new List()) + : this(name, identifiers, []) { } - public AutoDesignSet(string name, ActorIdentifier[] identifiers, List designs) - { - Name = name; - Identifiers = identifiers; - Designs = designs; - } - public enum Base : byte { Current, diff --git a/Glamourer/Automation/FixedDesignMigrator.cs b/Glamourer/Automation/FixedDesignMigrator.cs index 0dc20b4..fb9bca4 100644 --- a/Glamourer/Automation/FixedDesignMigrator.cs +++ b/Glamourer/Automation/FixedDesignMigrator.cs @@ -1,61 +1,51 @@ -using System.Collections.Generic; -using System.Linq; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Glamourer.Designs; using Glamourer.Interop; -using Glamourer.Services; -using Glamourer.Structs; using Newtonsoft.Json.Linq; using OtterGui.Classes; using Penumbra.GameData.Actors; +using Penumbra.GameData.Structs; using Penumbra.String; namespace Glamourer.Automation; -public class FixedDesignMigrator +public class FixedDesignMigrator(JobService jobs) { - private readonly JobService _jobs; - private List<(string Name, List<(string, JobGroup, bool)> Data)>? _migratedData; + private List<(string Name, List<(string, JobGroup, bool)> Data)>? _migratedData; - public FixedDesignMigrator(JobService jobs) - => _jobs = jobs; - - public void ConsumeMigratedData(ActorService actors, DesignFileSystem designFileSystem, AutoDesignManager autoManager) + public void ConsumeMigratedData(ActorManager actors, DesignFileSystem designFileSystem, AutoDesignManager autoManager) { if (_migratedData == null) return; - foreach (var data in _migratedData) + foreach (var (name, data) in _migratedData) { - var allEnabled = true; - var name = data.Name; if (autoManager.Any(d => name == d.Name)) continue; var id = ActorIdentifier.Invalid; - if (ByteString.FromString(data.Name, out var byteString, false)) + if (ByteString.FromString(name, out var byteString)) { - id = actors.AwaitedService.CreatePlayer(byteString, ushort.MaxValue); + id = actors.CreatePlayer(byteString, ushort.MaxValue); if (!id.IsValid) - id = actors.AwaitedService.CreateRetainer(byteString, ActorIdentifier.RetainerType.Both); + id = actors.CreateRetainer(byteString, ActorIdentifier.RetainerType.Both); } if (!id.IsValid) { byteString = ByteString.FromSpanUnsafe("Mig Ration"u8, true, false, true); - id = actors.AwaitedService.CreatePlayer(byteString, actors.AwaitedService.Data.Worlds.First().Key); + id = actors.CreatePlayer(byteString, actors.Data.Worlds.First().Key); if (!id.IsValid) { - Glamourer.Messager.NotificationMessage($"Could not migrate fixed design {data.Name}.", NotificationType.Error); - allEnabled = false; + Glamourer.Messager.NotificationMessage($"Could not migrate fixed design {name}.", NotificationType.Error); continue; } } autoManager.AddDesignSet(name, id); - autoManager.SetState(autoManager.Count - 1, allEnabled); + autoManager.SetState(autoManager.Count - 1, true); var set = autoManager[^1]; - foreach (var design in data.Data.AsEnumerable().Reverse()) + foreach (var design in data.AsEnumerable().Reverse()) { if (!designFileSystem.Find(design.Item1, out var child) || child is not DesignFileSystem.Leaf leaf) { @@ -66,7 +56,7 @@ public class FixedDesignMigrator autoManager.AddDesign(set, leaf.Value); autoManager.ChangeJobCondition(set, set.Designs.Count - 1, design.Item2); - autoManager.ChangeApplicationType(set, set.Designs.Count - 1, design.Item3 ? AutoDesign.Type.All : 0); + autoManager.ChangeApplicationType(set, set.Designs.Count - 1, design.Item3 ? ApplicationType.All : 0); } } } @@ -96,7 +86,7 @@ public class FixedDesignMigrator } var job = obj["JobGroups"]?.ToObject() ?? -1; - if (job < 0 || !_jobs.JobGroups.TryGetValue((ushort)job, out var group)) + if (job < 0 || !jobs.JobGroups.TryGetValue((JobGroupId)job, out var group)) { Glamourer.Messager.NotificationMessage("Could not semi-migrate fixed design: Invalid job group specified.", NotificationType.Warning); diff --git a/Glamourer/Configuration.cs b/Glamourer/Configuration.cs index 6aaa11c..d266a55 100644 --- a/Glamourer/Configuration.cs +++ b/Glamourer/Configuration.cs @@ -1,51 +1,97 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Configuration; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Configuration; +using Dalamud.Game.ClientState.Keys; +using Dalamud.Interface.ImGuiNotification; using Glamourer.Designs; using Glamourer.Gui; +using Glamourer.Gui.Tabs.DesignTab; using Glamourer.Services; using Newtonsoft.Json; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Filesystem; using OtterGui.Widgets; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Glamourer; +public enum HeightDisplayType +{ + None, + Centimetre, + Metre, + Wrong, + WrongFoot, + Corgi, + OlympicPool, +} + +public class DefaultDesignSettings +{ + public bool AlwaysForceRedrawing = false; + public bool ResetAdvancedDyes = false; + public bool ShowQuickDesignBar = true; + public bool ResetTemporarySettings = false; + public bool Locked = false; +} + public class Configuration : IPluginConfiguration, ISavable { - public bool Enabled { get; set; } = true; - public bool UseRestrictedGearProtection { get; set; } = false; - public bool OpenFoldersByDefault { get; set; } = false; - public bool AutoRedrawEquipOnChanges { get; set; } = false; - public bool EnableAutoDesigns { get; set; } = true; - public bool IncognitoMode { get; set; } = false; - public bool UnlockDetailMode { get; set; } = true; - public bool HideApplyCheckmarks { get; set; } = false; - public bool SmallEquip { get; set; } = false; - public bool UnlockedItemMode { get; set; } = false; - public byte DisableFestivals { get; set; } = 1; - public bool EnableGameContextMenu { get; set; } = true; - public bool HideWindowInCutscene { get; set; } = false; - public bool ShowAutomationSetEditing { get; set; } = true; - public bool ShowAllAutomatedApplicationRules { get; set; } = true; - public bool ShowUnlockedItemWarnings { get; set; } = true; - public bool RevertManualChangesOnZoneChange { get; set; } = false; - public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings; - public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); + [JsonIgnore] + public readonly EphemeralConfig Ephemeral; - public int LastSeenVersion { get; set; } = GlamourerChangelog.LastChangelogVersion; + public bool AttachToPcp { get; set; } = true; + public bool UseRestrictedGearProtection { get; set; } = false; + public bool OpenFoldersByDefault { get; set; } = false; + public bool AutoRedrawEquipOnChanges { get; set; } = false; + public bool EnableAutoDesigns { get; set; } = true; + public bool HideApplyCheckmarks { get; set; } = false; + public bool SmallEquip { get; set; } = false; + public bool UnlockedItemMode { get; set; } = false; + public byte DisableFestivals { get; set; } = 1; + public bool EnableGameContextMenu { get; set; } = true; + public bool HideWindowInCutscene { get; set; } = false; + public bool ShowAutomationSetEditing { get; set; } = true; + public bool ShowAllAutomatedApplicationRules { get; set; } = true; + public bool ShowUnlockedItemWarnings { get; set; } = true; + public bool RevertManualChangesOnZoneChange { get; set; } = false; + public bool ShowQuickBarInTabs { get; set; } = true; + public bool OpenWindowAtStart { get; set; } = false; + public bool ShowWindowWhenUiHidden { get; set; } = false; + public bool KeepAdvancedDyesAttached { get; set; } = true; + public bool ShowPalettePlusImport { get; set; } = true; + public bool UseFloatForColors { get; set; } = true; + public bool UseRgbForColors { get; set; } = true; + public bool ShowColorConfig { get; set; } = true; + public bool ChangeEntireItem { get; set; } = false; + public bool AlwaysApplyAssociatedMods { get; set; } = true; + public bool UseTemporarySettings { get; set; } = true; + public bool AllowDoubleClickToApply { get; set; } = false; + public bool RespectManualOnAutomationUpdate { get; set; } = false; + public bool PreventRandomRepeats { get; set; } = false; + public string PcpFolder { get; set; } = "PCP"; + public string PcpColor { get; set; } = ""; + + public DesignPanelFlag HideDesignPanel { get; set; } = 0; + public DesignPanelFlag AutoExpandDesignPanel { get; set; } = 0; + + public DefaultDesignSettings DefaultDesignSettings { get; set; } = new(); + + public HeightDisplayType HeightDisplayType { get; set; } = HeightDisplayType.Centimetre; + public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; + public ModifiableHotkey ToggleQuickDesignBar { get; set; } = new(VirtualKey.NO_KEY); + public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); + public DoubleModifier IncognitoModifier { get; set; } = new(ModifierHotkey.Control); public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New; + public QdbButtons QdbButtons { get; set; } = + QdbButtons.ApplyDesign | QdbButtons.RevertAll | QdbButtons.RevertAutomation | QdbButtons.RevertAdvancedDyes; + [JsonConverter(typeof(SortModeConverter))] [JsonProperty(Order = int.MaxValue)] public ISortMode SortMode { get; set; } = ISortMode.FoldersFirst; - public List<(string Code, bool Enabled)> Codes { get; set; } = new(); + public List<(string Code, bool Enabled)> Codes { get; set; } = []; #if DEBUG public bool DebugMode { get; set; } = true; @@ -61,24 +107,18 @@ public class Configuration : IPluginConfiguration, ISavable [JsonIgnore] private readonly SaveService _saveService; - public Configuration(SaveService saveService, ConfigMigrationService migrator) + public Configuration(SaveService saveService, ConfigMigrationService migrator, EphemeralConfig ephemeral) { _saveService = saveService; + Ephemeral = ephemeral; Load(migrator); } public void Save() => _saveService.DelaySave(this); - public void Load(ConfigMigrationService migrator) + private void Load(ConfigMigrationService migrator) { - static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) - { - Glamourer.Log.Error( - $"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); - errorArgs.ErrorContext.Handled = true; - } - if (!File.Exists(_saveService.FileNames.ConfigFile)) return; @@ -99,6 +139,14 @@ public class Configuration : IPluginConfiguration, ISavable } migrator.Migrate(this); + return; + + static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) + { + Glamourer.Log.Error( + $"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); + errorArgs.ErrorContext.Handled = true; + } } public string ToFilename(FilenameService fileNames) @@ -106,17 +154,18 @@ public class Configuration : IPluginConfiguration, ISavable public void Save(StreamWriter writer) { - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + using var jWriter = new JsonTextWriter(writer); + jWriter.Formatting = Formatting.Indented; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; serializer.Serialize(jWriter, this); } public static class Constants { - public const int CurrentVersion = 4; + public const int CurrentVersion = 8; public static readonly ISortMode[] ValidSortModes = - { + [ ISortMode.FoldersFirst, ISortMode.Lexicographical, new DesignFileSystem.CreationDate(), @@ -129,7 +178,7 @@ public class Configuration : IPluginConfiguration, ISavable ISortMode.InverseFoldersLast, ISortMode.InternalOrder, ISortMode.InverseInternalOrder, - }; + ]; } /// Convert SortMode Types to their name. diff --git a/Glamourer/DesignPanelFlag.cs b/Glamourer/DesignPanelFlag.cs new file mode 100644 index 0000000..f9465d9 --- /dev/null +++ b/Glamourer/DesignPanelFlag.cs @@ -0,0 +1,96 @@ +using Glamourer.Designs; +using Dalamud.Bindings.ImGui; +using OtterGui.Text; +using OtterGui.Text.EndObjects; + +namespace Glamourer; + +[Flags] +public enum DesignPanelFlag : uint +{ + Customization = 0x0001, + Equipment = 0x0002, + AdvancedCustomizations = 0x0004, + AdvancedDyes = 0x0008, + AppearanceDetails = 0x0010, + DesignDetails = 0x0020, + ModAssociations = 0x0040, + DesignLinks = 0x0080, + ApplicationRules = 0x0100, + DebugData = 0x0200, +} + +public static class DesignPanelFlagExtensions +{ + public static ReadOnlySpan ToName(this DesignPanelFlag flag) + => flag switch + { + DesignPanelFlag.Customization => "Customization"u8, + DesignPanelFlag.Equipment => "Equipment"u8, + DesignPanelFlag.AdvancedCustomizations => "Advanced Customization"u8, + DesignPanelFlag.AdvancedDyes => "Advanced Dyes"u8, + DesignPanelFlag.DesignDetails => "Design Details"u8, + DesignPanelFlag.ApplicationRules => "Application Rules"u8, + DesignPanelFlag.ModAssociations => "Mod Associations"u8, + DesignPanelFlag.DesignLinks => "Design Links"u8, + DesignPanelFlag.DebugData => "Debug Data"u8, + DesignPanelFlag.AppearanceDetails => "Appearance Details"u8, + _ => ""u8, + }; + + public static CollapsingHeader Header(this DesignPanelFlag flag, Configuration config) + { + if (config.HideDesignPanel.HasFlag(flag)) + return new CollapsingHeader() + { + Disposed = true, + }; + + var expand = config.AutoExpandDesignPanel.HasFlag(flag); + return ImUtf8.CollapsingHeaderId(flag.ToName(), expand ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None); + } + + public static void DrawTable(ReadOnlySpan label, DesignPanelFlag hidden, DesignPanelFlag expanded, Action setterHide, + Action setterExpand) + { + var checkBoxWidth = Math.Max(ImGui.GetFrameHeight(), ImUtf8.CalcTextSize("Expand"u8).X); + var textWidth = ImUtf8.CalcTextSize(DesignPanelFlag.AdvancedCustomizations.ToName()).X; + var tableSize = 2 * (textWidth + 2 * checkBoxWidth) + 10 * ImGui.GetStyle().CellPadding.X + 2 * ImGui.GetStyle().WindowPadding.X + 2 * ImGui.GetStyle().FrameBorderSize; + using var table = ImUtf8.Table(label, 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Borders, new Vector2(tableSize, 6 * ImGui.GetFrameHeight())); + if (!table) + return; + + var headerColor = ImGui.GetColorU32(ImGuiCol.TableHeaderBg); + var checkBoxOffset = (checkBoxWidth - ImGui.GetFrameHeight()) / 2; + ImUtf8.TableSetupColumn("Panel##1"u8, ImGuiTableColumnFlags.WidthFixed, textWidth); + ImUtf8.TableSetupColumn("Show##1"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth); + ImUtf8.TableSetupColumn("Expand##1"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth); + ImUtf8.TableSetupColumn("Panel##2"u8, ImGuiTableColumnFlags.WidthFixed, textWidth); + ImUtf8.TableSetupColumn("Show##2"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth); + ImUtf8.TableSetupColumn("Expand##2"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth); + + ImGui.TableHeadersRow(); + foreach (var panel in Enum.GetValues()) + { + using var id = ImUtf8.PushId((int)panel); + ImGui.TableNextColumn(); + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, headerColor); + ImUtf8.TextFrameAligned(panel.ToName()); + var isShown = !hidden.HasFlag(panel); + var isExpanded = expanded.HasFlag(panel); + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + checkBoxOffset); + if (ImUtf8.Checkbox("##show"u8, ref isShown)) + setterHide.Invoke(isShown ? hidden & ~panel : hidden | panel); + ImUtf8.HoverTooltip( + "Show this panel and associated functionality in all relevant tabs.\n\nToggling this off does NOT disable any functionality, just the display of it, so hide panels at your own risk."u8); + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + checkBoxOffset); + if (ImUtf8.Checkbox("##expand"u8, ref isExpanded)) + setterExpand.Invoke(isExpanded ? expanded | panel : expanded & ~panel); + ImUtf8.HoverTooltip("Expand this panel by default in all relevant tabs."u8); + } + } +} diff --git a/Glamourer/Designs/ApplicationCollection.cs b/Glamourer/Designs/ApplicationCollection.cs new file mode 100644 index 0000000..c03d4b4 --- /dev/null +++ b/Glamourer/Designs/ApplicationCollection.cs @@ -0,0 +1,68 @@ +using Glamourer.Api.Enums; +using Glamourer.GameData; +using Dalamud.Bindings.ImGui; +using Penumbra.GameData.Enums; + +namespace Glamourer.Designs; + +public record struct ApplicationCollection( + EquipFlag Equip, + BonusItemFlag BonusItem, + CustomizeFlag CustomizeRaw, + CrestFlag Crest, + CustomizeParameterFlag Parameters, + MetaFlag Meta) +{ + public static readonly ApplicationCollection All = new(EquipFlagExtensions.All, BonusExtensions.All, + CustomizeFlagExtensions.AllRelevant, CrestExtensions.AllRelevant, CustomizeParameterExtensions.All, MetaExtensions.All); + + public static readonly ApplicationCollection None = new(0, 0, CustomizeFlag.BodyType, 0, 0, 0); + + public static readonly ApplicationCollection Equipment = new(EquipFlagExtensions.All, BonusExtensions.All, + CustomizeFlag.BodyType, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState | MetaFlag.EarState); + + public static readonly ApplicationCollection Customizations = new(0, 0, CustomizeFlagExtensions.AllRelevant, 0, + CustomizeParameterExtensions.All, MetaFlag.Wetness); + + public static readonly ApplicationCollection Default = new(EquipFlagExtensions.All, BonusExtensions.All, + CustomizeFlagExtensions.AllRelevant, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState); + + public static ApplicationCollection FromKeys() + => (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch + { + (false, false) => All, + (true, true) => All, + (true, false) => Equipment, + (false, true) => Customizations, + }; + + public CustomizeFlag Customize + { + get => CustomizeRaw; + set => CustomizeRaw = value | CustomizeFlag.BodyType; + } + + public void RemoveEquip() + { + Equip = 0; + BonusItem = 0; + Crest = 0; + Meta &= ~(MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState); + } + + public void RemoveCustomize() + { + Customize = 0; + Parameters = 0; + Meta &= MetaFlag.Wetness; + } + + public ApplicationCollection Restrict(ApplicationCollection old) + => new(old.Equip & Equip, old.BonusItem & BonusItem, (old.Customize & Customize) | CustomizeFlag.BodyType, old.Crest & Crest, + old.Parameters & Parameters, old.Meta & Meta); + + public ApplicationCollection CloneSecure() + => new(Equip & EquipFlagExtensions.All, BonusItem & BonusExtensions.All, + (Customize & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType, Crest & CrestExtensions.AllRelevant, + Parameters & CustomizeParameterExtensions.All, Meta & MetaExtensions.All); +} diff --git a/Glamourer/Designs/ApplicationRules.cs b/Glamourer/Designs/ApplicationRules.cs new file mode 100644 index 0000000..281a940 --- /dev/null +++ b/Glamourer/Designs/ApplicationRules.cs @@ -0,0 +1,71 @@ +using Glamourer.Api.Enums; +using Glamourer.GameData; +using Glamourer.State; +using Dalamud.Bindings.ImGui; +using Penumbra.GameData.Enums; + +namespace Glamourer.Designs; + +public readonly struct ApplicationRules(ApplicationCollection application, bool materials) +{ + public static readonly ApplicationRules All = new(ApplicationCollection.All, true); + + public static ApplicationRules FromModifiers(ActorState state) + => FromModifiers(state, ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift); + + public static ApplicationRules NpcFromModifiers() + => NpcFromModifiers(ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift); + + public static ApplicationRules AllButParameters(ActorState state) + => new(ApplicationCollection.All with { Parameters = ComputeParameters(state.ModelData, state.BaseData, All.Parameters) }, true); + + public static ApplicationRules NpcFromModifiers(bool ctrl, bool shift) + { + var equip = ctrl || !shift ? EquipFlagExtensions.All : 0; + var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0; + var visor = equip != 0 ? MetaFlag.VisorState : 0; + return new ApplicationRules(new ApplicationCollection(equip, 0, customize, 0, 0, visor), false); + } + + public static ApplicationRules FromModifiers(ActorState state, bool ctrl, bool shift) + { + var equip = ctrl || !shift ? EquipFlagExtensions.All : 0; + var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0; + var bonus = equip == 0 ? 0 : BonusExtensions.All; + var crest = equip == 0 ? 0 : CrestExtensions.AllRelevant; + var parameters = customize == 0 ? 0 : CustomizeParameterExtensions.All; + var meta = state.ModelData.IsWet() ? MetaFlag.Wetness : 0; + if (equip != 0) + meta |= MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState; + + var collection = new ApplicationCollection(equip, bonus, customize, crest, + ComputeParameters(state.ModelData, state.BaseData, parameters), meta); + return new ApplicationRules(collection, equip != 0); + } + + public void Apply(DesignBase design) + => design.Application = application; + + public EquipFlag Equip + => application.Equip & EquipFlagExtensions.All; + + public CustomizeParameterFlag Parameters + => application.Parameters & CustomizeParameterExtensions.All; + + public bool Materials + => materials; + + private static CustomizeParameterFlag ComputeParameters(in DesignData model, in DesignData game, + CustomizeParameterFlag baseFlags = CustomizeParameterExtensions.All) + { + foreach (var flag in baseFlags.Iterate()) + { + var modelValue = model.Parameters[flag]; + var gameValue = game.Parameters[flag]; + if (modelValue.NearEqual(gameValue)) + baseFlags &= ~flag; + } + + return baseFlags; + } +} diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index e6593a9..848e7d6 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -1,21 +1,24 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; +using Glamourer.Automation; +using Glamourer.Designs.Links; +using Glamourer.Interop.Material; using Glamourer.Interop.Penumbra; using Glamourer.Services; +using Glamourer.State; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Classes; +using Penumbra.GameData.Structs; +using Notification = OtterGui.Classes.Notification; namespace Glamourer.Designs; -public sealed class Design : DesignBase, ISavable +public sealed class Design : DesignBase, ISavable, IDesignStandIn { #region Data - internal Design(CustomizationService customize, ItemManager items) - : base(items) + + internal Design(CustomizeService customize, ItemManager items) + : base(customize, items) { } internal Design(DesignBase other) @@ -25,47 +28,101 @@ public sealed class Design : DesignBase, ISavable internal Design(Design other) : base(other) { - Tags = Tags.ToArray(); - Description = Description; - AssociatedMods = new SortedList(other.AssociatedMods); + Tags = [.. other.Tags]; + Description = other.Description; + QuickDesign = other.QuickDesign; + ForcedRedraw = other.ForcedRedraw; + ResetAdvancedDyes = other.ResetAdvancedDyes; + ResetTemporarySettings = other.ResetTemporarySettings; + Color = other.Color; + AssociatedMods = new SortedList(other.AssociatedMods); + Links = Links.Clone(); } // Metadata - public new const int FileVersion = 1; + public new const int FileVersion = 2; - public Guid Identifier { get; internal init; } - public DateTimeOffset CreationDate { get; internal init; } - public DateTimeOffset LastEdit { get; internal set; } - public LowerString Name { get; internal set; } = LowerString.Empty; - public string Description { get; internal set; } = string.Empty; - public string[] Tags { get; internal set; } = Array.Empty(); - public int Index { get; internal set; } - public SortedList AssociatedMods { get; private set; } = new(); + public Guid Identifier { get; internal init; } + public DateTimeOffset CreationDate { get; internal init; } + public DateTimeOffset LastEdit { get; internal set; } + public LowerString Name { get; internal set; } = LowerString.Empty; + public string Description { get; internal set; } = string.Empty; + public string[] Tags { get; internal set; } = []; + public int Index { get; internal set; } + public bool ForcedRedraw { get; internal set; } + public bool ResetAdvancedDyes { get; internal set; } + public bool ResetTemporarySettings { get; internal set; } + public bool QuickDesign { get; internal set; } = true; + public string Color { get; internal set; } = string.Empty; + public SortedList AssociatedMods { get; private set; } = []; + public LinkContainer Links { get; private set; } = []; public string Incognito => Identifier.ToString()[..8]; + public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication) + => LinkContainer.GetAllLinks(this).Select(t => ((IDesignStandIn)t.Link.Link, t.Link.Type, JobFlag.All)); + + #endregion + + #region IDesignStandIn + + public string ResolveName(bool incognito) + => incognito ? Incognito : Name.Text; + + public string SerializeName() + => Identifier.ToString(); + + public ref readonly DesignData GetDesignData(in DesignData baseData) + => ref GetDesignDataRef(); + + public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData() + => Materials; + + public bool Equals(IDesignStandIn? other) + => other is Design d && d.Identifier == Identifier; + + public StateSource AssociatedSource() + => StateSource.Manual; + + public void AddData(JObject _) + { } + + public void ParseData(JObject _) + { } + + public bool ChangeData(object data) + => false; + #endregion #region Serialization public new JObject JsonSerialize() { - var ret = new JObject() - { - ["FileVersion"] = FileVersion, - ["Identifier"] = Identifier, - ["CreationDate"] = CreationDate, - ["LastEdit"] = LastEdit, - ["Name"] = Name.Text, - ["Description"] = Description, - ["Tags"] = JArray.FromObject(Tags), - ["WriteProtected"] = WriteProtected(), - ["Equipment"] = SerializeEquipment(), - ["Customize"] = SerializeCustomize(), - ["Mods"] = SerializeMods(), - } - ; + var ret = new JObject + { + ["FileVersion"] = FileVersion, + ["Identifier"] = Identifier, + ["CreationDate"] = CreationDate, + ["LastEdit"] = LastEdit, + ["Name"] = Name.Text, + ["Description"] = Description, + ["ForcedRedraw"] = ForcedRedraw, + ["ResetAdvancedDyes"] = ResetAdvancedDyes, + ["ResetTemporarySettings"] = ResetTemporarySettings, + ["Color"] = Color, + ["QuickDesign"] = QuickDesign, + ["Tags"] = JArray.FromObject(Tags), + ["WriteProtected"] = WriteProtected(), + ["Equipment"] = SerializeEquipment(), + ["Bonus"] = SerializeBonusItems(), + ["Customize"] = SerializeCustomize(), + ["Parameters"] = SerializeParameters(), + ["Materials"] = SerializeMaterials(), + ["Mods"] = SerializeMods(), + ["Links"] = Links.Serialize(), + }; return ret; } @@ -74,12 +131,17 @@ public sealed class Design : DesignBase, ISavable var ret = new JArray(); foreach (var (mod, settings) in AssociatedMods) { - var obj = new JObject() + var obj = new JObject { ["Name"] = mod.Name, ["Directory"] = mod.DirectoryName, - ["Enabled"] = settings.Enabled, }; + if (settings.Remove) + obj["Remove"] = true; + else if (settings.ForceInherit) + obj["Inherit"] = true; + else + obj["Enabled"] = settings.Enabled; if (settings.Enabled) { obj["Priority"] = settings.Priority; @@ -96,24 +158,84 @@ public sealed class Design : DesignBase, ISavable #region Deserialization - public static Design LoadDesign(CustomizationService customizations, ItemManager items, JObject json) + public static Design LoadDesign(SaveService saveService, CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader, + JObject json) { var version = json["FileVersion"]?.ToObject() ?? 0; return version switch { - FileVersion => LoadDesignV1(customizations, items, json), + 1 => LoadDesignV1(saveService, customizations, items, linkLoader, json), + FileVersion => LoadDesignV2(customizations, items, linkLoader, json), _ => throw new Exception("The design to be loaded has no valid Version."), }; } - private static Design LoadDesignV1(CustomizationService customizations, ItemManager items, JObject json) + /// The values for gloss and specular strength were switched. Swap them for all appropriate designs. + private static Design LoadDesignV1(SaveService saveService, CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader, + JObject json) { - static string[] ParseTags(JObject json) + var design = LoadDesignV2(customizations, items, linkLoader, json); + var materialDesignData = design.GetMaterialDataRef(); + if (materialDesignData.Values.Count == 0) + return design; + + var materialData = materialDesignData.Clone(); + // Guesstimate whether to migrate material rows: + // Update 1.3.0.10 released at that time, so any design last updated before that can be migrated. + if (design.LastEdit <= new DateTime(2024, 8, 7, 16, 0, 0, DateTimeKind.Utc)) { - var tags = json["Tags"]?.ToObject() ?? Array.Empty(); - return tags.OrderBy(t => t).Distinct().ToArray(); + Migrate("because it was saved the wrong way around before 1.3.0.10, and this design was not changed since that release."); + } + else + { + var hasNegativeGloss = false; + var hasNonPositiveGloss = false; + var specularLarger = 0; + foreach (var (key, value) in materialData.GetValues(MaterialValueIndex.Min(), MaterialValueIndex.Max())) + { + hasNegativeGloss |= value.Value.GlossStrength < 0; + hasNonPositiveGloss |= value.Value.GlossStrength <= 0; + if (value.Value.SpecularStrength > value.Value.GlossStrength) + ++specularLarger; + } + + // If there is any negative gloss, this is wrong and can be migrated. + if (hasNegativeGloss) + Migrate("because it had a negative Gloss value, which is not supported and thus probably outdated."); + // If there is any non-positive Gloss and some specular values that are larger, it is probably wrong and can be migrated. + else if (hasNonPositiveGloss && specularLarger > 0) + Migrate("because it had a zero Gloss value, and at least one Specular Strength larger than the Gloss, which is unusual."); + // If most of the specular strengths are larger, it is probably wrong and can be migrated. + else if (specularLarger > materialData.Values.Count / 2) + Migrate("because most of its Specular Strength values were larger than the Gloss values, which is unusual."); } + return design; + + void Migrate(string reason) + { + materialDesignData.Clear(); + foreach (var (key, value) in materialData.GetValues(MaterialValueIndex.Min(), MaterialValueIndex.Max())) + { + var gloss = Math.Clamp(value.Value.SpecularStrength, 0, (float)Half.MaxValue); + var specularStrength = Math.Clamp(value.Value.GlossStrength, 0, (float)Half.MaxValue); + var colorRow = value.Value with + { + GlossStrength = gloss, + SpecularStrength = specularStrength, + }; + materialDesignData.AddOrUpdateValue(MaterialValueIndex.FromKey(key), value with { Value = colorRow }); + } + + Glamourer.Messager.AddMessage(new Notification( + $"Swapped Gloss and Specular Strength in {materialDesignData.Values.Count} Rows in design {design.Incognito} {reason}", + NotificationType.Info)); + saveService.Save(SaveType.ImmediateSync, design); + } + } + + private static Design LoadDesignV2(CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader, JObject json) + { var creationDate = json["CreationDate"]?.ToObject() ?? throw new ArgumentNullException("CreationDate"); var design = new Design(customizations, items) @@ -124,14 +246,29 @@ public sealed class Design : DesignBase, ISavable Description = json["Description"]?.ToObject() ?? string.Empty, Tags = ParseTags(json), LastEdit = json["LastEdit"]?.ToObject() ?? creationDate, + QuickDesign = json["QuickDesign"]?.ToObject() ?? true, }; if (design.LastEdit < creationDate) design.LastEdit = creationDate; design.SetWriteProtected(json["WriteProtected"]?.ToObject() ?? false); LoadCustomize(customizations, json["Customize"], design, design.Name, true, false); LoadEquip(items, json["Equipment"], design, design.Name, true); + LoadBonus(items, design, json["Bonus"]); LoadMods(json["Mods"], design); + LoadParameters(json["Parameters"], design, design.Name); + LoadMaterials(json["Materials"], design, design.Name); + LoadLinks(linkLoader, json["Links"], design); + design.Color = json["Color"]?.ToObject() ?? string.Empty; + design.ForcedRedraw = json["ForcedRedraw"]?.ToObject() ?? false; + design.ResetAdvancedDyes = json["ResetAdvancedDyes"]?.ToObject() ?? false; + design.ResetTemporarySettings = json["ResetTemporarySettings"]?.ToObject() ?? false; return design; + + static string[] ParseTags(JObject json) + { + var tags = json["Tags"]?.ToObject() ?? []; + return tags.OrderBy(t => t).Distinct().ToArray(); + } } private static void LoadMods(JToken? mods, Design design) @@ -150,16 +287,42 @@ public sealed class Design : DesignBase, ISavable continue; } - var settingsDict = tok["Settings"]?.ToObject>() ?? new Dictionary(); - var settings = new SortedList>(settingsDict.Count); + var forceInherit = tok["Inherit"]?.ToObject() ?? false; + var removeSetting = tok["Remove"]?.ToObject() ?? false; + var settingsDict = tok["Settings"]?.ToObject>>() ?? []; + var settings = new Dictionary>(settingsDict.Count); foreach (var (key, value) in settingsDict) settings.Add(key, value); var priority = tok["Priority"]?.ToObject() ?? 0; - if (!design.AssociatedMods.TryAdd(new Mod(name, directory), new ModSettings(settings, priority, enabled.Value))) + if (!design.AssociatedMods.TryAdd(new Mod(name, directory), + new ModSettings(settings, priority, enabled.Value, forceInherit, removeSetting))) Glamourer.Messager.NotificationMessage("The loaded design contains a mod more than once, skipped.", NotificationType.Warning); } } + private static void LoadLinks(DesignLinkLoader linkLoader, JToken? links, Design design) + { + if (links is not JObject obj) + return; + + Parse(obj["Before"] as JArray, LinkOrder.Before); + Parse(obj["After"] as JArray, LinkOrder.After); + return; + + void Parse(JArray? array, LinkOrder order) + { + if (array == null) + return; + + foreach (var jObj in array.OfType()) + { + var identifier = jObj["Design"]?.ToObject() ?? throw new ArgumentNullException(nameof(design)); + var type = (ApplicationType)(jObj["Type"]?.ToObject() ?? 0); + linkLoader.AddObject(design, new LinkData(identifier, type, order)); + } + } + } + #endregion #region ISavable diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs index 9fe18ff..f87d75a 100644 --- a/Glamourer/Designs/DesignBase.cs +++ b/Glamourer/Designs/DesignBase.cs @@ -1,14 +1,12 @@ -using System; -using System.Linq; -using Dalamud.Interface.Internal.Notifications; -using Glamourer.Customization; +using Dalamud.Interface.ImGuiNotification; +using Glamourer.GameData; +using Glamourer.Interop.Material; using Glamourer.Services; -using Glamourer.Structs; using Newtonsoft.Json.Linq; using OtterGui.Classes; -using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.GameData.DataContainers; namespace Glamourer.Designs; @@ -16,172 +14,215 @@ public class DesignBase { public const int FileVersion = 1; - internal DesignBase(ItemManager items) + private DesignData _designData = new(); + private readonly DesignMaterialManager _materials = new(); + + /// For read-only information about custom material color changes. + public IReadOnlyList<(uint, MaterialValueDesign)> Materials + => _materials.Values; + + /// To make it clear something is edited here. + public DesignMaterialManager GetMaterialDataRef() + => _materials; + + /// For read-only information about the actual design. + public ref readonly DesignData DesignData + => ref _designData; + + /// To make it clear that something is edited here. + public ref DesignData GetDesignDataRef() + => ref _designData; + + internal DesignBase(CustomizeService customize, ItemManager items) { - DesignData.SetDefaultEquipment(items); + _designData.SetDefaultEquipment(items); + CustomizeSet = SetCustomizationSet(customize); + } + + /// Used when importing .cma or .chara files. + internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags, + BonusItemFlag bonusFlags) + { + _designData = designData; + ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant; + Application.Equip = equipFlags & EquipFlagExtensions.All; + Application.BonusItem = bonusFlags & BonusExtensions.All; + Application.Meta = 0; + CustomizeSet = SetCustomizationSet(customize); } internal DesignBase(DesignBase clone) { - DesignData = clone.DesignData; - ApplyCustomize = clone.ApplyCustomize & CustomizeFlagExtensions.AllRelevant; - ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All; - _designFlags = clone._designFlags & (DesignFlags)0x0F; + _designData = clone._designData; + _materials = clone._materials.Clone(); + CustomizeSet = clone.CustomizeSet; + Application = clone.Application.CloneSecure(); } - internal DesignData DesignData = new(); + /// Ensure that the customization set is updated when the design data changes. + internal void SetDesignData(CustomizeService customize, in DesignData other) + { + _designData = other; + CustomizeSet = SetCustomizationSet(customize); + } #region Application Data - [Flags] - private enum DesignFlags : byte + public CustomizeSet CustomizeSet { get; private set; } + + public ApplicationCollection Application = ApplicationCollection.Default; + + internal CustomizeFlag ApplyCustomize { - ApplyHatVisible = 0x01, - ApplyVisorState = 0x02, - ApplyWeaponVisible = 0x04, - ApplyWetness = 0x08, - WriteProtected = 0x10, + get => Application.Customize.FixApplication(CustomizeSet); + set => Application.Customize = (value & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType; } - internal CustomizeFlag ApplyCustomize = CustomizeFlagExtensions.AllRelevant; - internal EquipFlag ApplyEquip = EquipFlagExtensions.All; - private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible; + internal CustomizeFlag ApplyCustomizeExcludingBodyType + => Application.Customize.FixApplication(CustomizeSet) & ~CustomizeFlag.BodyType; - public bool DoApplyHatVisible() - => _designFlags.HasFlag(DesignFlags.ApplyHatVisible); + private bool _writeProtected; - public bool DoApplyVisorToggle() - => _designFlags.HasFlag(DesignFlags.ApplyVisorState); + public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize) + { + if (customize.Equals(_designData.Customize)) + return false; - public bool DoApplyWeaponVisible() - => _designFlags.HasFlag(DesignFlags.ApplyWeaponVisible); + _designData.Customize = customize; + CustomizeSet = customizeService.Manager.GetSet(customize.Clan, customize.Gender); + return true; + } - public bool DoApplyWetness() - => _designFlags.HasFlag(DesignFlags.ApplyWetness); + public bool DoApplyMeta(MetaIndex index) + => Application.Meta.HasFlag(index.ToFlag()); public bool WriteProtected() - => _designFlags.HasFlag(DesignFlags.WriteProtected); + => _writeProtected; - public bool SetApplyHatVisible(bool value) + public bool SetApplyMeta(MetaIndex index, bool value) { - var newFlag = value ? _designFlags | DesignFlags.ApplyHatVisible : _designFlags & ~DesignFlags.ApplyHatVisible; - if (newFlag == _designFlags) + var newFlag = value ? Application.Meta | index.ToFlag() : Application.Meta & ~index.ToFlag(); + if (newFlag == Application.Meta) return false; - _designFlags = newFlag; - return true; - } - - public bool SetApplyVisorToggle(bool value) - { - var newFlag = value ? _designFlags | DesignFlags.ApplyVisorState : _designFlags & ~DesignFlags.ApplyVisorState; - if (newFlag == _designFlags) - return false; - - _designFlags = newFlag; - return true; - } - - public bool SetApplyWeaponVisible(bool value) - { - var newFlag = value ? _designFlags | DesignFlags.ApplyWeaponVisible : _designFlags & ~DesignFlags.ApplyWeaponVisible; - if (newFlag == _designFlags) - return false; - - _designFlags = newFlag; - return true; - } - - public bool SetApplyWetness(bool value) - { - var newFlag = value ? _designFlags | DesignFlags.ApplyWetness : _designFlags & ~DesignFlags.ApplyWetness; - if (newFlag == _designFlags) - return false; - - _designFlags = newFlag; + Application.Meta = newFlag; return true; } public bool SetWriteProtected(bool value) { - var newFlag = value ? _designFlags | DesignFlags.WriteProtected : _designFlags & ~DesignFlags.WriteProtected; - if (newFlag == _designFlags) + if (value == _writeProtected) return false; - _designFlags = newFlag; + _writeProtected = value; return true; } public bool DoApplyEquip(EquipSlot slot) - => ApplyEquip.HasFlag(slot.ToFlag()); + => Application.Equip.HasFlag(slot.ToFlag()); public bool DoApplyStain(EquipSlot slot) - => ApplyEquip.HasFlag(slot.ToStainFlag()); + => Application.Equip.HasFlag(slot.ToStainFlag()); public bool DoApplyCustomize(CustomizeIndex idx) - => idx is not CustomizeIndex.Race and not CustomizeIndex.BodyType && ApplyCustomize.HasFlag(idx.ToFlag()); + => Application.Customize.HasFlag(idx.ToFlag()); + + public bool DoApplyCrest(CrestFlag slot) + => Application.Crest.HasFlag(slot); + + public bool DoApplyParameter(CustomizeParameterFlag flag) + => Application.Parameters.HasFlag(flag); + + public bool DoApplyBonusItem(BonusItemFlag slot) + => Application.BonusItem.HasFlag(slot); internal bool SetApplyEquip(EquipSlot slot, bool value) { - var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag(); - if (newValue == ApplyEquip) + var newValue = value ? Application.Equip | slot.ToFlag() : Application.Equip & ~slot.ToFlag(); + if (newValue == Application.Equip) return false; - ApplyEquip = newValue; + Application.Equip = newValue; + return true; + } + + internal bool SetApplyBonusItem(BonusItemFlag slot, bool value) + { + var newValue = value ? Application.BonusItem | slot : Application.BonusItem & ~slot; + if (newValue == Application.BonusItem) + return false; + + Application.BonusItem = newValue; return true; } internal bool SetApplyStain(EquipSlot slot, bool value) { - var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag(); - if (newValue == ApplyEquip) + var newValue = value ? Application.Equip | slot.ToStainFlag() : Application.Equip & ~slot.ToStainFlag(); + if (newValue == Application.Equip) return false; - ApplyEquip = newValue; + Application.Equip = newValue; return true; } internal bool SetApplyCustomize(CustomizeIndex idx, bool value) { - var newValue = value ? ApplyCustomize | idx.ToFlag() : ApplyCustomize & ~idx.ToFlag(); - if (newValue == ApplyCustomize) + var newValue = value ? Application.Customize | idx.ToFlag() : Application.Customize & ~idx.ToFlag(); + if (newValue == Application.Customize) return false; - ApplyCustomize = newValue; + Application.Customize = newValue; return true; } - public void FixCustomizeApplication(CustomizationService service, CustomizeFlag flags) - => FixCustomizeApplication(service.AwaitedService.GetList(DesignData.Customize.Clan, DesignData.Customize.Gender), flags); + internal bool SetApplyCrest(CrestFlag slot, bool value) + { + var newValue = value ? Application.Crest | slot : Application.Crest & ~slot; + if (newValue == Application.Crest) + return false; - public void FixCustomizeApplication(CustomizationSet set, CustomizeFlag flags) - => ApplyCustomize = flags.FixApplication(set); + Application.Crest = newValue; + return true; + } - internal FlagRestrictionResetter TemporarilyRestrictApplication(EquipFlag equipFlags, CustomizeFlag customizeFlags) - => new(this, equipFlags, customizeFlags); + internal bool SetApplyParameter(CustomizeParameterFlag flag, bool value) + { + var newValue = value ? Application.Parameters | flag : Application.Parameters & ~flag; + if (newValue == Application.Parameters) + return false; + + Application.Parameters = newValue; + return true; + } + + public IEnumerable FilteredItemNames + => _designData.FilteredItemNames(Application.Equip, Application.BonusItem); + + internal FlagRestrictionResetter TemporarilyRestrictApplication(ApplicationCollection restrictions) + => new(this, restrictions); internal readonly struct FlagRestrictionResetter : IDisposable { - private readonly DesignBase _design; - private readonly EquipFlag _oldEquipFlags; - private readonly CustomizeFlag _oldCustomizeFlags; + private readonly DesignBase _design; + private readonly ApplicationCollection _oldFlags; - public FlagRestrictionResetter(DesignBase d, EquipFlag equipFlags, CustomizeFlag customizeFlags) + public FlagRestrictionResetter(DesignBase d, ApplicationCollection restrictions) { - _design = d; - _oldEquipFlags = d.ApplyEquip; - _oldCustomizeFlags = d.ApplyCustomize; - d.ApplyEquip &= equipFlags; - d.ApplyCustomize &= customizeFlags; + _design = d; + _oldFlags = d.Application; + _design.Application = restrictions.Restrict(_oldFlags); } public void Dispose() - { - _design.ApplyEquip = _oldEquipFlags; - _design.ApplyCustomize = _oldCustomizeFlags; - } + => _design.Application = _oldFlags; } + private CustomizeSet SetCustomizationSet(CustomizeService customize) + => !_designData.IsHuman + ? customize.Manager.GetSet(SubRace.Midlander, Gender.Male) + : customize.Manager.GetSet(_designData.Customize.Clan, _designData.Customize.Gender); + #endregion #region Serialization @@ -192,39 +233,62 @@ public class DesignBase { ["FileVersion"] = FileVersion, ["Equipment"] = SerializeEquipment(), + ["Bonus"] = SerializeBonusItems(), ["Customize"] = SerializeCustomize(), + ["Parameters"] = SerializeParameters(), + ["Materials"] = SerializeMaterials(), }; return ret; } protected JObject SerializeEquipment() { - static JObject Serialize(CustomItemId id, StainId stain, bool apply, bool applyStain) - => new() - { - ["ItemId"] = id.Id, - ["Stain"] = stain.Id, - ["Apply"] = apply, - ["ApplyStain"] = applyStain, - }; - var ret = new JObject(); - if (DesignData.IsHuman) + if (_designData.IsHuman) { foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) { - var item = DesignData.Item(slot); - var stain = DesignData.Stain(slot); - ret[slot.ToString()] = Serialize(item.Id, stain, DoApplyEquip(slot), DoApplyStain(slot)); + var item = _designData.Item(slot); + var stains = _designData.Stain(slot); + var crestSlot = slot.ToCrestFlag(); + var crest = _designData.Crest(crestSlot); + ret[slot.ToString()] = Serialize(item.Id, stains, crest, DoApplyEquip(slot), DoApplyStain(slot), DoApplyCrest(crestSlot)); } - ret["Hat"] = new QuadBool(DesignData.IsHatVisible(), DoApplyHatVisible()).ToJObject("Show", "Apply"); - ret["Visor"] = new QuadBool(DesignData.IsVisorToggled(), DoApplyVisorToggle()).ToJObject("IsToggled", "Apply"); - ret["Weapon"] = new QuadBool(DesignData.IsWeaponVisible(), DoApplyWeaponVisible()).ToJObject("Show", "Apply"); + ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyMeta(MetaIndex.HatState)).ToJObject("Show", "Apply"); + ret["VieraEars"] = new QuadBool(_designData.AreEarsVisible(), DoApplyMeta(MetaIndex.EarState)).ToJObject("Show", "Apply"); + ret["Visor"] = new QuadBool(_designData.IsVisorToggled(), DoApplyMeta(MetaIndex.VisorState)).ToJObject("IsToggled", "Apply"); + ret["Weapon"] = new QuadBool(_designData.IsWeaponVisible(), DoApplyMeta(MetaIndex.WeaponState)).ToJObject("Show", "Apply"); } else { - ret["Array"] = DesignData.WriteEquipmentBytesBase64(); + ret["Array"] = _designData.WriteEquipmentBytesBase64(); + } + + return ret; + + static JObject Serialize(CustomItemId id, StainIds stains, bool crest, bool apply, bool applyStain, bool applyCrest) + => stains.AddToObject(new JObject + { + ["ItemId"] = id.Id, + ["Crest"] = crest, + ["Apply"] = apply, + ["ApplyStain"] = applyStain, + ["ApplyCrest"] = applyCrest, + }); + } + + protected JObject SerializeBonusItems() + { + var ret = new JObject(); + foreach (var slot in BonusExtensions.AllFlags) + { + var item = _designData.BonusItem(slot); + ret[slot.ToString()] = new JObject() + { + ["BonusId"] = item.Id.Id, + ["Apply"] = DoApplyBonusItem(slot), + }; } return ret; @@ -234,17 +298,17 @@ public class DesignBase { var ret = new JObject() { - ["ModelId"] = DesignData.ModelId, + ["ModelId"] = _designData.ModelId, }; - var customize = DesignData.Customize; - if (DesignData.IsHuman) + var customize = _designData.Customize; + if (_designData.IsHuman) foreach (var idx in Enum.GetValues()) { ret[idx.ToString()] = new JObject() { ["Value"] = customize[idx].Value, - ["Apply"] = DoApplyCustomize(idx), + ["Apply"] = Application.Customize.HasFlag(idx.ToFlag()), }; } else @@ -252,18 +316,105 @@ public class DesignBase ret["Wetness"] = new JObject() { - ["Value"] = DesignData.IsWet(), - ["Apply"] = DoApplyWetness(), + ["Value"] = _designData.IsWet(), + ["Apply"] = DoApplyMeta(MetaIndex.Wetness), }; return ret; } + protected JObject SerializeParameters() + { + var ret = new JObject(); + + foreach (var flag in CustomizeParameterExtensions.ValueFlags) + { + ret[flag.ToString()] = new JObject() + { + ["Value"] = DesignData.Parameters[flag][0], + ["Apply"] = DoApplyParameter(flag), + }; + } + + foreach (var flag in CustomizeParameterExtensions.PercentageFlags) + { + ret[flag.ToString()] = new JObject() + { + ["Percentage"] = DesignData.Parameters[flag][0], + ["Apply"] = DoApplyParameter(flag), + }; + } + + foreach (var flag in CustomizeParameterExtensions.RgbFlags) + { + ret[flag.ToString()] = new JObject() + { + ["Red"] = DesignData.Parameters[flag][0], + ["Green"] = DesignData.Parameters[flag][1], + ["Blue"] = DesignData.Parameters[flag][2], + ["Apply"] = DoApplyParameter(flag), + }; + } + + foreach (var flag in CustomizeParameterExtensions.RgbaFlags) + { + ret[flag.ToString()] = new JObject() + { + ["Red"] = DesignData.Parameters[flag][0], + ["Green"] = DesignData.Parameters[flag][1], + ["Blue"] = DesignData.Parameters[flag][2], + ["Alpha"] = DesignData.Parameters[flag][3], + ["Apply"] = DoApplyParameter(flag), + }; + } + + return ret; + } + + protected JObject SerializeMaterials() + { + var ret = new JObject(); + foreach (var (key, value) in Materials) + ret[key.ToString("X16")] = JToken.FromObject(value); + return ret; + } + + protected static void LoadMaterials(JToken? materials, DesignBase design, string name) + { + if (materials is not JObject obj) + return; + + design.GetMaterialDataRef().Clear(); + foreach (var (key, value) in obj.Properties().Zip(obj.PropertyValues())) + { + try + { + var k = uint.Parse(key.Name, NumberStyles.HexNumber); + var v = value.ToObject(); + if (!MaterialValueIndex.FromKey(k, out _)) + { + Glamourer.Messager.NotificationMessage($"Invalid material value key {k} for design {name}, skipped.", + NotificationType.Warning); + continue; + } + + if (!design.GetMaterialDataRef().TryAddValue(MaterialValueIndex.FromKey(k), v)) + Glamourer.Messager.NotificationMessage($"Duplicate material value key {k} for design {name}, skipped.", + NotificationType.Warning); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Error parsing material value for design {name}, skipped", + NotificationType.Warning); + } + } + } + #endregion #region Deserialization - public static DesignBase LoadDesignBase(CustomizationService customizations, ItemManager items, JObject json) + public static DesignBase LoadDesignBase(CustomizeService customizations, ItemManager items, JObject json) { var version = json["FileVersion"]?.ToObject() ?? 0; return version switch @@ -273,99 +424,219 @@ public class DesignBase }; } - private static DesignBase LoadDesignV1Base(CustomizationService customizations, ItemManager items, JObject json) + private static DesignBase LoadDesignV1Base(CustomizeService customizations, ItemManager items, JObject json) { - var ret = new DesignBase(items); + var ret = new DesignBase(customizations, items); LoadCustomize(customizations, json["Customize"], ret, "Temporary Design", false, true); LoadEquip(items, json["Equipment"], ret, "Temporary Design", true); + LoadParameters(json["Parameters"], ret, "Temporary Design"); + LoadMaterials(json["Materials"], ret, "Temporary Design"); + LoadBonus(items, ret, json["Bonus"]); return ret; } + protected static void LoadBonus(ItemManager items, DesignBase design, JToken? json) + { + if (json is not JObject) + { + design.Application.BonusItem = 0; + return; + } + + foreach (var slot in BonusExtensions.AllFlags) + { + if (json[slot.ToString()] is not JObject itemJson) + { + design.Application.BonusItem &= ~slot; + design.GetDesignDataRef().SetBonusItem(slot, EquipItem.BonusItemNothing(slot)); + continue; + } + + design.SetApplyBonusItem(slot, itemJson["Apply"]?.ToObject() ?? false); + var id = itemJson["BonusId"]?.ToObject() ?? 0; + var item = items.Resolve(slot, id); + design.GetDesignDataRef().SetBonusItem(slot, item); + } + } + + protected static void LoadParameters(JToken? parameters, DesignBase design, string name) + { + if (parameters == null) + { + design.Application.Parameters = 0; + design.GetDesignDataRef().Parameters = default; + return; + } + + foreach (var flag in CustomizeParameterExtensions.ValueFlags) + { + if (!TryGetToken(flag, out var token)) + continue; + + var value = token["Value"]?.ToObject() ?? 0f; + design.GetDesignDataRef().Parameters[flag] = new CustomizeParameterValue(value); + } + + foreach (var flag in CustomizeParameterExtensions.PercentageFlags) + { + if (!TryGetToken(flag, out var token)) + continue; + + var value = token["Percentage"]?.ToObject() ?? 0f; + design.GetDesignDataRef().Parameters[flag] = new CustomizeParameterValue(value); + } + + foreach (var flag in CustomizeParameterExtensions.RgbFlags) + { + if (!TryGetToken(flag, out var token)) + continue; + + var r = token["Red"]?.ToObject() ?? 0f; + var g = token["Green"]?.ToObject() ?? 0f; + var b = token["Blue"]?.ToObject() ?? 0f; + design.GetDesignDataRef().Parameters[flag] = new CustomizeParameterValue(r, g, b); + } + + foreach (var flag in CustomizeParameterExtensions.RgbaFlags) + { + if (!TryGetToken(flag, out var token)) + continue; + + var r = token["Red"]?.ToObject() ?? 0f; + var g = token["Green"]?.ToObject() ?? 0f; + var b = token["Blue"]?.ToObject() ?? 0f; + var a = token["Alpha"]?.ToObject() ?? 0f; + design.GetDesignDataRef().Parameters[flag] = new CustomizeParameterValue(r, g, b, a); + } + + MigrateLipOpacity(); + return; + + // Load the token and set application. + bool TryGetToken(CustomizeParameterFlag flag, [NotNullWhen(true)] out JToken? token) + { + token = parameters[flag.ToString()]; + if (token != null) + { + var apply = token["Apply"]?.ToObject() ?? false; + design.SetApplyParameter(flag, apply); + return true; + } + + design.Application.Parameters &= ~flag; + design.GetDesignDataRef().Parameters[flag] = CustomizeParameterValue.Zero; + return false; + } + + void MigrateLipOpacity() + { + var token = parameters["LipOpacity"]?["Percentage"]?.ToObject(); + var actualToken = parameters[CustomizeParameterFlag.LipDiffuse.ToString()]?["Alpha"]; + if (token != null && actualToken == null) + design.GetDesignDataRef().Parameters.LipDiffuse.W = token.Value; + } + } + protected static void LoadEquip(ItemManager items, JToken? equip, DesignBase design, string name, bool allowUnknown) { if (equip == null) { - design.DesignData.SetDefaultEquipment(items); + design._designData.SetDefaultEquipment(items); Glamourer.Messager.NotificationMessage("The loaded design does not contain any equipment data, reset to default.", NotificationType.Warning); return; } - if (!design.DesignData.IsHuman) + if (!design._designData.IsHuman) { var textArray = equip["Array"]?.ToObject() ?? string.Empty; - design.DesignData.SetEquipmentBytesFromBase64(textArray); + design._designData.SetEquipmentBytesFromBase64(textArray); return; } - static (CustomItemId, StainId, bool, bool) ParseItem(EquipSlot slot, JToken? item) + foreach (var slot in EquipSlotExtensions.EqdpSlots) { - var id = item?["ItemId"]?.ToObject() ?? ItemManager.NothingId(slot).Id; - var stain = (StainId)(item?["Stain"]?.ToObject() ?? 0); - var apply = item?["Apply"]?.ToObject() ?? false; - var applyStain = item?["ApplyStain"]?.ToObject() ?? false; - return (id, stain, apply, applyStain); + var (id, stains, crest, apply, applyStain, applyCrest) = ParseItem(slot, equip[slot.ToString()]); + + PrintWarning(items.ValidateItem(slot, id, out var item, allowUnknown)); + PrintWarning(items.ValidateStain(stains, out stains, allowUnknown)); + var crestSlot = slot.ToCrestFlag(); + design._designData.SetItem(slot, item); + design._designData.SetStain(slot, stains); + design._designData.SetCrest(crestSlot, crest); + design.SetApplyEquip(slot, apply); + design.SetApplyStain(slot, applyStain); + design.SetApplyCrest(crestSlot, applyCrest); } + { + var (id, stains, crest, apply, applyStain, applyCrest) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]); + if (id == ItemManager.NothingId(EquipSlot.MainHand)) + id = items.DefaultSword.ItemId; + var (idOff, stainsOff, crestOff, applyOff, applyStainOff, applyCrestOff) = + ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]); + if (id == ItemManager.NothingId(EquipSlot.OffHand)) + id = ItemManager.NothingId(FullEquipType.Shield); + + PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off, allowUnknown)); + PrintWarning(items.ValidateStain(stains, out stains, allowUnknown)); + PrintWarning(items.ValidateStain(stainsOff, out stainsOff, allowUnknown)); + design._designData.SetItem(EquipSlot.MainHand, main); + design._designData.SetItem(EquipSlot.OffHand, off); + design._designData.SetStain(EquipSlot.MainHand, stains); + design._designData.SetStain(EquipSlot.OffHand, stainsOff); + design._designData.SetCrest(CrestFlag.MainHand, crest); + design._designData.SetCrest(CrestFlag.OffHand, crestOff); + design.SetApplyEquip(EquipSlot.MainHand, apply); + design.SetApplyEquip(EquipSlot.OffHand, applyOff); + design.SetApplyStain(EquipSlot.MainHand, applyStain); + design.SetApplyStain(EquipSlot.OffHand, applyStainOff); + design.SetApplyCrest(CrestFlag.MainHand, applyCrest); + design.SetApplyCrest(CrestFlag.OffHand, applyCrestOff); + } + var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse); + design.SetApplyMeta(MetaIndex.HatState, metaValue.Enabled); + design._designData.SetHatVisible(metaValue.ForcedValue); + + metaValue = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse); + design.SetApplyMeta(MetaIndex.WeaponState, metaValue.Enabled); + design._designData.SetWeaponVisible(metaValue.ForcedValue); + + metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse); + design.SetApplyMeta(MetaIndex.VisorState, metaValue.Enabled); + design._designData.SetVisor(metaValue.ForcedValue); + + metaValue = QuadBool.FromJObject(equip["VieraEars"], "Show", "Apply", QuadBool.NullTrue); + design.SetApplyMeta(MetaIndex.EarState, metaValue.Enabled); + design._designData.SetEarsVisible(metaValue.ForcedValue); + return; + void PrintWarning(string msg) { if (msg.Length > 0 && name != "Temporary Design") Glamourer.Messager.NotificationMessage($"{msg} ({name})", NotificationType.Warning); } - foreach (var slot in EquipSlotExtensions.EqdpSlots) + static (CustomItemId, StainIds, bool, bool, bool, bool) ParseItem(EquipSlot slot, JToken? item) { - var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]); - - PrintWarning(items.ValidateItem(slot, id, out var item, allowUnknown)); - PrintWarning(items.ValidateStain(stain, out stain, allowUnknown)); - design.DesignData.SetItem(slot, item); - design.DesignData.SetStain(slot, stain); - design.SetApplyEquip(slot, apply); - design.SetApplyStain(slot, applyStain); + var id = item?["ItemId"]?.ToObject() ?? ItemManager.NothingId(slot).Id; + var stains = StainIds.ParseFromObject(item as JObject); + var crest = item?["Crest"]?.ToObject() ?? false; + var apply = item?["Apply"]?.ToObject() ?? false; + var applyStain = item?["ApplyStain"]?.ToObject() ?? false; + var applyCrest = item?["ApplyCrest"]?.ToObject() ?? false; + return (id, stains, crest, apply, applyStain, applyCrest); } - - { - var (id, stain, apply, applyStain) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]); - if (id == ItemManager.NothingId(EquipSlot.MainHand)) - id = items.DefaultSword.ItemId; - var (idOff, stainOff, applyOff, applyStainOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]); - if (id == ItemManager.NothingId(EquipSlot.OffHand)) - id = ItemManager.NothingId(FullEquipType.Shield); - - PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off, allowUnknown)); - PrintWarning(items.ValidateStain(stain, out stain, allowUnknown)); - PrintWarning(items.ValidateStain(stainOff, out stainOff, allowUnknown)); - design.DesignData.SetItem(EquipSlot.MainHand, main); - design.DesignData.SetItem(EquipSlot.OffHand, off); - design.DesignData.SetStain(EquipSlot.MainHand, stain); - design.DesignData.SetStain(EquipSlot.OffHand, stainOff); - design.SetApplyEquip(EquipSlot.MainHand, apply); - design.SetApplyEquip(EquipSlot.OffHand, applyOff); - design.SetApplyStain(EquipSlot.MainHand, applyStain); - design.SetApplyStain(EquipSlot.OffHand, applyStainOff); - } - var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse); - design.SetApplyHatVisible(metaValue.Enabled); - design.DesignData.SetHatVisible(metaValue.ForcedValue); - - metaValue = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse); - design.SetApplyWeaponVisible(metaValue.Enabled); - design.DesignData.SetWeaponVisible(metaValue.ForcedValue); - - metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse); - design.SetApplyVisorToggle(metaValue.Enabled); - design.DesignData.SetVisor(metaValue.ForcedValue); } - protected static void LoadCustomize(CustomizationService customizations, JToken? json, DesignBase design, string name, bool forbidNonHuman, + protected static void LoadCustomize(CustomizeService customizations, JToken? json, DesignBase design, string name, bool forbidNonHuman, bool allowUnknown) { if (json == null) { - design.DesignData.ModelId = 0; - design.DesignData.IsHuman = true; - design.DesignData.Customize = Customize.Default; + design._designData.ModelId = 0; + design._designData.IsHuman = true; + design.SetCustomize(customizations, CustomizeArray.Default); Glamourer.Messager.NotificationMessage("The loaded design does not contain any customization data, reset to default.", NotificationType.Warning); return; @@ -380,21 +651,23 @@ public class DesignBase } var wetness = QuadBool.FromJObject(json["Wetness"], "Value", "Apply", QuadBool.NullFalse); - design.DesignData.SetIsWet(wetness.ForcedValue); - design.SetApplyWetness(wetness.Enabled); + design._designData.SetIsWet(wetness.ForcedValue); + design.SetApplyMeta(MetaIndex.Wetness, wetness.Enabled); - design.DesignData.ModelId = json["ModelId"]?.ToObject() ?? 0; - PrintWarning(customizations.ValidateModelId(design.DesignData.ModelId, out design.DesignData.ModelId, out design.DesignData.IsHuman)); - if (design.DesignData.ModelId != 0 && forbidNonHuman) + design._designData.ModelId = json["ModelId"]?.ToObject() ?? 0; + PrintWarning(customizations.ValidateModelId(design._designData.ModelId, out design._designData.ModelId, + out design._designData.IsHuman)); + if (design._designData.ModelId != 0 && forbidNonHuman) { PrintWarning("Model IDs different from 0 are not currently allowed, reset model id to 0."); - design.DesignData.ModelId = 0; - design.DesignData.IsHuman = true; + design._designData.ModelId = 0; + design._designData.IsHuman = true; } - else if (!design.DesignData.IsHuman) + else if (!design._designData.IsHuman) { var arrayText = json["Array"]?.ToObject() ?? string.Empty; - design.DesignData.Customize.LoadBase64(arrayText); + design._designData.Customize.LoadBase64(arrayText); + design.CustomizeSet = design.SetCustomizationSet(customizations); return; } @@ -403,51 +676,45 @@ public class DesignBase PrintWarning(customizations.ValidateClan(clan, race, out race, out clan)); var gender = (Gender)((json[CustomizeIndex.Gender.ToString()]?["Value"]?.ToObject() ?? 0) + 1); PrintWarning(customizations.ValidateGender(race, gender, out gender)); - design.DesignData.Customize.Race = race; - design.DesignData.Customize.Clan = clan; - design.DesignData.Customize.Gender = gender; - design.SetApplyCustomize(CustomizeIndex.Race, json[CustomizeIndex.Race.ToString()]?["Apply"]?.ToObject() ?? false); - design.SetApplyCustomize(CustomizeIndex.Clan, json[CustomizeIndex.Clan.ToString()]?["Apply"]?.ToObject() ?? false); - design.SetApplyCustomize(CustomizeIndex.Gender, json[CustomizeIndex.Gender.ToString()]?["Apply"]?.ToObject() ?? false); - - var set = customizations.AwaitedService.GetList(clan, gender); + var bodyType = (CustomizeValue)(json[CustomizeIndex.BodyType.ToString()]?["Value"]?.ToObject() ?? 1); + design._designData.Customize.Race = race; + design._designData.Customize.Clan = clan; + design._designData.Customize.Gender = gender; + design._designData.Customize.BodyType = bodyType; + design.CustomizeSet = design.SetCustomizationSet(customizations); + design.SetApplyCustomize(CustomizeIndex.Race, json[CustomizeIndex.Race.ToString()]?["Apply"]?.ToObject() ?? false); + design.SetApplyCustomize(CustomizeIndex.Clan, json[CustomizeIndex.Clan.ToString()]?["Apply"]?.ToObject() ?? false); + design.SetApplyCustomize(CustomizeIndex.Gender, json[CustomizeIndex.Gender.ToString()]?["Apply"]?.ToObject() ?? false); + design.SetApplyCustomize(CustomizeIndex.BodyType, bodyType != 0); + var set = design.CustomizeSet; foreach (var idx in CustomizationExtensions.AllBasic) { - if (set.IsAvailable(idx)) - { - var tok = json[idx.ToString()]; - var data = (CustomizeValue)(tok?["Value"]?.ToObject() ?? 0); - PrintWarning(CustomizationService.ValidateCustomizeValue(set, design.DesignData.Customize.Face, idx, data, out data, + var tok = json[idx.ToString()]; + var data = (CustomizeValue)(tok?["Value"]?.ToObject() ?? 0); + if (set.IsAvailable(idx) && design._designData.Customize.BodyType == 1) + PrintWarning(CustomizeService.ValidateCustomizeValue(set, design._designData.Customize.Face, idx, data, out data, allowUnknown)); - var apply = tok?["Apply"]?.ToObject() ?? false; - design.DesignData.Customize[idx] = data; - design.SetApplyCustomize(idx, apply); - } - else - { - design.DesignData.Customize[idx] = CustomizeValue.Zero; - design.SetApplyCustomize(idx, false); - } + var apply = tok?["Apply"]?.ToObject() ?? false; + design._designData.Customize[idx] = data; + design.SetApplyCustomize(idx, apply); } - - design.FixCustomizeApplication(set, design.ApplyCustomize); } - public void MigrateBase64(ItemManager items, HumanModelList humans, string base64) + public void MigrateBase64(CustomizeService customize, ItemManager items, HumanModelList humans, string base64) { try { - DesignData = DesignBase64Migration.MigrateBase64(items, humans, base64, out var equipFlags, out var customizeFlags, - out var writeProtected, - out var applyHat, out var applyVisor, out var applyWeapon); - ApplyEquip = equipFlags; - ApplyCustomize = customizeFlags; + _designData = DesignBase64Migration.MigrateBase64(items, humans, base64, out var equipFlags, out var customizeFlags, + out var writeProtected, out var applyMeta); + Application.Equip = equipFlags; + ApplyCustomize = customizeFlags; + Application.Parameters = 0; + Application.Crest = 0; + Application.Meta = applyMeta; + Application.BonusItem = 0; SetWriteProtected(writeProtected); - SetApplyHatVisible(applyHat); - SetApplyVisorToggle(applyVisor); - SetApplyWeaponVisible(applyWeapon); - SetApplyWetness(true); + CustomizeSet = SetCustomizationSet(customize); } catch (Exception ex) { @@ -455,15 +722,5 @@ public class DesignBase } } - public void RemoveInvalidCustomize(CustomizationService customizations) - { - var set = customizations.AwaitedService.GetList(DesignData.Customize.Clan, DesignData.Customize.Gender); - foreach (var idx in CustomizationExtensions.AllBasic.Where(i => !set.IsAvailable(i))) - { - DesignData.Customize[idx] = CustomizeValue.Zero; - SetApplyCustomize(idx, false); - } - } - #endregion } diff --git a/Glamourer/Designs/DesignBase64Migration.cs b/Glamourer/Designs/DesignBase64Migration.cs index 18cef7a..8cd137f 100644 --- a/Glamourer/Designs/DesignBase64Migration.cs +++ b/Glamourer/Designs/DesignBase64Migration.cs @@ -1,9 +1,8 @@ -using System; -using Glamourer.Customization; +using Glamourer.Api.Enums; using Glamourer.Services; -using Glamourer.Structs; using OtterGui; -using Penumbra.GameData.Data; +using OtterGui.Extensions; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -16,7 +15,7 @@ public class DesignBase64Migration public const int Base64SizeV4 = 95; public static unsafe DesignData MigrateBase64(ItemManager items, HumanModelList humans, string base64, out EquipFlag equipFlags, - out CustomizeFlag customizeFlags, out bool writeProtected, out bool applyHat, out bool applyVisor, out bool applyWeapon) + out CustomizeFlag customizeFlags, out bool writeProtected, out MetaFlag metaFlags) { static void CheckSize(int length, int requiredLength) { @@ -28,9 +27,7 @@ public class DesignBase64Migration byte applicationFlags; ushort equipFlagsS; var bytes = Convert.FromBase64String(base64); - applyHat = false; - applyVisor = false; - applyWeapon = false; + metaFlags = MetaFlag.Wetness; var data = new DesignData(); switch (bytes[0]) { @@ -62,7 +59,7 @@ public class DesignBase64Migration data.SetHatVisible((bytes[90] & 0x01) == 0); data.SetVisor((bytes[90] & 0x10) != 0); data.SetWeaponVisible((bytes[90] & 0x02) == 0); - data.ModelId = (uint)bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24); + data.ModelId = bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24); break; } case 5: @@ -73,16 +70,19 @@ public class DesignBase64Migration data.SetHatVisible((bytes[90] & 0x01) == 0); data.SetVisor((bytes[90] & 0x10) != 0); data.SetWeaponVisible((bytes[90] & 0x02) == 0); - data.ModelId = (uint)bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24); + data.ModelId = bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24); break; default: throw new Exception($"Can not parse Base64 string into design for migration:\n\tInvalid Version {bytes[0]}."); } customizeFlags = (applicationFlags & 0x01) != 0 ? CustomizeFlagExtensions.All : 0; data.SetIsWet((applicationFlags & 0x02) != 0); - applyHat = (applicationFlags & 0x04) != 0; - applyWeapon = (applicationFlags & 0x08) != 0; - applyVisor = (applicationFlags & 0x10) != 0; + if ((applicationFlags & 0x04) != 0) + metaFlags |= MetaFlag.HatState; + if ((applicationFlags & 0x08) != 0) + metaFlags |= MetaFlag.WeaponState; + if ((applicationFlags & 0x10) != 0) + metaFlags |= MetaFlag.VisorState; writeProtected = (applicationFlags & 0x20) != 0; equipFlags = 0; @@ -97,16 +97,16 @@ public class DesignBase64Migration fixed (byte* ptr = bytes) { - var cur = (CharacterWeapon*)(ptr + 30); - var eq = (CharacterArmor*)(cur + 2); + var cur = (LegacyCharacterWeapon*)(ptr + 30); + var eq = (LegacyCharacterArmor*)(cur + 2); if (!humans.IsHuman(data.ModelId)) { - data.LoadNonHuman(data.ModelId, *(Customize*)(ptr + 4), (nint)eq); + data.LoadNonHuman(data.ModelId, *(CustomizeArray*)(ptr + 4), (nint)eq); return data; } - data.Customize.Load(*(Customize*)(ptr + 4)); + data.Customize = *(CustomizeArray*)(ptr + 4); foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex()) { var mdl = eq[idx]; @@ -121,9 +121,9 @@ public class DesignBase64Migration data.SetStain(slot, mdl.Stain); } - var main = cur[0].Set.Id == 0 + var main = cur[0].Skeleton.Id == 0 ? items.DefaultSword - : items.Identify(EquipSlot.MainHand, cur[0].Set, cur[0].Type, cur[0].Variant); + : items.Identify(EquipSlot.MainHand, cur[0].Skeleton, cur[0].Weapon, cur[0].Variant); if (!main.Valid) { Glamourer.Log.Warning("Base64 string invalid, weapon could not be identified."); @@ -135,10 +135,10 @@ public class DesignBase64Migration EquipItem off; // Fist weapon hack - if (main.ModelId.Id is > 1600 and < 1651 && cur[1].Variant == 0) + if (main.PrimaryId.Id is > 1600 and < 1651 && cur[1].Variant == 0) { - off = items.Identify(EquipSlot.OffHand, (SetId)(main.ModelId.Id + 50), main.WeaponType, main.Variant, main.Type); - var gauntlet = items.Identify(EquipSlot.Hands, cur[1].Set, (Variant)cur[1].Type.Id); + off = items.Identify(EquipSlot.OffHand, (PrimaryId)(main.PrimaryId.Id + 50), main.SecondaryId, main.Variant, main.Type); + var gauntlet = items.Identify(EquipSlot.Hands, cur[1].Skeleton, (Variant)cur[1].Weapon.Id); if (gauntlet.Valid) { data.SetItem(EquipSlot.Hands, gauntlet); @@ -147,9 +147,9 @@ public class DesignBase64Migration } else { - off = cur[0].Set.Id == 0 + off = cur[0].Skeleton.Id == 0 ? ItemManager.NothingItem(FullEquipType.Shield) - : items.Identify(EquipSlot.OffHand, cur[1].Set, cur[1].Type, cur[1].Variant, main.Type); + : items.Identify(EquipSlot.OffHand, cur[1].Skeleton, cur[1].Weapon, cur[1].Variant, main.Type); } if (main.Type.ValidOffhand() != FullEquipType.Unknown && !off.Valid) @@ -164,16 +164,16 @@ public class DesignBase64Migration } } - public static unsafe string CreateOldBase64(in DesignData save, EquipFlag equipFlags, CustomizeFlag customizeFlags, - bool setHat, bool setVisor, bool setWeapon, bool writeProtected, float alpha = 1.0f) + public static unsafe string CreateOldBase64(in DesignData save, EquipFlag equipFlags, CustomizeFlag customizeFlags, MetaFlag meta, + bool writeProtected, float alpha = 1.0f) { var data = stackalloc byte[Base64SizeV4]; data[0] = 5; data[1] = (byte)((customizeFlags == CustomizeFlagExtensions.All ? 0x01 : 0) | (save.IsWet() ? 0x02 : 0) - | (setHat ? 0x04 : 0) - | (setWeapon ? 0x08 : 0) - | (setVisor ? 0x10 : 0) + | (meta.HasFlag(MetaFlag.HatState) ? 0x04 : 0) + | (meta.HasFlag(MetaFlag.WeaponState) ? 0x08 : 0) + | (meta.HasFlag(MetaFlag.VisorState) ? 0x10 : 0) | (writeProtected ? 0x20 : 0)); data[2] = (byte)((equipFlags.HasFlag(EquipFlag.Mainhand) ? 0x01 : 0) | (equipFlags.HasFlag(EquipFlag.Offhand) ? 0x02 : 0) @@ -187,11 +187,13 @@ public class DesignBase64Migration | (equipFlags.HasFlag(EquipFlag.Wrist) ? 0x02 : 0) | (equipFlags.HasFlag(EquipFlag.RFinger) ? 0x04 : 0) | (equipFlags.HasFlag(EquipFlag.LFinger) ? 0x08 : 0)); - save.Customize.Write((nint)data + 4); - ((CharacterWeapon*)(data + 30))[0] = save.Item(EquipSlot.MainHand).Weapon(save.Stain(EquipSlot.MainHand)); - ((CharacterWeapon*)(data + 30))[1] = save.Item(EquipSlot.OffHand).Weapon(save.Stain(EquipSlot.OffHand)); + save.Customize.Write(data + 4); + ((LegacyCharacterWeapon*)(data + 30))[0] = + new LegacyCharacterWeapon(save.Item(EquipSlot.MainHand).Weapon(save.Stain(EquipSlot.MainHand))); + ((LegacyCharacterWeapon*)(data + 30))[1] = + new LegacyCharacterWeapon(save.Item(EquipSlot.OffHand).Weapon(save.Stain(EquipSlot.OffHand))); foreach (var slot in EquipSlotExtensions.EqdpSlots) - ((CharacterArmor*)(data + 44))[slot.ToIndex()] = save.Item(slot).Armor(save.Stain(slot)); + ((LegacyCharacterArmor*)(data + 44))[slot.ToIndex()] = new LegacyCharacterArmor(save.Item(slot).Armor(save.Stain(slot))); *(ushort*)(data + 84) = 1; // IsSet. *(float*)(data + 86) = 1f; data[90] = (byte)((save.IsHatVisible() ? 0x00 : 0x01) diff --git a/Glamourer/Designs/DesignColors.cs b/Glamourer/Designs/DesignColors.cs new file mode 100644 index 0000000..a8f3178 --- /dev/null +++ b/Glamourer/Designs/DesignColors.cs @@ -0,0 +1,292 @@ +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Utility.Raii; +using Glamourer.Gui; +using Glamourer.Services; +using Dalamud.Bindings.ImGui; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Extensions; + +namespace Glamourer.Designs; + +public class DesignColorUi(DesignColors colors, Configuration config) +{ + private string _newName = string.Empty; + + public void Draw() + { + using var table = ImRaii.Table("designColors", 3, ImGuiTableFlags.RowBg); + if (!table) + return; + + var changeString = string.Empty; + uint? changeValue = null; + + var buttonSize = new Vector2(ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("##Delete", ImGuiTableColumnFlags.WidthFixed, buttonSize.X); + ImGui.TableSetupColumn("##Select", ImGuiTableColumnFlags.WidthFixed, buttonSize.X); + ImGui.TableSetupColumn("Color Name", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableHeadersRow(); + + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), buttonSize, + "Revert the color used for missing design colors to its default.", colors.MissingColor == DesignColors.MissingColorDefault, + true)) + { + changeString = DesignColors.MissingColorName; + changeValue = DesignColors.MissingColorDefault; + } + + ImGui.TableNextColumn(); + if (DrawColorButton(DesignColors.MissingColorName, colors.MissingColor, out var newColor)) + { + changeString = DesignColors.MissingColorName; + changeValue = newColor; + } + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(DesignColors.MissingColorName); + ImGuiUtil.HoverTooltip("This color is used when the color specified in a design is not available."); + + + var disabled = !config.DeleteDesignModifier.IsActive(); + var tt = "Delete this color. This does not remove it from designs using it."; + if (disabled) + tt += $"\nHold {config.DeleteDesignModifier} to delete."; + + foreach (var ((name, color), idx) in colors.WithIndex()) + { + using var id = ImRaii.PushId(idx); + ImGui.TableNextColumn(); + + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), buttonSize, tt, disabled, true)) + { + changeString = name; + changeValue = null; + } + + ImGui.TableNextColumn(); + if (DrawColorButton(name, color, out newColor)) + { + changeString = name; + changeValue = newColor; + } + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(name); + } + + ImGui.TableNextColumn(); + (tt, disabled) = _newName.Length == 0 + ? ("Specify a name for a new color first.", true) + : _newName is DesignColors.MissingColorName or DesignColors.AutomaticName + ? ($"You can not use the name {DesignColors.MissingColorName} or {DesignColors.AutomaticName}, choose a different one.", true) + : colors.ContainsKey(_newName) + ? ($"The color {_newName} already exists, please choose a different name.", true) + : ($"Add a new color {_newName} to your list.", false); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), buttonSize, tt, disabled, true)) + { + changeString = _newName; + changeValue = 0xFFFFFFFF; + } + + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputTextWithHint("##newDesignColor", "New Color Name...", ref _newName, 64, ImGuiInputTextFlags.EnterReturnsTrue)) + { + changeString = _newName; + changeValue = 0xFFFFFFFF; + } + + + if (changeString.Length > 0) + { + if (!changeValue.HasValue) + colors.DeleteColor(changeString); + else + colors.SetColor(changeString, changeValue.Value); + } + } + + public static bool DrawColorButton(string tooltip, uint color, out uint newColor) + { + var vec = ImGui.ColorConvertU32ToFloat4(color); + if (!ImGui.ColorEdit4(tooltip, ref vec, ImGuiColorEditFlags.AlphaPreviewHalf | ImGuiColorEditFlags.NoInputs)) + { + ImGuiUtil.HoverTooltip(tooltip); + newColor = color; + return false; + } + + ImGuiUtil.HoverTooltip(tooltip); + + newColor = ImGui.ColorConvertFloat4ToU32(vec); + return newColor != color; + } +} + +public class DesignColors : ISavable, IReadOnlyDictionary +{ + public const string AutomaticName = "Automatic"; + public const string MissingColorName = "Missing Color"; + public const uint MissingColorDefault = 0xFF0000D0; + + private readonly SaveService _saveService; + private readonly Dictionary _colors = []; + public uint MissingColor { get; private set; } = MissingColorDefault; + + public event Action? ColorChanged; + + public DesignColors(SaveService saveService) + { + _saveService = saveService; + Load(); + } + + public uint GetColor(Design? design) + { + if (design == null) + return ColorId.NormalDesign.Value(); + + if (design.Color.Length == 0) + return AutoColor(design); + + return TryGetValue(design.Color, out var color) ? color : MissingColor; + } + + public void SetColor(string key, uint newColor) + { + if (key.Length == 0) + return; + + if (key is MissingColorName && MissingColor != newColor) + { + MissingColor = newColor; + SaveAndInvoke(); + return; + } + + if (_colors.TryAdd(key, newColor)) + { + SaveAndInvoke(); + return; + } + + _colors.TryGetValue(key, out var color); + _colors[key] = newColor; + + if (color != newColor) + SaveAndInvoke(); + } + + private void SaveAndInvoke() + { + ColorChanged?.Invoke(); + _saveService.DelaySave(this, TimeSpan.FromSeconds(2)); + } + + public void DeleteColor(string key) + { + if (_colors.Remove(key)) + SaveAndInvoke(); + } + + public string ToFilename(FilenameService fileNames) + => fileNames.DesignColorFile; + + public void Save(StreamWriter writer) + { + var jObj = new JObject + { + ["Version"] = 1, + ["MissingColor"] = MissingColor, + ["Definitions"] = JToken.FromObject(_colors), + }; + writer.Write(jObj.ToString(Formatting.Indented)); + } + + private void Load() + { + _colors.Clear(); + var file = _saveService.FileNames.DesignColorFile; + if (!File.Exists(file)) + return; + + try + { + var text = File.ReadAllText(file); + var jObj = JObject.Parse(text); + var version = jObj["Version"]?.ToObject() ?? 0; + switch (version) + { + case 1: + { + var dict = jObj["Definitions"]?.ToObject>() ?? new Dictionary(); + _colors.EnsureCapacity(dict.Count); + foreach (var kvp in dict) + _colors.Add(kvp.Key, kvp.Value); + MissingColor = jObj["MissingColor"]?.ToObject() ?? MissingColorDefault; + break; + } + default: throw new Exception($"Unknown Version {version}"); + } + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, "Could not read design color file.", NotificationType.Error); + } + } + + public IEnumerator> GetEnumerator() + => _colors.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _colors.Count; + + public bool ContainsKey(string key) + => _colors.ContainsKey(key); + + public bool TryGetValue(string key, out uint value) + { + if (_colors.TryGetValue(key, out value)) + { + if (value == 0) + value = ImGui.GetColorU32(ImGuiCol.Text); + return true; + } + + return false; + } + + public static uint AutoColor(DesignBase design) + { + var customize = design.ApplyCustomizeExcludingBodyType == 0; + var equip = design.Application.Equip == 0; + return (customize, equip) switch + { + (true, true) => ColorId.StateDesign.Value(), + (true, false) => ColorId.EquipmentDesign.Value(), + (false, true) => ColorId.CustomizationDesign.Value(), + (false, false) => ColorId.NormalDesign.Value(), + }; + } + + public uint this[string key] + => _colors[key]; + + public IEnumerable Keys + => _colors.Keys; + + public IEnumerable Values + => _colors.Values; +} diff --git a/Glamourer/Designs/DesignConverter.cs b/Glamourer/Designs/DesignConverter.cs index 22bf287..058b023 100644 --- a/Glamourer/Designs/DesignConverter.cs +++ b/Glamourer/Designs/DesignConverter.cs @@ -1,34 +1,26 @@ -using System; -using System.Diagnostics; -using System.Text; -using Glamourer.Customization; +using Glamourer.Designs.Links; +using Glamourer.Interop.Material; using Glamourer.Services; using Glamourer.State; -using Glamourer.Structs; using Glamourer.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Structs; namespace Glamourer.Designs; -public class DesignConverter +public class DesignConverter( + SaveService saveService, + ItemManager _items, + DesignManager _designs, + CustomizeService _customize, + HumanModelList _humans, + DesignLinkLoader _linkLoader) { - public const byte Version = 5; - - private readonly ItemManager _items; - private readonly DesignManager _designs; - private readonly CustomizationService _customize; - private readonly HumanModelList _humans; - - public DesignConverter(ItemManager items, DesignManager designs, CustomizationService customize, HumanModelList humans) - { - _items = items; - _designs = designs; - _customize = customize; - _humans = humans; - } + public const byte Version = 6; public JObject ShareJObject(DesignBase design) => design.JsonSerialize(); @@ -36,40 +28,66 @@ public class DesignConverter public JObject ShareJObject(Design design) => design.JsonSerialize(); - public JObject ShareJObject(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags) + public JObject ShareJObject(ActorState state, in ApplicationRules rules) { - var design = Convert(state, equipFlags, customizeFlags); + var design = Convert(state, rules); return ShareJObject(design); } public string ShareBase64(Design design) - => ShareBackwardCompatible(ShareJObject(design), design); + => ToBase64(ShareJObject(design)); public string ShareBase64(DesignBase design) - => ShareBackwardCompatible(ShareJObject(design), design); + => ToBase64(ShareJObject(design)); - public string ShareBase64(ActorState state) - => ShareBase64(state, EquipFlagExtensions.All, CustomizeFlagExtensions.All); + public string ShareBase64(ActorState state, in ApplicationRules rules) + => ShareBase64(state.ModelData, state.Materials, rules); - public string ShareBase64(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags) + public string ShareBase64(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules) { - var design = Convert(state, equipFlags, customizeFlags); - return ShareBackwardCompatible(ShareJObject(design), design); + var design = Convert(data, materials, rules); + return ToBase64(ShareJObject(design)); } - public DesignBase Convert(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags) + public DesignBase Convert(ActorState state, in ApplicationRules rules) + => Convert(state.ModelData, state.Materials, rules); + + public DesignBase Convert(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules) { var design = _designs.CreateTemporary(); - design.ApplyEquip = equipFlags & EquipFlagExtensions.All; - design.SetApplyHatVisible(design.DoApplyEquip(EquipSlot.Head)); - design.SetApplyVisorToggle(design.DoApplyEquip(EquipSlot.Head)); - design.SetApplyWeaponVisible(design.DoApplyEquip(EquipSlot.MainHand) || design.DoApplyEquip(EquipSlot.OffHand)); - design.SetApplyWetness(true); - design.DesignData = state.ModelData; - design.FixCustomizeApplication(_customize, customizeFlags); + rules.Apply(design); + design.SetDesignData(_customize, data); + if (rules.Materials) + ComputeMaterials(design.GetMaterialDataRef(), materials, rules.Equip); return design; } + public DesignBase? FromJObject(JObject? jObject, bool customize, bool equip) + { + if (jObject == null) + return null; + + try + { + var ret = jObject["Identifier"] != null + ? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObject) + : DesignBase.LoadDesignBase(_customize, _items, jObject); + + if (!customize) + ret.Application.RemoveCustomize(); + + if (!equip) + ret.Application.RemoveEquip(); + + return ret; + } + catch (Exception ex) + { + Glamourer.Log.Warning($"Failure to parse JObject to design:\n{ex}"); + return null; + } + } + public DesignBase? FromBase64(string base64, bool customize, bool equip, out byte version) { DesignBase ret; @@ -83,14 +101,14 @@ public class DesignConverter case (byte)'{': var jObj1 = JObject.Parse(Encoding.UTF8.GetString(bytes)); ret = jObj1["Identifier"] != null - ? Design.LoadDesign(_customize, _items, jObj1) + ? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj1) : DesignBase.LoadDesignBase(_customize, _items, jObj1); break; case 1: case 2: case 4: ret = _designs.CreateTemporary(); - ret.MigrateBase64(_items, _humans, base64); + ret.MigrateBase64(_customize, _items, _humans, base64); break; case 3: { @@ -98,21 +116,32 @@ public class DesignConverter var jObj2 = JObject.Parse(decompressed); Debug.Assert(version == 3); ret = jObj2["Identifier"] != null - ? Design.LoadDesign(_customize, _items, jObj2) + ? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2) : DesignBase.LoadDesignBase(_customize, _items, jObj2); break; } - case Version: + case 5: { bytes = bytes[DesignBase64Migration.Base64SizeV4..]; version = bytes.DecompressToString(out var decompressed); var jObj2 = JObject.Parse(decompressed); - Debug.Assert(version == Version); + Debug.Assert(version == 5); ret = jObj2["Identifier"] != null - ? Design.LoadDesign(_customize, _items, jObj2) + ? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2) : DesignBase.LoadDesignBase(_customize, _items, jObj2); break; } + case 6: + { + version = bytes.DecompressToString(out var decompressed); + var jObj2 = JObject.Parse(decompressed); + Debug.Assert(version == 6); + ret = jObj2["Identifier"] != null + ? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2) + : DesignBase.LoadDesignBase(_customize, _items, jObj2); + break; + } + default: throw new Exception($"Unknown Version {bytes[0]}."); } } @@ -123,43 +152,103 @@ public class DesignConverter } if (!customize) - { - ret.ApplyCustomize = 0; - ret.SetApplyWetness(false); - } - else - { - ret.FixCustomizeApplication(_customize, ret.ApplyCustomize); - } + ret.Application.RemoveCustomize(); if (!equip) - { - ret.ApplyEquip = 0; - ret.SetApplyHatVisible(false); - ret.SetApplyWeaponVisible(false); - ret.SetApplyVisorToggle(false); - } + ret.Application.RemoveEquip(); return ret; } - private static string ShareBase64(JObject jObj) + public static string ToBase64(JToken jObject) { - var json = jObj.ToString(Formatting.None); + var json = jObject.ToString(Formatting.None); var compressed = json.Compress(Version); return System.Convert.ToBase64String(compressed); } - private static string ShareBackwardCompatible(JObject jObject, DesignBase design) + public IEnumerable<(EquipSlot Slot, EquipItem Item, StainIds Stains)> FromDrawData(IReadOnlyList armors, + CharacterWeapon mainhand, CharacterWeapon offhand, bool skipWarnings) { - var oldBase64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.ApplyEquip, design.ApplyCustomize, - design.DoApplyHatVisible(), design.DoApplyVisorToggle(), design.DoApplyWeaponVisible(), design.WriteProtected(), 1f); - var oldBytes = System.Convert.FromBase64String(oldBase64); - var json = jObject.ToString(Formatting.None); - var compressed = json.Compress(Version); - var bytes = new byte[oldBytes.Length + compressed.Length]; - oldBytes.CopyTo(bytes, 0); - compressed.CopyTo(bytes, oldBytes.Length); - return System.Convert.ToBase64String(bytes); + if (armors.Count != 10) + throw new ArgumentException("Invalid length of armor array."); + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var index = (int)slot.ToIndex(); + var armor = armors[index]; + var item = _items.Identify(slot, armor.Set, armor.Variant); + if (!item.Valid) + { + if (!skipWarnings) + Glamourer.Log.Warning($"Appearance data {armor} for slot {slot} invalid, item could not be identified."); + item = ItemManager.NothingItem(slot); + } + + yield return (slot, item, armor.Stains); + } + + var mh = _items.Identify(EquipSlot.MainHand, mainhand.Skeleton, mainhand.Weapon, mainhand.Variant); + if (!skipWarnings && !mh.Valid) + { + Glamourer.Log.Warning($"Appearance data {mainhand} for mainhand weapon invalid, item could not be identified."); + mh = _items.DefaultSword; + } + + yield return (EquipSlot.MainHand, mh, mainhand.Stains); + + var oh = _items.Identify(EquipSlot.OffHand, offhand.Skeleton, offhand.Weapon, offhand.Variant, mh.Type); + if (!skipWarnings && !oh.Valid) + { + Glamourer.Log.Warning($"Appearance data {offhand} for offhand weapon invalid, item could not be identified."); + oh = _items.GetDefaultOffhand(mh); + if (!oh.Valid) + oh = ItemManager.NothingItem(FullEquipType.Shield); + } + + yield return (EquipSlot.OffHand, oh, offhand.Stains); + } + + private static void ComputeMaterials(DesignMaterialManager manager, in StateMaterialManager materials, + EquipFlag equipFlags = EquipFlagExtensions.All, BonusItemFlag bonusFlags = BonusExtensions.All) + { + foreach (var (key, value) in materials.Values) + { + var idx = MaterialValueIndex.FromKey(key); + if (idx.RowIndex >= ColorTable.NumRows) + continue; + if (idx.MaterialIndex >= MaterialService.MaterialsPerModel) + continue; + + switch (idx.DrawObject) + { + case MaterialValueIndex.DrawObjectType.Mainhand when idx.SlotIndex == 0: + if ((equipFlags & (EquipFlag.Mainhand | EquipFlag.MainhandStain)) == 0) + continue; + + break; + case MaterialValueIndex.DrawObjectType.Offhand when idx.SlotIndex == 0: + if ((equipFlags & (EquipFlag.Offhand | EquipFlag.OffhandStain)) == 0) + continue; + + break; + case MaterialValueIndex.DrawObjectType.Human: + if (idx.SlotIndex < 10) + { + if ((((uint)idx.SlotIndex).ToEquipSlot().ToBothFlags() & equipFlags) == 0) + continue; + } + else if (idx.SlotIndex >= 16) + { + if (((idx.SlotIndex - 16u).ToBonusSlot() & bonusFlags) == 0) + continue; + } + + break; + default: continue; + } + + manager.AddOrUpdateValue(idx, value.Convert()); + } } } diff --git a/Glamourer/Designs/DesignData.cs b/Glamourer/Designs/DesignData.cs index 1364720..c7ca8e5 100644 --- a/Glamourer/Designs/DesignData.cs +++ b/Glamourer/Designs/DesignData.cs @@ -1,73 +1,159 @@ -using System; -using System.Buffers.Text; -using System.Runtime.CompilerServices; -using Glamourer.Customization; +using Glamourer.GameData; using Glamourer.Services; +using OtterGui.Classes; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.String.Functions; -using CustomizeData = Penumbra.GameData.Structs.CustomizeData; namespace Glamourer.Designs; public unsafe struct DesignData { - private string _nameHead = string.Empty; - private string _nameBody = string.Empty; - private string _nameHands = string.Empty; - private string _nameLegs = string.Empty; - private string _nameFeet = string.Empty; - private string _nameEars = string.Empty; - private string _nameNeck = string.Empty; - private string _nameWrists = string.Empty; - private string _nameRFinger = string.Empty; - private string _nameLFinger = string.Empty; - private string _nameMainhand = string.Empty; - private string _nameOffhand = string.Empty; - private fixed uint _itemIds[12]; - private fixed ushort _iconIds[12]; - private fixed byte _equipmentBytes[48]; - public Customize Customize = Customize.Default; - public uint ModelId; - private WeaponType _secondaryMainhand; - private WeaponType _secondaryOffhand; - private FullEquipType _typeMainhand; - private FullEquipType _typeOffhand; - private byte _states; - public bool IsHuman = true; + public const int NumEquipment = 10; + public const int EquipmentByteSize = NumEquipment * CharacterArmor.Size; + public const int NumBonusItems = 1; + public const int NumWeapons = 2; + + private string _nameHead = string.Empty; + private string _nameBody = string.Empty; + private string _nameHands = string.Empty; + private string _nameLegs = string.Empty; + private string _nameFeet = string.Empty; + private string _nameEars = string.Empty; + private string _nameNeck = string.Empty; + private string _nameWrists = string.Empty; + private string _nameRFinger = string.Empty; + private string _nameLFinger = string.Empty; + private string _nameMainhand = string.Empty; + private string _nameOffhand = string.Empty; + private string _nameGlasses = string.Empty; + + private fixed uint _itemIds[NumEquipment + NumWeapons]; + private fixed uint _iconIds[NumEquipment + NumWeapons + NumBonusItems]; + private fixed byte _equipmentBytes[EquipmentByteSize + NumWeapons * CharacterWeapon.Size]; + private fixed ushort _bonusIds[NumBonusItems]; + private fixed ushort _bonusModelIds[NumBonusItems]; + private fixed byte _bonusVariants[NumBonusItems]; + public CustomizeParameterData Parameters; + public CustomizeArray Customize = CustomizeArray.Default; + public uint ModelId; + public CrestFlag CrestVisibility; + private FullEquipType _typeMainhand; + private FullEquipType _typeOffhand; + private byte _states; + public bool IsHuman = true; public DesignData() { } - public readonly StainId Stain(EquipSlot slot) + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public readonly bool ContainsName(LowerString name) + => ItemNames.Any(name.IsContained); + + public readonly StainIds Stain(EquipSlot slot) { var index = slot.ToIndex(); - return index > 11 ? (StainId)0 : _equipmentBytes[4 * index + 3]; + return index switch + { + < 10 => new StainIds(_equipmentBytes[CharacterArmor.Size * index + 3], _equipmentBytes[CharacterArmor.Size * index + 4]), + 10 => new StainIds(_equipmentBytes[EquipmentByteSize + 6], _equipmentBytes[EquipmentByteSize + 7]), + 11 => new StainIds(_equipmentBytes[EquipmentByteSize + 14], _equipmentBytes[EquipmentByteSize + 15]), + _ => StainIds.None, + }; } - public FullEquipType MainhandType + public readonly bool Crest(CrestFlag slot) + => CrestVisibility.HasFlag(slot); + + public readonly IEnumerable ItemNames + { + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + get + { + yield return _nameHead; + yield return _nameBody; + yield return _nameHands; + yield return _nameLegs; + yield return _nameFeet; + yield return _nameEars; + yield return _nameNeck; + yield return _nameWrists; + yield return _nameRFinger; + yield return _nameLFinger; + yield return _nameMainhand; + yield return _nameOffhand; + yield return _nameGlasses; + } + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public readonly IEnumerable FilteredItemNames(EquipFlag item, BonusItemFlag bonusItem) + { + if (item.HasFlag(EquipFlag.Head)) + yield return _nameHead; + if (item.HasFlag(EquipFlag.Body)) + yield return _nameBody; + if (item.HasFlag(EquipFlag.Hands)) + yield return _nameHands; + if (item.HasFlag(EquipFlag.Legs)) + yield return _nameLegs; + if (item.HasFlag(EquipFlag.Feet)) + yield return _nameFeet; + if (item.HasFlag(EquipFlag.Ears)) + yield return _nameEars; + if (item.HasFlag(EquipFlag.Neck)) + yield return _nameNeck; + if (item.HasFlag(EquipFlag.Wrist)) + yield return _nameWrists; + if (item.HasFlag(EquipFlag.RFinger)) + yield return _nameRFinger; + if (item.HasFlag(EquipFlag.LFinger)) + yield return _nameLFinger; + if (item.HasFlag(EquipFlag.Mainhand)) + yield return _nameMainhand; + if (item.HasFlag(EquipFlag.Offhand)) + yield return _nameOffhand; + if (bonusItem.HasFlag(BonusItemFlag.Glasses)) + yield return _nameGlasses; + } + + public readonly FullEquipType MainhandType => _typeMainhand; - public FullEquipType OffhandType + public readonly FullEquipType OffhandType => _typeOffhand; public readonly EquipItem Item(EquipSlot slot) - => slot.ToIndex() switch + { + fixed (byte* ptr = _equipmentBytes) + { + return slot.ToIndex() switch + { + // @formatter:off + 0 => EquipItem.FromIds(_itemIds[ 0], _iconIds[ 0], ((CharacterArmor*)ptr)[0].Set, 0, ((CharacterArmor*)ptr)[0].Variant, FullEquipType.Head, name: _nameHead ), + 1 => EquipItem.FromIds(_itemIds[ 1], _iconIds[ 1], ((CharacterArmor*)ptr)[1].Set, 0, ((CharacterArmor*)ptr)[1].Variant, FullEquipType.Body, name: _nameBody ), + 2 => EquipItem.FromIds(_itemIds[ 2], _iconIds[ 2], ((CharacterArmor*)ptr)[2].Set, 0, ((CharacterArmor*)ptr)[2].Variant, FullEquipType.Hands, name: _nameHands ), + 3 => EquipItem.FromIds(_itemIds[ 3], _iconIds[ 3], ((CharacterArmor*)ptr)[3].Set, 0, ((CharacterArmor*)ptr)[3].Variant, FullEquipType.Legs, name: _nameLegs ), + 4 => EquipItem.FromIds(_itemIds[ 4], _iconIds[ 4], ((CharacterArmor*)ptr)[4].Set, 0, ((CharacterArmor*)ptr)[4].Variant, FullEquipType.Feet, name: _nameFeet ), + 5 => EquipItem.FromIds(_itemIds[ 5], _iconIds[ 5], ((CharacterArmor*)ptr)[5].Set, 0, ((CharacterArmor*)ptr)[5].Variant, FullEquipType.Ears, name: _nameEars ), + 6 => EquipItem.FromIds(_itemIds[ 6], _iconIds[ 6], ((CharacterArmor*)ptr)[6].Set, 0, ((CharacterArmor*)ptr)[6].Variant, FullEquipType.Neck, name: _nameNeck ), + 7 => EquipItem.FromIds(_itemIds[ 7], _iconIds[ 7], ((CharacterArmor*)ptr)[7].Set, 0, ((CharacterArmor*)ptr)[7].Variant, FullEquipType.Wrists, name: _nameWrists ), + 8 => EquipItem.FromIds(_itemIds[ 8], _iconIds[ 8], ((CharacterArmor*)ptr)[8].Set, 0, ((CharacterArmor*)ptr)[8].Variant, FullEquipType.Finger, name: _nameRFinger ), + 9 => EquipItem.FromIds(_itemIds[ 9], _iconIds[ 9], ((CharacterArmor*)ptr)[9].Set, 0, ((CharacterArmor*)ptr)[9].Variant, FullEquipType.Finger, name: _nameLFinger ), + 10 => EquipItem.FromIds(_itemIds[10], _iconIds[10], *(PrimaryId*)(ptr + EquipmentByteSize + 0), *(SecondaryId*)(ptr + EquipmentByteSize + 2), *(Variant*)(ptr + EquipmentByteSize + 4), _typeMainhand, name: _nameMainhand), + 11 => EquipItem.FromIds(_itemIds[11], _iconIds[11], *(PrimaryId*)(ptr + EquipmentByteSize + 8), *(SecondaryId*)(ptr + EquipmentByteSize + 10), *(Variant*)(ptr + EquipmentByteSize + 12), _typeOffhand, name: _nameOffhand ), + _ => new EquipItem(), + // @formatter:on + }; + } + } + + public readonly EquipItem BonusItem(BonusItemFlag slot) + => slot switch { // @formatter:off - 0 => EquipItem.FromIds(_itemIds[ 0], _iconIds[ 0], (SetId)(_equipmentBytes[ 0] | (_equipmentBytes[ 1] << 8)), (WeaponType)0, _equipmentBytes[ 2], FullEquipType.Head, name: _nameHead ), - 1 => EquipItem.FromIds(_itemIds[ 1], _iconIds[ 1], (SetId)(_equipmentBytes[ 4] | (_equipmentBytes[ 5] << 8)), (WeaponType)0, _equipmentBytes[ 6], FullEquipType.Body, name: _nameBody ), - 2 => EquipItem.FromIds(_itemIds[ 2], _iconIds[ 2], (SetId)(_equipmentBytes[ 8] | (_equipmentBytes[ 9] << 8)), (WeaponType)0, _equipmentBytes[10], FullEquipType.Hands, name: _nameHands ), - 3 => EquipItem.FromIds(_itemIds[ 3], _iconIds[ 3], (SetId)(_equipmentBytes[12] | (_equipmentBytes[13] << 8)), (WeaponType)0, _equipmentBytes[14], FullEquipType.Legs, name: _nameLegs ), - 4 => EquipItem.FromIds(_itemIds[ 4], _iconIds[ 4], (SetId)(_equipmentBytes[16] | (_equipmentBytes[17] << 8)), (WeaponType)0, _equipmentBytes[18], FullEquipType.Feet, name: _nameFeet ), - 5 => EquipItem.FromIds(_itemIds[ 5], _iconIds[ 5], (SetId)(_equipmentBytes[20] | (_equipmentBytes[21] << 8)), (WeaponType)0, _equipmentBytes[22], FullEquipType.Ears, name: _nameEars ), - 6 => EquipItem.FromIds(_itemIds[ 6], _iconIds[ 6], (SetId)(_equipmentBytes[24] | (_equipmentBytes[25] << 8)), (WeaponType)0, _equipmentBytes[26], FullEquipType.Neck, name: _nameNeck ), - 7 => EquipItem.FromIds(_itemIds[ 7], _iconIds[ 7], (SetId)(_equipmentBytes[28] | (_equipmentBytes[29] << 8)), (WeaponType)0, _equipmentBytes[30], FullEquipType.Wrists, name: _nameWrists ), - 8 => EquipItem.FromIds(_itemIds[ 8], _iconIds[ 8], (SetId)(_equipmentBytes[32] | (_equipmentBytes[33] << 8)), (WeaponType)0, _equipmentBytes[34], FullEquipType.Finger, name: _nameRFinger ), - 9 => EquipItem.FromIds(_itemIds[ 9], _iconIds[ 9], (SetId)(_equipmentBytes[36] | (_equipmentBytes[37] << 8)), (WeaponType)0, _equipmentBytes[38], FullEquipType.Finger, name: _nameLFinger ), - 10 => EquipItem.FromIds(_itemIds[10], _iconIds[10], (SetId)(_equipmentBytes[40] | (_equipmentBytes[41] << 8)), _secondaryMainhand, _equipmentBytes[42], _typeMainhand, name: _nameMainhand), - 11 => EquipItem.FromIds(_itemIds[11], _iconIds[11], (SetId)(_equipmentBytes[44] | (_equipmentBytes[45] << 8)), _secondaryOffhand, _equipmentBytes[46], _typeOffhand, name: _nameOffhand ), - _ => new EquipItem(), + BonusItemFlag.Glasses => EquipItem.FromBonusIds(_bonusIds[0], _iconIds[12], _bonusModelIds[0], _bonusVariants[0], BonusItemFlag.Glasses, _nameGlasses), + _ => EquipItem.BonusItemNothing(slot), // @formatter:on }; @@ -96,22 +182,22 @@ public unsafe struct DesignData { fixed (byte* ptr = _equipmentBytes) { - var armorPtr = (CharacterArmor*)ptr; - return slot is EquipSlot.MainHand ? armorPtr[10].ToWeapon(_secondaryMainhand) : armorPtr[11].ToWeapon(_secondaryOffhand); + var weaponPtr = (CharacterWeapon*)(ptr + EquipmentByteSize); + return weaponPtr[slot is EquipSlot.MainHand ? 0 : 1]; } } public bool SetItem(EquipSlot slot, EquipItem item) { var index = slot.ToIndex(); - if (index > 11) + if (index > NumEquipment + NumWeapons) return false; - _itemIds[index] = item.ItemId.Id; - _iconIds[index] = item.IconId.Id; - _equipmentBytes[4 * index + 0] = (byte)item.ModelId.Id; - _equipmentBytes[4 * index + 1] = (byte)(item.ModelId.Id >> 8); - _equipmentBytes[4 * index + 2] = item.Variant.Id; + _itemIds[index] = item.ItemId.Id; + _iconIds[index] = item.IconId.Id; + _equipmentBytes[CharacterArmor.Size * index + 0] = (byte)item.PrimaryId.Id; + _equipmentBytes[CharacterArmor.Size * index + 1] = (byte)(item.PrimaryId.Id >> 8); + _equipmentBytes[CharacterArmor.Size * index + 2] = item.Variant.Id; switch (index) { // @formatter:off @@ -127,36 +213,93 @@ public unsafe struct DesignData case 9: _nameLFinger = item.Name; return true; // @formatter:on case 10: - _nameMainhand = item.Name; - _secondaryMainhand = item.WeaponType; - _typeMainhand = item.Type; + _nameMainhand = item.Name; + _equipmentBytes[EquipmentByteSize + 2] = (byte)item.SecondaryId.Id; + _equipmentBytes[EquipmentByteSize + 3] = (byte)(item.SecondaryId.Id >> 8); + _equipmentBytes[EquipmentByteSize + 4] = item.Variant.Id; + _typeMainhand = item.Type; return true; case 11: - _nameOffhand = item.Name; - _secondaryOffhand = item.WeaponType; - _typeOffhand = item.Type; + _nameOffhand = item.Name; + _equipmentBytes[EquipmentByteSize + 10] = (byte)item.SecondaryId.Id; + _equipmentBytes[EquipmentByteSize + 11] = (byte)(item.SecondaryId.Id >> 8); + _equipmentBytes[EquipmentByteSize + 12] = item.Variant.Id; + _typeOffhand = item.Type; return true; } return true; } - public bool SetStain(EquipSlot slot, StainId stain) + public bool SetBonusItem(BonusItemFlag slot, EquipItem item) + { + var index = slot.ToIndex(); + if (index > NumBonusItems) + return false; + + _iconIds[NumEquipment + NumWeapons + index] = item.IconId.Id; + _bonusIds[index] = item.Id.BonusItem.Id; + _bonusModelIds[index] = item.PrimaryId.Id; + _bonusVariants[index] = item.Variant.Id; + switch (index) + { + case 0: + _nameGlasses = item.Name; + return true; + default: return false; + } + } + + public bool SetStain(EquipSlot slot, StainIds stains) => slot.ToIndex() switch { - 0 => SetIfDifferent(ref _equipmentBytes[3], stain.Id), - 1 => SetIfDifferent(ref _equipmentBytes[7], stain.Id), - 2 => SetIfDifferent(ref _equipmentBytes[11], stain.Id), - 3 => SetIfDifferent(ref _equipmentBytes[15], stain.Id), - 4 => SetIfDifferent(ref _equipmentBytes[19], stain.Id), - 5 => SetIfDifferent(ref _equipmentBytes[23], stain.Id), - 6 => SetIfDifferent(ref _equipmentBytes[27], stain.Id), - 7 => SetIfDifferent(ref _equipmentBytes[31], stain.Id), - 8 => SetIfDifferent(ref _equipmentBytes[35], stain.Id), - 9 => SetIfDifferent(ref _equipmentBytes[39], stain.Id), - 10 => SetIfDifferent(ref _equipmentBytes[43], stain.Id), - 11 => SetIfDifferent(ref _equipmentBytes[47], stain.Id), - _ => false, + // @formatter:off + 0 => SetIfDifferent(ref _equipmentBytes[0 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[0 * CharacterArmor.Size + 4], stains.Stain2.Id), + 1 => SetIfDifferent(ref _equipmentBytes[1 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[1 * CharacterArmor.Size + 4], stains.Stain2.Id), + 2 => SetIfDifferent(ref _equipmentBytes[2 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[2 * CharacterArmor.Size + 4], stains.Stain2.Id), + 3 => SetIfDifferent(ref _equipmentBytes[3 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[3 * CharacterArmor.Size + 4], stains.Stain2.Id), + 4 => SetIfDifferent(ref _equipmentBytes[4 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[4 * CharacterArmor.Size + 4], stains.Stain2.Id), + 5 => SetIfDifferent(ref _equipmentBytes[5 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[5 * CharacterArmor.Size + 4], stains.Stain2.Id), + 6 => SetIfDifferent(ref _equipmentBytes[6 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[6 * CharacterArmor.Size + 4], stains.Stain2.Id), + 7 => SetIfDifferent(ref _equipmentBytes[7 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[7 * CharacterArmor.Size + 4], stains.Stain2.Id), + 8 => SetIfDifferent(ref _equipmentBytes[8 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[8 * CharacterArmor.Size + 4], stains.Stain2.Id), + 9 => SetIfDifferent(ref _equipmentBytes[9 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[9 * CharacterArmor.Size + 4], stains.Stain2.Id), + 10 => SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 6], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 7], stains.Stain2.Id), + 11 => SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 14], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 15], stains.Stain2.Id), + _ => false, + // @formatter:on + }; + + public bool SetCrest(CrestFlag slot, bool visible) + { + var newValue = visible ? CrestVisibility | slot : CrestVisibility & ~slot; + if (newValue == CrestVisibility) + return false; + + CrestVisibility = newValue; + return true; + } + + public readonly bool GetMeta(MetaIndex index) + => index switch + { + MetaIndex.Wetness => IsWet(), + MetaIndex.HatState => IsHatVisible(), + MetaIndex.VisorState => IsVisorToggled(), + MetaIndex.WeaponState => IsWeaponVisible(), + MetaIndex.EarState => AreEarsVisible(), + _ => false, + }; + + public bool SetMeta(MetaIndex index, bool value) + => index switch + { + MetaIndex.Wetness => SetIsWet(value), + MetaIndex.HatState => SetHatVisible(value), + MetaIndex.VisorState => SetVisor(value), + MetaIndex.WeaponState => SetWeaponVisible(value), + MetaIndex.EarState => SetEarsVisible(value), + _ => false, }; public readonly bool IsWet() @@ -199,6 +342,9 @@ public unsafe struct DesignData public readonly bool IsWeaponVisible() => (_states & 0x08) == 0x08; + public readonly bool AreEarsVisible() + => (_states & 0x10) == 0x00; + public bool SetWeaponVisible(bool value) { if (value == IsWeaponVisible()) @@ -208,26 +354,45 @@ public unsafe struct DesignData return true; } + public bool SetEarsVisible(bool value) + { + if (value == AreEarsVisible()) + return false; + + _states = (byte)(value ? _states & ~0x10 : _states | 0x10); + return true; + } + public void SetDefaultEquipment(ItemManager items) { foreach (var slot in EquipSlotExtensions.EqdpSlots) { SetItem(slot, ItemManager.NothingItem(slot)); - SetStain(slot, 0); + SetStain(slot, StainIds.None); + SetCrest(slot.ToCrestFlag(), false); } SetItem(EquipSlot.MainHand, items.DefaultSword); - SetStain(EquipSlot.MainHand, 0); + SetStain(EquipSlot.MainHand, StainIds.None); + SetCrest(CrestFlag.MainHand, false); SetItem(EquipSlot.OffHand, ItemManager.NothingItem(FullEquipType.Shield)); - SetStain(EquipSlot.OffHand, 0); + SetStain(EquipSlot.OffHand, StainIds.None); + SetCrest(CrestFlag.OffHand, false); + SetDefaultBonusItems(); + } + + public void SetDefaultBonusItems() + { + foreach (var slot in BonusExtensions.AllFlags) + SetBonusItem(slot, EquipItem.BonusItemNothing(slot)); } - public bool LoadNonHuman(uint modelId, Customize customize, nint equipData) + public bool LoadNonHuman(uint modelId, CustomizeArray customize, nint equipData) { ModelId = modelId; IsHuman = false; - Customize.Load(customize); + Customize.Read(customize.Data); fixed (byte* ptr = _equipmentBytes) { MemoryUtility.MemCpyUnchecked(ptr, (byte*)equipData, 40); @@ -235,13 +400,14 @@ public unsafe struct DesignData SetHatVisible(true); SetWeaponVisible(true); + SetEarsVisible(true); SetVisor(false); fixed (uint* ptr = _itemIds) { MemoryUtility.MemSet(ptr, 0, 10 * 4); } - fixed (ushort* ptr = _iconIds) + fixed (uint* ptr = _iconIds) { MemoryUtility.MemSet(ptr, 0, 10 * 2); } @@ -256,13 +422,14 @@ public unsafe struct DesignData _nameWrists = string.Empty; _nameRFinger = string.Empty; _nameLFinger = string.Empty; + _nameGlasses = string.Empty; return true; } public readonly byte[] GetCustomizeBytes() { - var ret = new byte[CustomizeData.Size]; - fixed (byte* retPtr = ret, inPtr = Customize.Data.Data) + var ret = new byte[CustomizeArray.Size]; + fixed (byte* retPtr = ret, inPtr = Customize.Data) { MemoryUtility.MemCpyUnchecked(retPtr, inPtr, ret.Length); } @@ -272,7 +439,7 @@ public unsafe struct DesignData public readonly byte[] GetEquipmentBytes() { - var ret = new byte[40]; + var ret = new byte[80]; fixed (byte* retPtr = ret, inPtr = _equipmentBytes) { MemoryUtility.MemCpyUnchecked(retPtr, inPtr, ret.Length); @@ -293,8 +460,8 @@ public unsafe struct DesignData { fixed (byte* dataPtr = _equipmentBytes) { - var data = new Span(dataPtr, 40); - return Convert.TryFromBase64String(base64, data, out var written) && written == 40; + var data = new Span(dataPtr, 80); + return Convert.TryFromBase64String(base64, data, out var written) && written == 80; } } diff --git a/Glamourer/Designs/DesignEditor.cs b/Glamourer/Designs/DesignEditor.cs new file mode 100644 index 0000000..448e373 --- /dev/null +++ b/Glamourer/Designs/DesignEditor.cs @@ -0,0 +1,397 @@ +using Glamourer.Designs.History; +using Glamourer.Designs.Links; +using Glamourer.Events; +using Glamourer.GameData; +using Glamourer.Interop.Material; +using Glamourer.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs; + +public class DesignEditor( + SaveService saveService, + DesignChanged designChanged, + CustomizeService customizations, + ItemManager items, + Configuration config) + : IDesignEditor +{ + protected readonly DesignChanged DesignChanged = designChanged; + protected readonly SaveService SaveService = saveService; + protected readonly ItemManager Items = items; + protected readonly CustomizeService Customizations = customizations; + protected readonly Configuration Config = config; + protected readonly Dictionary UndoStore = []; + + private bool _forceFullItemOff; + + /// Whether an Undo for the given design is possible. + public bool CanUndo(Design? design) + => design != null && UndoStore.ContainsKey(design.Identifier); + + /// + public void ChangeCustomize(object data, CustomizeIndex idx, CustomizeValue value, ApplySettings _ = default) + { + var design = (Design)data; + var oldValue = design.DesignData.Customize[idx]; + switch (idx) + { + case CustomizeIndex.Race: + case CustomizeIndex.BodyType: + Glamourer.Log.Error("Somehow race or body type was changed in a design. This should not happen."); + return; + case CustomizeIndex.Clan: + { + var customize = design.DesignData.Customize; + if (Customizations.ChangeClan(ref customize, (SubRace)value.Value) == 0) + return; + if (!design.SetCustomize(Customizations, customize)) + return; + + break; + } + case CustomizeIndex.Gender: + { + var customize = design.DesignData.Customize; + if (Customizations.ChangeGender(ref customize, (Gender)(value.Value + 1)) == 0) + return; + if (!design.SetCustomize(Customizations, customize)) + return; + + break; + } + default: + if (!Customizations.IsCustomizationValid(design.DesignData.Customize.Clan, design.DesignData.Customize.Gender, + design.DesignData.Customize.Face, idx, value) + || !design.GetDesignDataRef().Customize.Set(idx, value)) + return; + + break; + } + + design.LastEdit = DateTimeOffset.UtcNow; + Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}."); + SaveService.QueueSave(design); + DesignChanged.Invoke(DesignChanged.Type.Customize, design, new CustomizeTransaction(idx, oldValue, value)); + } + + /// + public void ChangeEntireCustomize(object data, in CustomizeArray customize, CustomizeFlag apply, ApplySettings _ = default) + { + var design = (Design)data; + var (newCustomize, applied, changed) = Customizations.Combine(design.DesignData.Customize, customize, apply, true); + if (changed == 0) + return; + + var oldCustomize = design.DesignData.Customize; + design.SetCustomize(Customizations, newCustomize); + design.LastEdit = DateTimeOffset.UtcNow; + Glamourer.Log.Debug($"Changed entire customize with resulting flags {applied} and {changed}."); + SaveService.QueueSave(design); + DesignChanged.Invoke(DesignChanged.Type.EntireCustomize, design, new EntireCustomizeTransaction(changed, oldCustomize, newCustomize)); + } + + /// + public void ChangeCustomizeParameter(object data, CustomizeParameterFlag flag, CustomizeParameterValue value, ApplySettings _ = default) + { + var design = (Design)data; + var old = design.DesignData.Parameters[flag]; + if (!design.GetDesignDataRef().Parameters.Set(flag, value)) + return; + + var @new = design.DesignData.Parameters[flag]; + design.LastEdit = DateTimeOffset.UtcNow; + Glamourer.Log.Debug($"Set customize parameter {flag} in design {design.Identifier} from {old} to {@new}."); + SaveService.QueueSave(design); + DesignChanged.Invoke(DesignChanged.Type.Parameter, design, new ParameterTransaction(flag, old, @new)); + } + + /// + public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings _ = default) + { + var design = (Design)data; + switch (slot) + { + case EquipSlot.MainHand: + { + var currentMain = design.DesignData.Item(EquipSlot.MainHand); + var currentOff = design.DesignData.Item(EquipSlot.OffHand); + if (!Items.IsItemValid(EquipSlot.MainHand, item.ItemId, out item)) + return; + + if (!ChangeMainhandPeriphery(design, currentMain, currentOff, item, out var newOff, out var newGauntlets)) + return; + + var currentGauntlets = design.DesignData.Item(EquipSlot.Hands); + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + Glamourer.Log.Debug( + $"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.ItemId}) to {item.Name} ({item.ItemId})."); + DesignChanged.Invoke(DesignChanged.Type.Weapon, design, + new WeaponTransaction(currentMain, currentOff, currentGauntlets, item, newOff ?? currentOff, + newGauntlets ?? currentGauntlets)); + return; + } + case EquipSlot.OffHand: + { + var currentMain = design.DesignData.Item(EquipSlot.MainHand); + var currentOff = design.DesignData.Item(EquipSlot.OffHand); + if (!Items.IsOffhandValid(currentOff.Type, item.ItemId, out item)) + return; + + if (!design.GetDesignDataRef().SetItem(EquipSlot.OffHand, item)) + return; + + var currentGauntlets = design.DesignData.Item(EquipSlot.Hands); + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + Glamourer.Log.Debug( + $"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.ItemId}) to {item.Name} ({item.ItemId})."); + DesignChanged.Invoke(DesignChanged.Type.Weapon, design, + new WeaponTransaction(currentMain, currentOff, currentGauntlets, currentMain, item, currentGauntlets)); + return; + } + default: + { + if (!Items.IsItemValid(slot, item.Id, out item)) + return; + + var old = design.DesignData.Item(slot); + if (!design.GetDesignDataRef().SetItem(slot, item)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + Glamourer.Log.Debug( + $"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId})."); + SaveService.QueueSave(design); + DesignChanged.Invoke(DesignChanged.Type.Equip, design, new EquipTransaction(slot, old, item)); + return; + } + } + } + + /// + public void ChangeBonusItem(object data, BonusItemFlag slot, EquipItem item, ApplySettings settings = default) + { + var design = (Design)data; + if (item.Type.ToBonus() != slot) + return; + + var oldItem = design.DesignData.BonusItem(slot); + if (!design.GetDesignDataRef().SetBonusItem(slot, item)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set {slot} bonus item to {item}."); + DesignChanged.Invoke(DesignChanged.Type.BonusItem, design, new BonusItemTransaction(slot, oldItem, item)); + } + + /// + public void ChangeStains(object data, EquipSlot slot, StainIds stains, ApplySettings _ = default) + { + var design = (Design)data; + if (Items.ValidateStain(stains, out var _, false).Length > 0) + return; + + var oldStain = design.DesignData.Stain(slot); + if (!design.GetDesignDataRef().SetStain(slot, stains)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stains}."); + DesignChanged.Invoke(DesignChanged.Type.Stains, design, new StainTransaction(slot, oldStain, stains)); + } + + /// + public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainIds? stains, ApplySettings _ = default) + { + if (item.HasValue) + ChangeItem(data, slot, item.Value, _); + if (stains.HasValue) + ChangeStains(data, slot, stains.Value, _); + } + + /// + public void ChangeCrest(object data, CrestFlag slot, bool crest, ApplySettings _ = default) + { + var design = (Design)data; + var oldCrest = design.DesignData.Crest(slot); + if (!design.GetDesignDataRef().SetCrest(slot, crest)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set crest visibility of {slot} equipment piece to {crest}."); + DesignChanged.Invoke(DesignChanged.Type.Crest, design, new CrestTransaction(slot, oldCrest, crest)); + } + + /// + public void ChangeMetaState(object data, MetaIndex metaIndex, bool value, ApplySettings _ = default) + { + var design = (Design)data; + if (!design.GetDesignDataRef().SetMeta(metaIndex, value)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set value of {metaIndex} to {value}."); + DesignChanged.Invoke(DesignChanged.Type.Other, design, new MetaTransaction(metaIndex, !value, value)); + } + + public void ChangeMaterialRevert(Design design, MaterialValueIndex index, bool revert) + { + var materials = design.GetMaterialDataRef(); + if (!materials.TryGetValue(index, out var oldValue)) + return; + + materials.AddOrUpdateValue(index, oldValue with { Revert = revert }); + Glamourer.Log.Debug($"Changed advanced dye value for {index} to {(revert ? "Revert." : "no longer Revert.")}"); + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + DesignChanged.Invoke(DesignChanged.Type.MaterialRevert, design, new MaterialRevertTransaction(index, !revert, revert)); + } + + public void ChangeMaterialValue(Design design, MaterialValueIndex index, ColorRow? row) + { + var materials = design.GetMaterialDataRef(); + if (materials.TryGetValue(index, out var oldValue)) + { + if (!row.HasValue) + { + materials.RemoveValue(index); + Glamourer.Log.Debug($"Removed advanced dye value for {index}."); + } + else if (!row.Value.NearEqual(oldValue.Value)) + { + materials.UpdateValue(index, new MaterialValueDesign(row.Value, oldValue.Enabled, oldValue.Revert), out _); + Glamourer.Log.Debug($"Updated advanced dye value for {index} to new value."); + } + else + { + return; + } + } + else + { + if (!row.HasValue) + return; + if (!materials.TryAddValue(index, new MaterialValueDesign(row.Value, true, false))) + return; + + Glamourer.Log.Debug($"Added new advanced dye value for {index}."); + } + + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.DelaySave(design); + DesignChanged.Invoke(DesignChanged.Type.Material, design, new MaterialTransaction(index, oldValue.Value, row)); + } + + public void ChangeApplyMaterialValue(Design design, MaterialValueIndex index, bool value) + { + var materials = design.GetMaterialDataRef(); + if (!materials.TryGetValue(index, out var oldValue) || oldValue.Enabled == value) + return; + + materials.AddOrUpdateValue(index, oldValue with { Enabled = value }); + Glamourer.Log.Debug($"Changed application of advanced dye for {index} to {value}."); + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + DesignChanged.Invoke(DesignChanged.Type.ApplyMaterial, design, new ApplicationTransaction(index, !value, value)); + } + + + /// + public void ApplyDesign(object data, MergedDesign other, ApplySettings settings = default) + => ApplyDesign(data, other.Design, settings); + + /// + public void ApplyDesign(object data, DesignBase other, ApplySettings _ = default) + { + var design = (Design)data; + UndoStore[design.Identifier] = design.DesignData; + foreach (var index in MetaExtensions.AllRelevant.Where(other.DoApplyMeta)) + design.GetDesignDataRef().SetMeta(index, other.DesignData.GetMeta(index)); + + if (!design.DesignData.IsHuman) + return; + + ChangeEntireCustomize(design, other.DesignData.Customize, other.ApplyCustomize); + + _forceFullItemOff = true; + foreach (var slot in EquipSlotExtensions.FullSlots) + { + ChangeEquip(design, slot, + other.DoApplyEquip(slot) ? other.DesignData.Item(slot) : null, + other.DoApplyStain(slot) ? other.DesignData.Stain(slot) : null); + } + + _forceFullItemOff = false; + + foreach (var slot in BonusExtensions.AllFlags) + { + if (other.DoApplyBonusItem(slot)) + ChangeBonusItem(design, slot, other.DesignData.BonusItem(slot)); + } + + foreach (var slot in Enum.GetValues().Where(other.DoApplyCrest)) + ChangeCrest(design, slot, other.DesignData.Crest(slot)); + + foreach (var parameter in CustomizeParameterExtensions.AllFlags.Where(other.DoApplyParameter)) + ChangeCustomizeParameter(design, parameter, other.DesignData.Parameters[parameter]); + + foreach (var (key, value) in other.Materials) + { + if (!value.Enabled) + continue; + + design.GetMaterialDataRef().AddOrUpdateValue(MaterialValueIndex.FromKey(key), value); + } + } + + /// Change a mainhand weapon and either fix or apply appropriate offhand and potentially gauntlets. + private bool ChangeMainhandPeriphery(DesignBase design, EquipItem currentMain, EquipItem currentOff, EquipItem newMain, + out EquipItem? newOff, + out EquipItem? newGauntlets) + { + newOff = null; + newGauntlets = null; + if (newMain.Type != currentMain.Type) + { + var defaultOffhand = Items.GetDefaultOffhand(newMain); + if (!Items.IsOffhandValid(newMain, defaultOffhand.ItemId, out var o)) + return false; + + newOff = o; + } + else if (!_forceFullItemOff && Config.ChangeEntireItem && newMain.Type is not FullEquipType.Sword) // Skip applying shields. + { + var defaultOffhand = Items.GetDefaultOffhand(newMain); + if (Items.IsOffhandValid(newMain, defaultOffhand.ItemId, out var o)) + newOff = o; + + if (newMain.Type is FullEquipType.Fists && Items.ItemData.Tertiary.TryGetValue(newMain.ItemId, out var g)) + newGauntlets = g; + } + + if (!design.GetDesignDataRef().SetItem(EquipSlot.MainHand, newMain)) + return false; + + if (newOff.HasValue && !design.GetDesignDataRef().SetItem(EquipSlot.OffHand, newOff.Value)) + { + design.GetDesignDataRef().SetItem(EquipSlot.MainHand, currentMain); + return false; + } + + if (newGauntlets.HasValue && !design.GetDesignDataRef().SetItem(EquipSlot.Hands, newGauntlets.Value)) + { + design.GetDesignDataRef().SetItem(EquipSlot.MainHand, currentMain); + design.GetDesignDataRef().SetItem(EquipSlot.OffHand, currentOff); + return false; + } + + return true; + } +} diff --git a/Glamourer/Designs/DesignFileSystem.cs b/Glamourer/Designs/DesignFileSystem.cs index 960c460..fd47793 100644 --- a/Glamourer/Designs/DesignFileSystem.cs +++ b/Glamourer/Designs/DesignFileSystem.cs @@ -1,12 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; +using Glamourer.Designs.History; using Glamourer.Events; -using Glamourer.Interop.Penumbra; using Glamourer.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -47,11 +41,11 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable public struct CreationDate : ISortMode { - public string Name - => "Creation Date (Older First)"; + public ReadOnlySpan Name + => "Creation Date (Older First)"u8; - public string Description - => "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date."; + public ReadOnlySpan Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date."u8; public IEnumerable GetChildren(Folder f) => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderBy(l => l.Value.CreationDate)); @@ -59,11 +53,11 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable public struct UpdateDate : ISortMode { - public string Name - => "Update Date (Older First)"; + public ReadOnlySpan Name + => "Update Date (Older First)"u8; - public string Description - => "In each folder, sort all subfolders lexicographically, then sort all leaves using their last update date."; + public ReadOnlySpan Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their last update date."u8; public IEnumerable GetChildren(Folder f) => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderBy(l => l.Value.LastEdit)); @@ -71,11 +65,11 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable public struct InverseCreationDate : ISortMode { - public string Name - => "Creation Date (Newer First)"; + public ReadOnlySpan Name + => "Creation Date (Newer First)"u8; - public string Description - => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date."; + public ReadOnlySpan Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date."u8; public IEnumerable GetChildren(Folder f) => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderByDescending(l => l.Value.CreationDate)); @@ -83,11 +77,11 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable public struct InverseUpdateDate : ISortMode { - public string Name - => "Update Date (Newer First)"; + public ReadOnlySpan Name + => "Update Date (Newer First)"u8; - public string Description - => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse last update date."; + public ReadOnlySpan Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse last update date."u8; public IEnumerable GetChildren(Folder f) => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderByDescending(l => l.Value.LastEdit)); @@ -99,34 +93,35 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable _saveService.QueueSave(this); } - private void OnDesignChange(DesignChanged.Type type, Design design, object? data) + private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? data) { switch (type) { case DesignChanged.Type.Created: var parent = Root; - if (data is string path) + if ((data as CreationTransaction?)?.Path is { } path) try { parent = FindOrCreateAllFolders(path); } catch (Exception ex) { - Glamourer.Messager.NotificationMessage(ex, $"Could not move design to {path} because the folder could not be created.", NotificationType.Error); + Glamourer.Messager.NotificationMessage(ex, $"Could not move design to {path} because the folder could not be created.", + NotificationType.Error); } CreateDuplicateLeaf(parent, design.Name.Text, design); return; case DesignChanged.Type.Deleted: - if (FindLeaf(design, out var leaf1)) + if (TryGetValue(design, out var leaf1)) Delete(leaf1); return; case DesignChanged.Type.ReloadedAll: Reload(); return; - case DesignChanged.Type.Renamed when data is string oldName: - if (!FindLeaf(design, out var leaf2)) + case DesignChanged.Type.Renamed when (data as RenameTransaction?)?.Old is { } oldName: + if (!TryGetValue(design, out var leaf2)) return; var old = oldName.FixName(); @@ -155,15 +150,6 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable ? (string.Empty, false) : (DesignToIdentifier(design), true); - // Search the entire filesystem for the leaf corresponding to a design. - public bool FindLeaf(Design design, [NotNullWhen(true)] out Leaf? leaf) - { - leaf = Root.GetAllDescendants(ISortMode.Lexicographical) - .OfType() - .FirstOrDefault(l => l.Value == design); - return leaf != null; - } - internal static void MigrateOldPaths(SaveService saveService, Dictionary oldPaths) { if (oldPaths.Count == 0) diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index 8628c85..92f8398 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -1,76 +1,84 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Utility; -using Glamourer.Customization; +using Dalamud.Utility; +using Glamourer.Designs.History; +using Glamourer.Designs.Links; using Glamourer.Events; +using Glamourer.GameData; +using Glamourer.Interop.Material; using Glamourer.Interop.Penumbra; using Glamourer.Services; -using Glamourer.State; +using OtterGui.Extensions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; + namespace Glamourer.Designs; -public class DesignManager +public sealed class DesignManager : DesignEditor { - private readonly CustomizationService _customizations; - private readonly ItemManager _items; - private readonly HumanModelList _humans; - private readonly SaveService _saveService; - private readonly DesignChanged _event; - private readonly List _designs = new(); + public readonly DesignStorage Designs; + private readonly HumanModelList _humans; - public IReadOnlyList Designs - => _designs; - - public DesignManager(SaveService saveService, ItemManager items, CustomizationService customizations, - DesignChanged @event, HumanModelList humans) + public DesignManager(SaveService saveService, ItemManager items, CustomizeService customizations, + DesignChanged @event, HumanModelList humans, DesignStorage storage, DesignLinkLoader designLinkLoader, Configuration config) + : base(saveService, @event, customizations, items, config) { - _saveService = saveService; - _items = items; - _customizations = customizations; - _event = @event; - _humans = humans; + Designs = storage; + _humans = humans; + + LoadDesigns(designLinkLoader); CreateDesignFolder(saveService); - LoadDesigns(); MigrateOldDesigns(); + designLinkLoader.SetAllObjects(); } + #region Design Management + /// /// Clear currently loaded designs and load all designs anew from file. /// Invalid data is fixed, but changes are not saved until manual changes. /// - public void LoadDesigns() + private void LoadDesigns(DesignLinkLoader linkLoader) { - _designs.Clear(); - List<(Design, string)> invalidNames = new(); - var skipped = 0; - foreach (var file in _saveService.FileNames.Designs()) + _humans.Awaiter.Wait(); + Customizations.Awaiter.Wait(); + Items.ItemData.Awaiter.Wait(); + + var stopwatch = Stopwatch.StartNew(); + Designs.Clear(); + var skipped = 0; + ThreadLocal> designs = new(() => [], true); + Parallel.ForEach(SaveService.FileNames.Designs(), (f, _) => { try { - var text = File.ReadAllText(file.FullName); + var text = File.ReadAllText(f.FullName); var data = JObject.Parse(text); - var design = Design.LoadDesign(_customizations, _items, data); - if (design.Identifier.ToString() != Path.GetFileNameWithoutExtension(file.Name)) - invalidNames.Add((design, file.FullName)); - if (_designs.Any(f => f.Identifier == design.Identifier)) - throw new Exception($"Identifier {design.Identifier} was not unique."); - - design.Index = _designs.Count; - _designs.Add(design); + var design = Design.LoadDesign(SaveService, Customizations, Items, linkLoader, data); + designs.Value!.Add((design, f.FullName)); } catch (Exception ex) { Glamourer.Log.Error($"Could not load design, skipped:\n{ex}"); - ++skipped; + Interlocked.Increment(ref skipped); } + }); + + List<(Design, string)> invalidNames = []; + foreach (var (design, path) in designs.Values.SelectMany(v => v)) + { + if (design.Identifier.ToString() != Path.GetFileNameWithoutExtension(path)) + invalidNames.Add((design, path)); + if (Designs.Contains(design.Identifier)) + { + Glamourer.Log.Error($"Could not load design, skipped: Identifier {design.Identifier} was not unique."); + ++skipped; + continue; + } + + design.Index = Designs.Count; + Designs.Add(design); } var failed = MoveInvalidNames(invalidNames); @@ -79,30 +87,35 @@ public class DesignManager $"Moved {invalidNames.Count - failed} designs to correct names.{(failed > 0 ? $" Failed to move {failed} designs to correct names." : string.Empty)}"); Glamourer.Log.Information( - $"Loaded {_designs.Count} designs.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}"); - _event.Invoke(DesignChanged.Type.ReloadedAll, null!); + $"Loaded {Designs.Count} designs in {stopwatch.ElapsedMilliseconds} ms.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}"); + DesignChanged.Invoke(DesignChanged.Type.ReloadedAll, null!, null); } /// Create a new temporary design without adding it to the manager. public DesignBase CreateTemporary() - => new(_items); + => new(Customizations, Items); /// Create a new design of a given name. public Design CreateEmpty(string name, bool handlePath) { var (actualName, path) = ParseName(name, handlePath); - var design = new Design(_customizations, _items) + var design = new Design(Customizations, Items) { - CreationDate = DateTimeOffset.UtcNow, - LastEdit = DateTimeOffset.UtcNow, - Identifier = CreateNewGuid(), - Name = actualName, - Index = _designs.Count, + CreationDate = DateTimeOffset.UtcNow, + LastEdit = DateTimeOffset.UtcNow, + Identifier = CreateNewGuid(), + Name = actualName, + Index = Designs.Count, + ForcedRedraw = Config.DefaultDesignSettings.AlwaysForceRedrawing, + ResetAdvancedDyes = Config.DefaultDesignSettings.ResetAdvancedDyes, + QuickDesign = Config.DefaultDesignSettings.ShowQuickDesignBar, + ResetTemporarySettings = Config.DefaultDesignSettings.ResetTemporarySettings, }; - _designs.Add(design); + design.SetWriteProtected(Config.DefaultDesignSettings.Locked); + Designs.Add(design); Glamourer.Log.Debug($"Added new design {design.Identifier}."); - _saveService.ImmediateSave(design); - _event.Invoke(DesignChanged.Type.Created, design, path); + SaveService.ImmediateSave(design); + DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path)); return design; } @@ -112,17 +125,22 @@ public class DesignManager var (actualName, path) = ParseName(name, handlePath); var design = new Design(clone) { - CreationDate = DateTimeOffset.UtcNow, - LastEdit = DateTimeOffset.UtcNow, - Identifier = CreateNewGuid(), - Name = actualName, - Index = _designs.Count, + CreationDate = DateTimeOffset.UtcNow, + LastEdit = DateTimeOffset.UtcNow, + Identifier = CreateNewGuid(), + Name = actualName, + Index = Designs.Count, + ForcedRedraw = Config.DefaultDesignSettings.AlwaysForceRedrawing, + ResetAdvancedDyes = Config.DefaultDesignSettings.ResetAdvancedDyes, + QuickDesign = Config.DefaultDesignSettings.ShowQuickDesignBar, + ResetTemporarySettings = Config.DefaultDesignSettings.ResetTemporarySettings, }; - _designs.Add(design); + design.SetWriteProtected(Config.DefaultDesignSettings.Locked); + Designs.Add(design); Glamourer.Log.Debug($"Added new design {design.Identifier} by cloning Temporary Design."); - _saveService.ImmediateSave(design); - _event.Invoke(DesignChanged.Type.Created, design, path); + SaveService.ImmediateSave(design); + DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path)); return design; } @@ -136,26 +154,31 @@ public class DesignManager LastEdit = DateTimeOffset.UtcNow, Identifier = CreateNewGuid(), Name = actualName, - Index = _designs.Count, + Index = Designs.Count, }; - _designs.Add(design); + design.SetWriteProtected(Config.DefaultDesignSettings.Locked); + Designs.Add(design); Glamourer.Log.Debug( $"Added new design {design.Identifier} by cloning {clone.Identifier.ToString()}."); - _saveService.ImmediateSave(design); - _event.Invoke(DesignChanged.Type.Created, design, path); + SaveService.ImmediateSave(design); + DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path)); return design; } /// Delete a design. public void Delete(Design design) { - foreach (var d in _designs.Skip(design.Index + 1)) + foreach (var d in Designs.Skip(design.Index + 1)) --d.Index; - _designs.RemoveAt(design.Index); - _saveService.ImmediateDelete(design); - _event.Invoke(DesignChanged.Type.Deleted, design); + Designs.RemoveAt(design.Index); + SaveService.ImmediateDelete(design); + DesignChanged.Invoke(DesignChanged.Type.Deleted, design, null); } + #endregion + + #region Edit Information + /// Rename a design. public void Rename(Design design, string newName) { @@ -165,9 +188,9 @@ public class DesignManager design.Name = newName; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Renamed design {design.Identifier}."); - _event.Invoke(DesignChanged.Type.Renamed, design, oldName); + DesignChanged.Invoke(DesignChanged.Type.Renamed, design, new RenameTransaction(oldName, newName)); } /// Change the description of a design. @@ -179,9 +202,23 @@ public class DesignManager design.Description = description; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Changed description of design {design.Identifier}."); - _event.Invoke(DesignChanged.Type.ChangedDescription, design, oldDescription); + DesignChanged.Invoke(DesignChanged.Type.ChangedDescription, design, new DescriptionTransaction(oldDescription, description)); + } + + /// Change the associated color of a design. + public void ChangeColor(Design design, string newColor) + { + var oldColor = design.Color; + if (oldColor == newColor) + return; + + design.Color = newColor; + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Changed color of design {design.Identifier}."); + DesignChanged.Invoke(DesignChanged.Type.ChangedColor, design, new DesignColorTransaction(oldColor, newColor)); } /// Add a new tag to a design. The tags remain sorted. @@ -192,16 +229,12 @@ public class DesignManager design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray(); design.LastEdit = DateTimeOffset.UtcNow; - var idx = design.Tags.IndexOf(tag); - _saveService.QueueSave(design); + var idx = design.Tags.AsEnumerable().IndexOf(tag); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}."); - _event.Invoke(DesignChanged.Type.AddedTag, design, (tag, idx)); + DesignChanged.Invoke(DesignChanged.Type.AddedTag, design, new TagAddedTransaction(tag, idx)); } - /// Remove a tag from a design if it exists. - public void RemoveTag(Design design, string tag) - => RemoveTag(design, design.Tags.IndexOf(tag)); - /// Remove a tag from a design by its index. public void RemoveTag(Design design, int tagIdx) { @@ -211,9 +244,9 @@ public class DesignManager var oldTag = design.Tags[tagIdx]; design.Tags = design.Tags.Take(tagIdx).Concat(design.Tags.Skip(tagIdx + 1)).ToArray(); design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}."); - _event.Invoke(DesignChanged.Type.RemovedTag, design, (oldTag, tagIdx)); + DesignChanged.Invoke(DesignChanged.Type.RemovedTag, design, new TagRemovedTransaction(oldTag, tagIdx)); } /// Rename a tag from a design by its index. The tags stay sorted. @@ -226,9 +259,10 @@ public class DesignManager design.Tags[tagIdx] = newTag; Array.Sort(design.Tags); design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags."); - _event.Invoke(DesignChanged.Type.ChangedTag, design, (oldTag, newTag, tagIdx)); + DesignChanged.Invoke(DesignChanged.Type.ChangedTag, design, + new TagChangedTransaction(oldTag, newTag, tagIdx, design.Tags.AsEnumerable().IndexOf(newTag))); } /// Add an associated mod to a design. @@ -238,9 +272,9 @@ public class DesignManager return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} to design {design.Identifier}."); - _event.Invoke(DesignChanged.Type.AddedMod, design, (mod, settings)); + DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, new ModAddedTransaction(mod, settings)); } /// Remove an associated mod from a design. @@ -250,9 +284,28 @@ public class DesignManager return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Removed associated mod {mod.DirectoryName} from design {design.Identifier}."); - _event.Invoke(DesignChanged.Type.RemovedMod, design, (mod, settings)); + DesignChanged.Invoke(DesignChanged.Type.RemovedMod, design, new ModRemovedTransaction(mod, settings)); + } + + /// Add or update an associated mod to a design. + public void UpdateMod(Design design, Mod mod, ModSettings settings) + { + var hasOldSettings = design.AssociatedMods.TryGetValue(mod, out var oldSettings); + design.AssociatedMods[mod] = settings; + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + if (hasOldSettings) + { + Glamourer.Log.Debug($"Updated associated mod {mod.DirectoryName} from design {design.Identifier}."); + DesignChanged.Invoke(DesignChanged.Type.UpdatedMod, design, new ModUpdatedTransaction(mod, oldSettings, settings)); + } + else + { + Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} from design {design.Identifier}."); + DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, new ModAddedTransaction(mod, settings)); + } } /// Set the write protection status of a design. @@ -261,260 +314,202 @@ public class DesignManager if (!design.SetWriteProtected(value)) return; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Set design {design.Identifier} to {(value ? "no longer be " : string.Empty)} write-protected."); - _event.Invoke(DesignChanged.Type.WriteProtection, design, value); + DesignChanged.Invoke(DesignChanged.Type.WriteProtection, design, null); } - /// Change a customization value. - public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value) + /// Set the quick design bar display status of a design. + public void SetQuickDesign(Design design, bool value) { - var oldValue = design.DesignData.Customize[idx]; - switch (idx) - { - case CustomizeIndex.Race: - case CustomizeIndex.BodyType: - Glamourer.Log.Error("Somehow race or body type was changed in a design. This should not happen."); - return; - case CustomizeIndex.Clan: - if (_customizations.ChangeClan(ref design.DesignData.Customize, (SubRace)value.Value) == 0) - return; + if (value == design.QuickDesign) + return; - design.RemoveInvalidCustomize(_customizations); - break; - case CustomizeIndex.Gender: - if (_customizations.ChangeGender(ref design.DesignData.Customize, (Gender)(value.Value + 1)) == 0) - return; + design.QuickDesign = value; + SaveService.QueueSave(design); + Glamourer.Log.Debug( + $"Set design {design.Identifier} to {(!value ? "no longer be " : string.Empty)} displayed in the quick design bar."); + DesignChanged.Invoke(DesignChanged.Type.QuickDesignBar, design, null); + } - design.RemoveInvalidCustomize(_customizations); - break; - default: - if (!_customizations.IsCustomizationValid(design.DesignData.Customize.Clan, design.DesignData.Customize.Gender, - design.DesignData.Customize.Face, idx, value) - || !design.DesignData.Customize.Set(idx, value)) - return; + #endregion - break; - } + #region Edit Application Rules - design.LastEdit = DateTimeOffset.UtcNow; - Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}."); - _saveService.QueueSave(design); - _event.Invoke(DesignChanged.Type.Customize, design, (oldValue, value, idx)); + public void ChangeForcedRedraw(Design design, bool forcedRedraw) + { + if (design.ForcedRedraw == forcedRedraw) + return; + + design.ForcedRedraw = forcedRedraw; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set {design.Identifier} to {(forcedRedraw ? string.Empty : "not")} force redraws."); + DesignChanged.Invoke(DesignChanged.Type.ForceRedraw, design, null); + } + + public void ChangeResetAdvancedDyes(Design design, bool resetAdvancedDyes) + { + if (design.ResetAdvancedDyes == resetAdvancedDyes) + return; + + design.ResetAdvancedDyes = resetAdvancedDyes; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set {design.Identifier} to {(resetAdvancedDyes ? string.Empty : "not")} reset advanced dyes."); + DesignChanged.Invoke(DesignChanged.Type.ResetAdvancedDyes, design, null); + } + + public void ChangeResetTemporarySettings(Design design, bool resetTemporarySettings) + { + if (design.ResetTemporarySettings == resetTemporarySettings) + return; + + design.ResetTemporarySettings = resetTemporarySettings; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set {design.Identifier} to {(resetTemporarySettings ? string.Empty : "not")} reset temporary settings."); + DesignChanged.Invoke(DesignChanged.Type.ResetTemporarySettings, design, null); } /// Change whether to apply a specific customize value. public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value) { - var set = _customizations.AwaitedService.GetList(design.DesignData.Customize.Clan, design.DesignData.Customize.Gender); - value &= set.IsAvailable(idx) || idx is CustomizeIndex.Clan or CustomizeIndex.Gender; if (!design.SetApplyCustomize(idx, value)) return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of customization {idx.ToDefaultName()} to {value}."); - _event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx); - } - - /// Change a non-weapon equipment piece. - public void ChangeEquip(Design design, EquipSlot slot, EquipItem item) - { - if (!_items.IsItemValid(slot, item.ItemId, out item)) - return; - - var old = design.DesignData.Item(slot); - if (!design.DesignData.SetItem(slot, item)) - return; - - design.LastEdit = DateTimeOffset.UtcNow; - Glamourer.Log.Debug( - $"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId})."); - _saveService.QueueSave(design); - _event.Invoke(DesignChanged.Type.Equip, design, (old, item, slot)); - } - - /// Change a weapon. - public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item) - { - var currentMain = design.DesignData.Item(EquipSlot.MainHand); - var currentOff = design.DesignData.Item(EquipSlot.OffHand); - switch (slot) - { - case EquipSlot.MainHand: - var newOff = currentOff; - if (!_items.IsItemValid(EquipSlot.MainHand, item.ItemId, out item)) - return; - - if (item.Type != currentMain.Type) - { - var defaultOffhand = _items.GetDefaultOffhand(item); - if (!_items.IsOffhandValid(item, defaultOffhand.ItemId, out newOff)) - return; - } - - if (!(design.DesignData.SetItem(EquipSlot.MainHand, item) | design.DesignData.SetItem(EquipSlot.OffHand, newOff))) - return; - - design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); - Glamourer.Log.Debug( - $"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.ItemId}) to {item.Name} ({item.ItemId})."); - _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff)); - - return; - case EquipSlot.OffHand: - if (!_items.IsOffhandValid(currentOff.Type, item.ItemId, out item)) - return; - - if (!design.DesignData.SetItem(EquipSlot.OffHand, item)) - return; - - design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); - Glamourer.Log.Debug( - $"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.ItemId}) to {item.Name} ({item.ItemId})."); - _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, currentMain, item)); - return; - default: return; - } + DesignChanged.Invoke(DesignChanged.Type.ApplyCustomize, design, new ApplicationTransaction(idx, !value, value)); } /// Change whether to apply a specific equipment piece. - public void ChangeApplyEquip(Design design, EquipSlot slot, bool value) + public void ChangeApplyItem(Design design, EquipSlot slot, bool value) { if (!design.SetApplyEquip(slot, value)) return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}."); - _event.Invoke(DesignChanged.Type.ApplyEquip, design, slot); + DesignChanged.Invoke(DesignChanged.Type.ApplyEquip, design, new ApplicationTransaction((slot, false), !value, value)); } - /// Change the stain for any equipment piece. - public void ChangeStain(Design design, EquipSlot slot, StainId stain) + /// Change whether to apply a specific equipment piece. + public void ChangeApplyBonusItem(Design design, BonusItemFlag slot, bool value) { - if (_items.ValidateStain(stain, out _, false).Length > 0) - return; - - var oldStain = design.DesignData.Stain(slot); - if (!design.DesignData.SetStain(slot, stain)) + if (!design.SetApplyBonusItem(slot, value)) return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); - Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Id}."); - _event.Invoke(DesignChanged.Type.Stain, design, (oldStain, stain, slot)); + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set applying of {slot} bonus item to {value}."); + DesignChanged.Invoke(DesignChanged.Type.ApplyBonusItem, design, new ApplicationTransaction(slot, !value, value)); } /// Change whether to apply a specific stain. - public void ChangeApplyStain(Design design, EquipSlot slot, bool value) + public void ChangeApplyStains(Design design, EquipSlot slot, bool value) { if (!design.SetApplyStain(slot, value)) return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}."); - _event.Invoke(DesignChanged.Type.ApplyStain, design, slot); + DesignChanged.Invoke(DesignChanged.Type.ApplyStain, design, new ApplicationTransaction((slot, true), !value, value)); } - /// Change the bool value of one of the meta flags. - public void ChangeMeta(Design design, ActorState.MetaIndex metaIndex, bool value) + /// Change whether to apply a specific crest visibility. + public void ChangeApplyCrest(Design design, CrestFlag slot, bool value) { - var change = metaIndex switch - { - ActorState.MetaIndex.Wetness => design.DesignData.SetIsWet(value), - ActorState.MetaIndex.HatState => design.DesignData.SetHatVisible(value), - ActorState.MetaIndex.VisorState => design.DesignData.SetVisor(value), - ActorState.MetaIndex.WeaponState => design.DesignData.SetWeaponVisible(value), - _ => throw new ArgumentOutOfRangeException(nameof(metaIndex), metaIndex, null), - }; - if (!change) + if (!design.SetApplyCrest(slot, value)) return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); - Glamourer.Log.Debug($"Set value of {metaIndex} to {value}."); - _event.Invoke(DesignChanged.Type.Other, design, (metaIndex, false, value)); + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set applying of crest visibility of {slot} equipment piece to {value}."); + DesignChanged.Invoke(DesignChanged.Type.ApplyCrest, design, new ApplicationTransaction(slot, !value, value)); } /// Change the application value of one of the meta flags. - public void ChangeApplyMeta(Design design, ActorState.MetaIndex metaIndex, bool value) + public void ChangeApplyMeta(Design design, MetaIndex metaIndex, bool value) { - var change = metaIndex switch - { - ActorState.MetaIndex.Wetness => design.SetApplyWetness(value), - ActorState.MetaIndex.HatState => design.SetApplyHatVisible(value), - ActorState.MetaIndex.VisorState => design.SetApplyVisorToggle(value), - ActorState.MetaIndex.WeaponState => design.SetApplyWeaponVisible(value), - _ => throw new ArgumentOutOfRangeException(nameof(metaIndex), metaIndex, null), - }; - if (!change) + if (!design.SetApplyMeta(metaIndex, value)) return; design.LastEdit = DateTimeOffset.UtcNow; - _saveService.QueueSave(design); + SaveService.QueueSave(design); Glamourer.Log.Debug($"Set applying of {metaIndex} to {value}."); - _event.Invoke(DesignChanged.Type.Other, design, (metaIndex, true, value)); + DesignChanged.Invoke(DesignChanged.Type.Other, design, new ApplicationTransaction(metaIndex, !value, value)); } - /// Apply an entire design based on its appliance rules piece by piece. - public void ApplyDesign(Design design, DesignBase other) + /// Change the application value of a customize parameter. + public void ChangeApplyParameter(Design design, CustomizeParameterFlag flag, bool value) { - if (other.DoApplyWetness()) - design.DesignData.SetIsWet(other.DesignData.IsWet()); - if (other.DoApplyHatVisible()) - design.DesignData.SetHatVisible(other.DesignData.IsHatVisible()); - if (other.DoApplyVisorToggle()) - design.DesignData.SetVisor(other.DesignData.IsVisorToggled()); - if (other.DoApplyWeaponVisible()) - design.DesignData.SetWeaponVisible(other.DesignData.IsWeaponVisible()); + if (!design.SetApplyParameter(flag, value)) + return; - if (design.DesignData.IsHuman) - { - foreach (var index in Enum.GetValues()) - { - if (other.DoApplyCustomize(index)) - ChangeCustomize(design, index, other.DesignData.Customize[index]); - } + design.LastEdit = DateTimeOffset.UtcNow; + SaveService.QueueSave(design); + Glamourer.Log.Debug($"Set applying of parameter {flag} to {value}."); + DesignChanged.Invoke(DesignChanged.Type.ApplyParameter, design, new ApplicationTransaction(flag, !value, value)); + } - foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - if (other.DoApplyEquip(slot)) - ChangeEquip(design, slot, other.DesignData.Item(slot)); + /// Change multiple application values at once. + public void ChangeApplyMulti(Design design, bool? equipment, bool? customization, bool? bonus, bool? parameters, bool? meta, bool? stains, + bool? materials, bool? crest) + { + if (equipment is { } e) + foreach (var f in EquipSlotExtensions.FullSlots) + ChangeApplyItem(design, f, e); + if (stains is { } s) + foreach (var f in EquipSlotExtensions.FullSlots) + ChangeApplyStains(design, f, s); + if (customization is { } c) + foreach (var f in CustomizationExtensions.All.Where(design.CustomizeSet.IsAvailable).Prepend(CustomizeIndex.Clan) + .Prepend(CustomizeIndex.Gender)) + ChangeApplyCustomize(design, f, c); + if (bonus is { } b) + foreach (var f in BonusExtensions.AllFlags) + ChangeApplyBonusItem(design, f, b); + if (meta is { } m) + foreach (var f in MetaExtensions.AllRelevant) + ChangeApplyMeta(design, f, m); + if (crest is { } cr) + foreach (var f in CrestExtensions.AllRelevantSet) + ChangeApplyCrest(design, f, cr); - if (other.DoApplyStain(slot)) - ChangeStain(design, slot, other.DesignData.Stain(slot)); - } - } + if (parameters is { } p) + foreach (var f in CustomizeParameterExtensions.AllFlags) + ChangeApplyParameter(design, f, p); - if (other.DoApplyEquip(EquipSlot.MainHand)) - ChangeWeapon(design, EquipSlot.MainHand, other.DesignData.Item(EquipSlot.MainHand)); + if (materials is { } ma) + foreach (var (key, _) in design.GetMaterialData().ToArray()) + ChangeApplyMaterialValue(design, MaterialValueIndex.FromKey(key), ma); + } - if (other.DoApplyEquip(EquipSlot.OffHand)) - ChangeWeapon(design, EquipSlot.OffHand, other.DesignData.Item(EquipSlot.OffHand)); + #endregion - if (other.DoApplyStain(EquipSlot.MainHand)) - ChangeStain(design, EquipSlot.MainHand, other.DesignData.Stain(EquipSlot.MainHand)); + public void UndoDesignChange(Design design) + { + if (!UndoStore.Remove(design.Identifier, out var otherData)) + return; - if (other.DoApplyStain(EquipSlot.OffHand)) - ChangeStain(design, EquipSlot.OffHand, other.DesignData.Stain(EquipSlot.OffHand)); + var other = CreateTemporary(); + other.SetDesignData(Customizations, otherData); + ApplyDesign(design, other); } private void MigrateOldDesigns() { - if (!File.Exists(_saveService.FileNames.MigrationDesignFile)) + if (!File.Exists(SaveService.FileNames.MigrationDesignFile)) return; var errors = 0; var skips = 0; var successes = 0; - var oldDesigns = _designs.ToList(); + var oldDesigns = Designs.ToList(); try { - var text = File.ReadAllText(_saveService.FileNames.MigrationDesignFile); + var text = File.ReadAllText(SaveService.FileNames.MigrationDesignFile); var dict = JsonConvert.DeserializeObject>(text) ?? new Dictionary(); var migratedFileSystemPaths = new Dictionary(dict.Count); foreach (var (name, base64) in dict) @@ -522,14 +517,14 @@ public class DesignManager try { var actualName = Path.GetFileName(name); - var design = new Design(_customizations, _items) + var design = new Design(Customizations, Items) { - CreationDate = File.GetCreationTimeUtc(_saveService.FileNames.MigrationDesignFile), - LastEdit = File.GetLastWriteTimeUtc(_saveService.FileNames.MigrationDesignFile), + CreationDate = File.GetCreationTimeUtc(SaveService.FileNames.MigrationDesignFile), + LastEdit = File.GetLastWriteTimeUtc(SaveService.FileNames.MigrationDesignFile), Identifier = CreateNewGuid(), Name = actualName, }; - design.MigrateBase64(_items, _humans, base64); + design.MigrateBase64(Customizations, Items, _humans, base64); if (!oldDesigns.Any(d => d.Name == design.Name && d.CreationDate == design.CreationDate)) { Add(design, $"Migrated old design to {design.Identifier}."); @@ -550,24 +545,24 @@ public class DesignManager } } - DesignFileSystem.MigrateOldPaths(_saveService, migratedFileSystemPaths); + DesignFileSystem.MigrateOldPaths(SaveService, migratedFileSystemPaths); Glamourer.Log.Information( $"Successfully migrated {successes} old designs. Skipped {skips} already migrated designs. Failed to migrate {errors} designs."); } catch (Exception e) { - Glamourer.Log.Error($"Could not migrate old design file {_saveService.FileNames.MigrationDesignFile}:\n{e}"); + Glamourer.Log.Error($"Could not migrate old design file {SaveService.FileNames.MigrationDesignFile}:\n{e}"); } try { - File.Move(_saveService.FileNames.MigrationDesignFile, - Path.ChangeExtension(_saveService.FileNames.MigrationDesignFile, ".json.bak")); - Glamourer.Log.Information($"Moved migrated design file {_saveService.FileNames.MigrationDesignFile} to backup file."); + File.Move(SaveService.FileNames.MigrationDesignFile, + Path.ChangeExtension(SaveService.FileNames.MigrationDesignFile, ".json.bak"), true); + Glamourer.Log.Information($"Moved migrated design file {SaveService.FileNames.MigrationDesignFile} to backup file."); } catch (Exception ex) { - Glamourer.Log.Error($"Could not move migrated design file {_saveService.FileNames.MigrationDesignFile} to backup file:\n{ex}"); + Glamourer.Log.Error($"Could not move migrated design file {SaveService.FileNames.MigrationDesignFile} to backup file:\n{ex}"); } } @@ -597,7 +592,7 @@ public class DesignManager { try { - var correctName = _saveService.FileNames.DesignFile(design); + var correctName = SaveService.FileNames.DesignFile(design); File.Move(name, correctName, false); Glamourer.Log.Information($"Moved invalid design file from {Path.GetFileName(name)} to {Path.GetFileName(correctName)}."); } @@ -617,7 +612,7 @@ public class DesignManager while (true) { var guid = Guid.NewGuid(); - if (_designs.All(d => d.Identifier != guid)) + if (!Designs.Contains(guid)) return guid; } } @@ -627,18 +622,17 @@ public class DesignManager /// Returns false if the design is already contained or if the identifier is already in use. /// The design is treated as newly created and invokes an event. /// - private bool Add(Design design, string? message) + private void Add(Design design, string? message) { - if (_designs.Any(d => d == design || d.Identifier == design.Identifier)) - return false; + if (Designs.Any(d => d == design || d.Identifier == design.Identifier)) + return; - design.Index = _designs.Count; - _designs.Add(design); + design.Index = Designs.Count; + Designs.Add(design); if (!message.IsNullOrEmpty()) Glamourer.Log.Debug(message); - _saveService.ImmediateSave(design); - _event.Invoke(DesignChanged.Type.Created, design); - return true; + SaveService.ImmediateSave(design); + DesignChanged.Invoke(DesignChanged.Type.Created, design, null); } /// Split a given string into its folder path and its name, if is true. diff --git a/Glamourer/Designs/DesignStorage.cs b/Glamourer/Designs/DesignStorage.cs new file mode 100644 index 0000000..a87415c --- /dev/null +++ b/Glamourer/Designs/DesignStorage.cs @@ -0,0 +1,18 @@ +using OtterGui.Services; + +namespace Glamourer.Designs; + +public class DesignStorage : List, IService +{ + public bool TryGetValue(Guid identifier, [NotNullWhen(true)] out Design? design) + { + design = ByIdentifier(identifier); + return design != null; + } + + public Design? ByIdentifier(Guid identifier) + => this.FirstOrDefault(d => d.Identifier == identifier); + + public bool Contains(Guid identifier) + => ByIdentifier(identifier) != null; +} diff --git a/Glamourer/Designs/History/DesignTransaction.cs b/Glamourer/Designs/History/DesignTransaction.cs new file mode 100644 index 0000000..65086db --- /dev/null +++ b/Glamourer/Designs/History/DesignTransaction.cs @@ -0,0 +1,185 @@ +using Glamourer.GameData; +using Glamourer.Interop.Material; +using Glamourer.Interop.Penumbra; +using Glamourer.State; +using Penumbra.GameData.Enums; + +namespace Glamourer.Designs.History; + +/// Only Designs. Can not be reverted. +public readonly record struct CreationTransaction(string Name, string? Path) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + { } +} + +/// Only Designs. +public readonly record struct RenameTransaction(string Old, string New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is RenameTransaction other ? new RenameTransaction(other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).Rename((Design)data, Old); +} + +/// Only Designs. +public readonly record struct DescriptionTransaction(string Old, string New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is DescriptionTransaction other ? new DescriptionTransaction(other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).ChangeDescription((Design)data, Old); +} + +/// Only Designs. +public readonly record struct DesignColorTransaction(string Old, string New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is DesignColorTransaction other ? new DesignColorTransaction(other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).ChangeColor((Design)data, Old); +} + +/// Only Designs. +public readonly record struct TagAddedTransaction(string New, int Index) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).RemoveTag((Design)data, Index); +} + +/// Only Designs. +public readonly record struct TagRemovedTransaction(string Old, int Index) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).AddTag((Design)data, Old); +} + +/// Only Designs. +public readonly record struct TagChangedTransaction(string Old, string New, int IndexOld, int IndexNew) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is TagChangedTransaction other && other.IndexNew == IndexOld + ? new TagChangedTransaction(other.Old, New, other.IndexOld, IndexNew) + : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).RenameTag((Design)data, IndexNew, Old); +} + +/// Only Designs. +public readonly record struct ModAddedTransaction(Mod Mod, ModSettings Settings) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).RemoveMod((Design)data, Mod); +} + +/// Only Designs. +public readonly record struct ModRemovedTransaction(Mod Mod, ModSettings Settings) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).AddMod((Design)data, Mod, Settings); +} + +/// Only Designs. +public readonly record struct ModUpdatedTransaction(Mod Mod, ModSettings Old, ModSettings New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is ModUpdatedTransaction other && Mod == other.Mod ? new ModUpdatedTransaction(Mod, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).UpdateMod((Design)data, Mod, Old); +} + +/// Only Designs. +public readonly record struct MaterialTransaction(MaterialValueIndex Index, ColorRow? Old, ColorRow? New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is MaterialTransaction other && Index == other.Index ? new MaterialTransaction(Index, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + { + if (editor is DesignManager e) + e.ChangeMaterialValue((Design)data, Index, Old); + } +} + +/// Only Designs. +public readonly record struct MaterialRevertTransaction(MaterialValueIndex Index, bool Old, bool New) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + => ((DesignManager)editor).ChangeMaterialRevert((Design)data, Index, Old); +} + +/// Only Designs. +public readonly record struct ApplicationTransaction(object Index, bool Old, bool New) + : ITransaction +{ + public ITransaction? Merge(ITransaction other) + => null; + + public void Revert(IDesignEditor editor, object data) + { + var manager = (DesignManager)editor; + var design = (Design)data; + switch (Index) + { + case CustomizeIndex idx: + manager.ChangeApplyCustomize(design, idx, Old); + break; + case (EquipSlot slot, true): + manager.ChangeApplyStains(design, slot, Old); + break; + case (EquipSlot slot, _): + manager.ChangeApplyItem(design, slot, Old); + break; + case BonusItemFlag slot: + manager.ChangeApplyBonusItem(design, slot, Old); + break; + case CrestFlag slot: + manager.ChangeApplyCrest(design, slot, Old); + break; + case MetaIndex slot: + manager.ChangeApplyMeta(design, slot, Old); + break; + case CustomizeParameterFlag slot: + manager.ChangeApplyParameter(design, slot, Old); + break; + case MaterialValueIndex slot: + manager.ChangeApplyMaterialValue(design, slot, Old); + break; + } + } +} diff --git a/Glamourer/Designs/History/EditorHistory.cs b/Glamourer/Designs/History/EditorHistory.cs new file mode 100644 index 0000000..caec151 --- /dev/null +++ b/Glamourer/Designs/History/EditorHistory.cs @@ -0,0 +1,191 @@ +using Glamourer.Api.Enums; +using Glamourer.Events; +using Glamourer.State; +using OtterGui.Services; +using Penumbra.GameData.Interop; + +namespace Glamourer.Designs.History; + +public class EditorHistory : IDisposable, IService +{ + public const int MaxUndo = 16; + + private sealed class Queue : IReadOnlyList + { + private DateTime _lastAdd = DateTime.UtcNow; + + private readonly ITransaction[] _data = new ITransaction[MaxUndo]; + public int Offset { get; private set; } + public int Count { get; private set; } + + public void Add(ITransaction transaction) + { + if (!TryMerge(transaction)) + { + if (Count == MaxUndo) + { + _data[Offset] = transaction; + Offset = (Offset + 1) % MaxUndo; + } + else + { + if (Offset > 0) + { + _data[(Count + Offset) % MaxUndo] = transaction; + ++Count; + } + else + { + _data[Count] = transaction; + ++Count; + } + } + } + + _lastAdd = DateTime.UtcNow; + } + + private bool TryMerge(ITransaction newTransaction) + { + if (Count == 0) + return false; + + var time = DateTime.UtcNow; + if (time - _lastAdd > TimeSpan.FromMilliseconds(250)) + return false; + + var lastIdx = (Offset + Count - 1) % MaxUndo; + if (newTransaction.Merge(_data[lastIdx]) is not { } transaction) + return false; + + _data[lastIdx] = transaction; + return true; + } + + public ITransaction? RemoveLast() + { + if (Count == 0) + return null; + + --Count; + var idx = (Offset + Count) % MaxUndo; + return _data[idx]; + } + + public IEnumerator GetEnumerator() + { + var end = Offset + (Offset + Count) % MaxUndo; + for (var i = Offset; i < end; ++i) + yield return _data[i]; + + end = Count - end; + for (var i = 0; i < end; ++i) + yield return _data[i]; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public ITransaction this[int index] + => index < 0 || index >= Count + ? throw new IndexOutOfRangeException() + : _data[(Offset + index) % MaxUndo]; + } + + private readonly DesignEditor _designEditor; + private readonly StateEditor _stateEditor; + private readonly DesignChanged _designChanged; + private readonly StateChanged _stateChanged; + + private readonly Dictionary _stateEntries = []; + private readonly Dictionary _designEntries = []; + + private bool _undoMode; + + public EditorHistory(DesignManager designEditor, StateManager stateEditor, DesignChanged designChanged, StateChanged stateChanged) + { + _designEditor = designEditor; + _stateEditor = stateEditor; + _designChanged = designChanged; + _stateChanged = stateChanged; + + _designChanged.Subscribe(OnDesignChanged, DesignChanged.Priority.EditorHistory); + _stateChanged.Subscribe(OnStateChanged, StateChanged.Priority.EditorHistory); + } + + public void Dispose() + { + _designChanged.Unsubscribe(OnDesignChanged); + _stateChanged.Unsubscribe(OnStateChanged); + } + + public bool CanUndo(ActorState state) + => _stateEntries.TryGetValue(state, out var list) && list.Count > 0; + + public bool CanUndo(Design design) + => _designEntries.TryGetValue(design, out var list) && list.Count > 0; + + public bool Undo(ActorState state) + { + if (!_stateEntries.TryGetValue(state, out var list) || list.Count == 0) + return false; + + _undoMode = true; + list.RemoveLast()!.Revert(_stateEditor, state); + _undoMode = false; + return true; + } + + public bool Undo(Design design) + { + if (!_designEntries.TryGetValue(design, out var list) || list.Count == 0) + return false; + + _undoMode = true; + list.RemoveLast()!.Revert(_designEditor, design); + _undoMode = false; + return true; + } + + + private void AddStateTransaction(ActorState state, ITransaction transaction) + { + if (!_stateEntries.TryGetValue(state, out var list)) + { + list = []; + _stateEntries.Add(state, list); + } + + list.Add(transaction); + } + + private void AddDesignTransaction(Design design, ITransaction transaction) + { + if (!_designEntries.TryGetValue(design, out var list)) + { + list = []; + _designEntries.Add(design, list); + } + + list.Add(transaction); + } + + + private void OnStateChanged(StateChangeType type, StateSource source, ActorState state, ActorData actors, ITransaction? data) + { + if (_undoMode || source is not StateSource.Manual) + return; + + if (data is not null) + AddStateTransaction(state, data); + } + + private void OnDesignChanged(DesignChanged.Type type, Design design, ITransaction? data) + { + if (_undoMode) + return; + + if (data is not null) + AddDesignTransaction(design, data); + } +} diff --git a/Glamourer/Designs/History/Transaction.cs b/Glamourer/Designs/History/Transaction.cs new file mode 100644 index 0000000..47b10bf --- /dev/null +++ b/Glamourer/Designs/History/Transaction.cs @@ -0,0 +1,113 @@ +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Glamourer.GameData; + +namespace Glamourer.Designs.History; + +public interface ITransaction +{ + public ITransaction? Merge(ITransaction other); + public void Revert(IDesignEditor editor, object data); +} + +public readonly record struct CustomizeTransaction(CustomizeIndex Slot, CustomizeValue Old, CustomizeValue New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is CustomizeTransaction other && Slot == other.Slot ? new CustomizeTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeCustomize(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct EntireCustomizeTransaction(CustomizeFlag Apply, CustomizeArray Old, CustomizeArray New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is EntireCustomizeTransaction other ? new EntireCustomizeTransaction(Apply | other.Apply, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeEntireCustomize(data, Old, Apply, ApplySettings.Manual); +} + +public readonly record struct EquipTransaction(EquipSlot Slot, EquipItem Old, EquipItem New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is EquipTransaction other && Slot == other.Slot ? new EquipTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeItem(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct BonusItemTransaction(BonusItemFlag Slot, EquipItem Old, EquipItem New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is BonusItemTransaction other && Slot == other.Slot ? new BonusItemTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeBonusItem(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct WeaponTransaction( + EquipItem OldMain, + EquipItem OldOff, + EquipItem OldGauntlets, + EquipItem NewMain, + EquipItem NewOff, + EquipItem NewGauntlets) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is WeaponTransaction other + ? new WeaponTransaction(other.OldMain, other.OldOff, other.OldGauntlets, NewMain, NewOff, NewGauntlets) + : null; + + public void Revert(IDesignEditor editor, object data) + { + editor.ChangeItem(data, EquipSlot.MainHand, OldMain, ApplySettings.Manual); + editor.ChangeItem(data, EquipSlot.OffHand, OldOff, ApplySettings.Manual); + editor.ChangeItem(data, EquipSlot.Hands, OldGauntlets, ApplySettings.Manual); + } +} + +public readonly record struct StainTransaction(EquipSlot Slot, StainIds Old, StainIds New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is StainTransaction other && Slot == other.Slot ? new StainTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeStains(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct CrestTransaction(CrestFlag Slot, bool Old, bool New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is CrestTransaction other && Slot == other.Slot ? new CrestTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeCrest(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct ParameterTransaction(CustomizeParameterFlag Slot, CustomizeParameterValue Old, CustomizeParameterValue New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => older is ParameterTransaction other && Slot == other.Slot ? new ParameterTransaction(Slot, other.Old, New) : null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeCustomizeParameter(data, Slot, Old, ApplySettings.Manual); +} + +public readonly record struct MetaTransaction(MetaIndex Slot, bool Old, bool New) + : ITransaction +{ + public ITransaction? Merge(ITransaction older) + => null; + + public void Revert(IDesignEditor editor, object data) + => editor.ChangeMetaState(data, Slot, Old, ApplySettings.Manual); +} diff --git a/Glamourer/Designs/IDesignEditor.cs b/Glamourer/Designs/IDesignEditor.cs new file mode 100644 index 0000000..c18c98b --- /dev/null +++ b/Glamourer/Designs/IDesignEditor.cs @@ -0,0 +1,92 @@ +using Glamourer.Designs.Links; +using Glamourer.GameData; +using Glamourer.State; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs; + +public readonly record struct ApplySettings( + uint Key = 0, + StateSource Source = StateSource.Manual, + bool RespectManual = false, + bool FromJobChange = false, + bool UseSingleSource = false, + bool MergeLinks = false, + bool ResetMaterials = false, + bool IsFinal = false) +{ + public static readonly ApplySettings Manual = new() + { + Key = 0, + Source = StateSource.Manual, + FromJobChange = false, + RespectManual = false, + UseSingleSource = false, + MergeLinks = false, + ResetMaterials = false, + IsFinal = false, + }; + + public static readonly ApplySettings ManualWithLinks = new() + { + Key = 0, + Source = StateSource.Manual, + FromJobChange = false, + RespectManual = false, + UseSingleSource = false, + MergeLinks = true, + ResetMaterials = false, + IsFinal = false, + }; + + public static readonly ApplySettings Game = new() + { + Key = 0, + Source = StateSource.Game, + FromJobChange = false, + RespectManual = false, + UseSingleSource = false, + MergeLinks = false, + ResetMaterials = true, + IsFinal = false, + }; +} + +public interface IDesignEditor +{ + /// Change a customization value. + public void ChangeCustomize(object data, CustomizeIndex idx, CustomizeValue value, ApplySettings settings = default); + + /// Change an entire customize array according to the given flags. + public void ChangeEntireCustomize(object data, in CustomizeArray customizeInput, CustomizeFlag apply, ApplySettings settings = default); + + /// Change a customize parameter. + public void ChangeCustomizeParameter(object data, CustomizeParameterFlag flag, CustomizeParameterValue v, ApplySettings settings = default); + + /// Change an equipment piece. + public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings settings = default) + => ChangeEquip(data, slot, item, null, settings); + + /// Change a bonus item. + public void ChangeBonusItem(object data, BonusItemFlag slot, EquipItem item, ApplySettings settings = default); + + /// Change the stain for any equipment piece. + public void ChangeStains(object data, EquipSlot slot, StainIds stains, ApplySettings settings = default) + => ChangeEquip(data, slot, null, stains, settings); + + /// Change an equipment piece and its stain at the same time. + public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainIds? stains, ApplySettings settings = default); + + /// Change the crest visibility for any equipment piece. + public void ChangeCrest(object data, CrestFlag slot, bool crest, ApplySettings settings = default); + + /// Change the bool value of one of the meta flags. + public void ChangeMetaState(object data, MetaIndex slot, bool value, ApplySettings settings = default); + + /// Change all values applies from the given design. + public void ApplyDesign(object data, MergedDesign design, ApplySettings settings = default); + + /// Change all values applies from the given design. + public void ApplyDesign(object data, DesignBase design, ApplySettings settings = default); +} diff --git a/Glamourer/Designs/IDesignStandIn.cs b/Glamourer/Designs/IDesignStandIn.cs new file mode 100644 index 0000000..d07acb9 --- /dev/null +++ b/Glamourer/Designs/IDesignStandIn.cs @@ -0,0 +1,31 @@ +using Glamourer.Automation; +using Glamourer.Interop.Material; +using Glamourer.State; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs; + +public interface IDesignStandIn : IEquatable +{ + public string ResolveName(bool incognito); + public ref readonly DesignData GetDesignData(in DesignData baseRef); + + public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData(); + + public string SerializeName(); + public StateSource AssociatedSource(); + + public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication); + + public void AddData(JObject jObj); + + public void ParseData(JObject jObj); + + public bool ChangeData(object data); + + public bool ForcedRedraw { get; } + + public bool ResetAdvancedDyes { get; } + public bool ResetTemporarySettings { get; } +} diff --git a/Glamourer/Designs/Links/DesignLink.cs b/Glamourer/Designs/Links/DesignLink.cs new file mode 100644 index 0000000..a9fb805 --- /dev/null +++ b/Glamourer/Designs/Links/DesignLink.cs @@ -0,0 +1,19 @@ +using Glamourer.Automation; + +namespace Glamourer.Designs.Links; + +public record struct DesignLink(Design Link, ApplicationType Type); + +public readonly record struct LinkData(Guid Identity, ApplicationType Type, LinkOrder Order) +{ + public override string ToString() + => Identity.ToString(); +} + +public enum LinkOrder : byte +{ + Self, + After, + Before, + None, +}; diff --git a/Glamourer/Designs/Links/DesignLinkLoader.cs b/Glamourer/Designs/Links/DesignLinkLoader.cs new file mode 100644 index 0000000..24138a8 --- /dev/null +++ b/Glamourer/Designs/Links/DesignLinkLoader.cs @@ -0,0 +1,28 @@ +using Dalamud.Interface.ImGuiNotification; +using OtterGui.Classes; +using OtterGui.Extensions; +using OtterGui.Services; +using Notification = OtterGui.Classes.Notification; + +namespace Glamourer.Designs.Links; + +public sealed class DesignLinkLoader(DesignStorage designStorage, MessageService messager) + : DelayedReferenceLoader(messager), IService +{ + protected override bool TryGetObject(LinkData data, [NotNullWhen(true)] out Design? obj) + => designStorage.FindFirst(d => d.Identifier == data.Identity, out obj); + + protected override bool SetObject(Design parent, Design child, LinkData data, out string error) + => LinkContainer.AddLink(parent, child, data.Type, data.Order, out error); + + protected override void HandleChildNotFound(Design parent, LinkData data) + { + Messager.AddMessage(new Notification( + $"Could not find the design {data.Identity}. If this design was deleted, please re-save {parent.Identifier}.", + NotificationType.Warning)); + } + + protected override void HandleChildNotSet(Design parent, Design child, string error) + => Messager.AddMessage(new Notification($"Could not link {child.Identifier} to {parent.Identifier}: {error}", + NotificationType.Warning)); +} diff --git a/Glamourer/Designs/Links/DesignLinkManager.cs b/Glamourer/Designs/Links/DesignLinkManager.cs new file mode 100644 index 0000000..df1f147 --- /dev/null +++ b/Glamourer/Designs/Links/DesignLinkManager.cs @@ -0,0 +1,86 @@ +using Glamourer.Automation; +using Glamourer.Designs.History; +using Glamourer.Events; +using Glamourer.Services; +using OtterGui.Services; + +namespace Glamourer.Designs.Links; + +public sealed class DesignLinkManager : IService, IDisposable +{ + private readonly DesignStorage _storage; + private readonly DesignChanged _event; + private readonly SaveService _saveService; + + public DesignLinkManager(DesignStorage storage, DesignChanged @event, SaveService saveService) + { + _storage = storage; + _event = @event; + _saveService = saveService; + + _event.Subscribe(OnDesignChanged, DesignChanged.Priority.DesignLinkManager); + } + + public void Dispose() + => _event.Unsubscribe(OnDesignChanged); + + public void MoveDesignLink(Design parent, int idxFrom, LinkOrder orderFrom, int idxTo, LinkOrder orderTo) + { + if (!parent.Links.Reorder(idxFrom, orderFrom, idxTo, orderTo)) + return; + + parent.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(parent); + Glamourer.Log.Debug($"Moved link from {orderFrom} {idxFrom} to {idxTo} {orderTo}."); + _event.Invoke(DesignChanged.Type.ChangedLink, parent, null); + } + + public void AddDesignLink(Design parent, Design child, LinkOrder order) + { + if (!LinkContainer.AddLink(parent, child, ApplicationType.All, order, out _)) + return; + + parent.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(parent); + Glamourer.Log.Debug($"Added new {order} link to {child.Identifier} for {parent.Identifier}."); + _event.Invoke(DesignChanged.Type.ChangedLink, parent, null); + } + + public void RemoveDesignLink(Design parent, int idx, LinkOrder order) + { + if (!parent.Links.Remove(idx, order)) + return; + + parent.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(parent); + Glamourer.Log.Debug($"Removed the {order} link at {idx} for {parent.Identifier}."); + _event.Invoke(DesignChanged.Type.ChangedLink, parent, null); + } + + public void ChangeApplicationType(Design parent, int idx, LinkOrder order, ApplicationType applicationType) + { + applicationType &= ApplicationType.All; + if (!parent.Links.ChangeApplicationRules(idx, order, applicationType, out var old)) + return; + + _saveService.QueueSave(parent); + Glamourer.Log.Debug($"Changed link application type from {old} to {applicationType} for design link {order} {idx + 1} in design {parent.Identifier}."); + _event.Invoke(DesignChanged.Type.ChangedLink, parent, null); + } + + private void OnDesignChanged(DesignChanged.Type type, Design deletedDesign, ITransaction? _) + { + if (type is not DesignChanged.Type.Deleted) + return; + + foreach (var design in _storage) + { + if (!design.Links.Remove(deletedDesign)) + continue; + + design.LastEdit = DateTimeOffset.UtcNow; + Glamourer.Log.Debug($"Removed {deletedDesign.Identifier} from {design.Identifier} links due to deletion."); + _saveService.QueueSave(design); + } + } +} diff --git a/Glamourer/Designs/Links/DesignMerger.cs b/Glamourer/Designs/Links/DesignMerger.cs new file mode 100644 index 0000000..847d5f1 --- /dev/null +++ b/Glamourer/Designs/Links/DesignMerger.cs @@ -0,0 +1,328 @@ +using Glamourer.Api.Enums; +using Glamourer.Automation; +using Glamourer.Designs.Special; +using Glamourer.GameData; +using Glamourer.Interop.Material; +using Glamourer.Services; +using Glamourer.State; +using Glamourer.Unlocks; +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs.Links; + +public class DesignMerger( + DesignManager designManager, + CustomizeService _customize, + Configuration _config, + ItemUnlockManager _itemUnlocks, + CustomizeUnlockManager _customizeUnlocks) : IService +{ + public MergedDesign Merge(LinkContainer designs, in CustomizeArray currentCustomize, in DesignData baseRef, bool respectOwnership, + bool modAssociations) + => Merge(designs.Select(d => ((IDesignStandIn)d.Link, d.Type, JobFlag.All)), currentCustomize, baseRef, respectOwnership, + modAssociations); + + public MergedDesign Merge(IEnumerable<(IDesignStandIn, ApplicationType, JobFlag)> designs, in CustomizeArray currentCustomize, + in DesignData baseRef, bool respectOwnership, bool modAssociations) + { + var ret = new MergedDesign(designManager); + ret.Design.SetCustomize(_customize, currentCustomize); + var startBodyType = currentCustomize.BodyType; + CustomizeFlag fixFlags = 0; + respectOwnership &= _config.UnlockedItemMode; + foreach (var (design, type, jobs) in designs) + { + if (type is 0) + continue; + + ref readonly var data = ref design.GetDesignData(baseRef); + var source = design.AssociatedSource(); + + if (!data.IsHuman) + continue; + + var collection = type.ApplyWhat(design); + ReduceMeta(data, collection.Meta, ret, source); + ReduceCustomize(data, collection.Customize, ref fixFlags, ret, source, respectOwnership, startBodyType); + ReduceEquip(data, collection.Equip, ret, source, respectOwnership); + ReduceBonusItems(data, collection.BonusItem, ret, source, respectOwnership); + ReduceMainhands(data, jobs, collection.Equip, ret, source, respectOwnership); + ReduceOffhands(data, jobs, collection.Equip, ret, source, respectOwnership); + ReduceCrests(data, collection.Crest, ret, source); + ReduceParameters(data, collection.Parameters, ret, source); + ReduceMods(design as Design, ret, modAssociations); + if (type.HasFlag(ApplicationType.GearCustomization)) + ReduceMaterials(design, ret); + if (design.ForcedRedraw) + ret.ForcedRedraw = true; + if (design.ResetAdvancedDyes) + ret.ResetAdvancedDyes = true; + if (design.ResetTemporarySettings) + ret.ResetTemporarySettings = true; + } + + ApplyFixFlags(ret, fixFlags); + return ret; + } + + + private static void ReduceMaterials(IDesignStandIn designStandIn, MergedDesign ret) + { + if (designStandIn is not DesignBase design) + return; + + var materials = ret.Design.GetMaterialDataRef(); + foreach (var (key, value) in design.Materials.Where(p => p.Item2.Enabled)) + materials.TryAddValue(MaterialValueIndex.FromKey(key), value); + } + + private static void ReduceMods(Design? design, MergedDesign ret, bool modAssociations) + { + if (design == null || !modAssociations) + return; + + foreach (var (mod, settings) in design.AssociatedMods) + ret.AssociatedMods.TryAdd(mod, settings); + } + + private static void ReduceMeta(in DesignData design, MetaFlag applyMeta, MergedDesign ret, StateSource source) + { + applyMeta &= ~ret.Design.Application.Meta; + if (applyMeta == 0) + return; + + foreach (var index in MetaExtensions.AllRelevant) + { + if (!applyMeta.HasFlag(index.ToFlag())) + continue; + + ret.Design.SetApplyMeta(index, true); + ret.Design.GetDesignDataRef().SetMeta(index, design.GetMeta(index)); + ret.Sources[index] = source; + } + } + + private static void ReduceCrests(in DesignData design, CrestFlag crestFlags, MergedDesign ret, StateSource source) + { + crestFlags &= ~ret.Design.Application.Crest; + if (crestFlags == 0) + return; + + foreach (var slot in CrestExtensions.AllRelevantSet) + { + if (!crestFlags.HasFlag(slot)) + continue; + + ret.Design.GetDesignDataRef().SetCrest(slot, design.Crest(slot)); + ret.Design.SetApplyCrest(slot, true); + ret.Sources[slot] = source; + } + } + + private static void ReduceParameters(in DesignData design, CustomizeParameterFlag parameterFlags, MergedDesign ret, + StateSource source) + { + parameterFlags &= ~ret.Design.Application.Parameters; + if (parameterFlags == 0) + return; + + foreach (var flag in CustomizeParameterExtensions.AllFlags) + { + if (!parameterFlags.HasFlag(flag)) + continue; + + ret.Design.GetDesignDataRef().Parameters.Set(flag, design.Parameters[flag]); + ret.Design.SetApplyParameter(flag, true); + ret.Sources[flag] = source; + } + } + + private void ReduceEquip(in DesignData design, EquipFlag equipFlags, MergedDesign ret, StateSource source, + bool respectOwnership) + { + equipFlags &= ~ret.Design.Application.Equip; + if (equipFlags == 0) + return; + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var flag = slot.ToFlag(); + + if (equipFlags.HasFlag(flag)) + { + var item = design.Item(slot); + if (!respectOwnership || _itemUnlocks.IsUnlocked(item.Id, out _)) + ret.Design.GetDesignDataRef().SetItem(slot, item); + ret.Design.SetApplyEquip(slot, true); + ret.Sources[slot, false] = source; + } + + var stainFlag = slot.ToStainFlag(); + if (equipFlags.HasFlag(stainFlag)) + { + ret.Design.GetDesignDataRef().SetStain(slot, design.Stain(slot)); + ret.Design.SetApplyStain(slot, true); + ret.Sources[slot, true] = source; + } + } + + foreach (var slot in EquipSlotExtensions.WeaponSlots) + { + var stainFlag = slot.ToStainFlag(); + if (equipFlags.HasFlag(stainFlag)) + { + ret.Design.GetDesignDataRef().SetStain(slot, design.Stain(slot)); + ret.Design.SetApplyStain(slot, true); + ret.Sources[slot, true] = source; + } + } + } + + private void ReduceBonusItems(in DesignData design, BonusItemFlag bonusItems, MergedDesign ret, StateSource source, bool respectOwnership) + { + bonusItems &= ~ret.Design.Application.BonusItem; + if (bonusItems == 0) + return; + + foreach (var slot in BonusExtensions.AllFlags.Where(b => bonusItems.HasFlag(b))) + { + var item = design.BonusItem(slot); + if (!respectOwnership || true) // TODO: maybe check unlocks + ret.Design.GetDesignDataRef().SetBonusItem(slot, item); + ret.Design.SetApplyBonusItem(slot, true); + ret.Sources[slot] = source; + } + } + + private void ReduceMainhands(in DesignData design, JobFlag allowedJobs, EquipFlag equipFlags, MergedDesign ret, StateSource source, + bool respectOwnership) + { + if (!equipFlags.HasFlag(EquipFlag.Mainhand)) + return; + + var weapon = design.Item(EquipSlot.MainHand); + if (respectOwnership && !_itemUnlocks.IsUnlocked(weapon.Id, out _)) + return; + + if (!ret.Design.DoApplyEquip(EquipSlot.MainHand)) + { + ret.Design.SetApplyEquip(EquipSlot.MainHand, true); + ret.Design.GetDesignDataRef().SetItem(EquipSlot.MainHand, weapon); + } + + ret.Weapons.TryAdd(weapon.Type, weapon, source, allowedJobs); + } + + private void ReduceOffhands(in DesignData design, JobFlag allowedJobs, EquipFlag equipFlags, MergedDesign ret, StateSource source, + bool respectOwnership) + { + if (!equipFlags.HasFlag(EquipFlag.Offhand)) + return; + + var weapon = design.Item(EquipSlot.OffHand); + if (respectOwnership && !_itemUnlocks.IsUnlocked(weapon.Id, out _)) + return; + + if (!ret.Design.DoApplyEquip(EquipSlot.OffHand)) + { + ret.Design.SetApplyEquip(EquipSlot.OffHand, true); + ret.Design.GetDesignDataRef().SetItem(EquipSlot.OffHand, weapon); + } + + if (weapon.Valid) + ret.Weapons.TryAdd(weapon.Type, weapon, source, allowedJobs); + } + + private void ReduceCustomize(in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag fixFlags, MergedDesign ret, + StateSource source, bool respectOwnership, CustomizeValue startBodyType) + { + customizeFlags &= ~ret.Design.ApplyCustomizeExcludingBodyType; + if (ret.Design.DesignData.Customize.BodyType != startBodyType) + customizeFlags &= ~CustomizeFlag.BodyType; + + if (customizeFlags == 0) + return; + + // Skip anything not human. + if (!ret.Design.DesignData.IsHuman || !design.IsHuman) + return; + + var customize = ret.Design.DesignData.Customize; + if (customizeFlags.HasFlag(CustomizeFlag.Clan)) + { + fixFlags |= _customize.ChangeClan(ref customize, design.Customize.Clan); + ret.Design.SetApplyCustomize(CustomizeIndex.Clan, true); + ret.Design.SetApplyCustomize(CustomizeIndex.Race, true); + customizeFlags &= ~(CustomizeFlag.Clan | CustomizeFlag.Race); + ret.Sources[CustomizeIndex.Clan] = source; + ret.Sources[CustomizeIndex.Race] = source; + } + + if (customizeFlags.HasFlag(CustomizeFlag.Gender)) + { + fixFlags |= _customize.ChangeGender(ref customize, design.Customize.Gender); + ret.Design.SetApplyCustomize(CustomizeIndex.Gender, true); + customizeFlags &= ~CustomizeFlag.Gender; + ret.Sources[CustomizeIndex.Gender] = source; + } + + if (customizeFlags.HasFlag(CustomizeFlag.Face)) + { + customize[CustomizeIndex.Face] = design.Customize.Face; + ret.Design.SetApplyCustomize(CustomizeIndex.Face, true); + customizeFlags &= ~CustomizeFlag.Face; + ret.Sources[CustomizeIndex.Face] = source; + } + + if (customizeFlags.HasFlag(CustomizeFlag.BodyType)) + { + customize[CustomizeIndex.BodyType] = design.Customize.BodyType; + customizeFlags &= ~CustomizeFlag.BodyType; + ret.Sources[CustomizeIndex.BodyType] = source; + } + + var set = _customize.Manager.GetSet(customize.Clan, customize.Gender); + var face = customize.Face; + foreach (var index in Enum.GetValues()) + { + var flag = index.ToFlag(); + if (!customizeFlags.HasFlag(flag)) + continue; + + var value = design.Customize[index]; + if (!CustomizeService.IsCustomizationValid(set, face, index, value, out var data)) + continue; + + if (data.HasValue && respectOwnership && !_customizeUnlocks.IsUnlocked(data.Value, out _)) + continue; + + customize[index] = data?.Value ?? value; + ret.Design.SetApplyCustomize(index, true); + ret.Sources[index] = source; + fixFlags &= ~flag; + } + + ret.Design.SetCustomize(_customize, customize); + } + + private static void ApplyFixFlags(MergedDesign ret, CustomizeFlag fixFlags) + { + if (fixFlags == 0) + return; + + var source = ret.Design.DoApplyCustomize(CustomizeIndex.Clan) + ? ret.Sources[CustomizeIndex.Clan] + : ret.Sources[CustomizeIndex.Gender]; + foreach (var index in Enum.GetValues()) + { + var flag = index.ToFlag(); + if (!fixFlags.HasFlag(flag)) + continue; + + ret.Sources[index] = source; + ret.Design.SetApplyCustomize(index, true); + } + } +} diff --git a/Glamourer/Designs/Links/LinkContainer.cs b/Glamourer/Designs/Links/LinkContainer.cs new file mode 100644 index 0000000..6cfc121 --- /dev/null +++ b/Glamourer/Designs/Links/LinkContainer.cs @@ -0,0 +1,205 @@ +using Glamourer.Automation; +using Newtonsoft.Json.Linq; +using OtterGui.Filesystem; + +namespace Glamourer.Designs.Links; + +public sealed class LinkContainer : List +{ + public List Before + => this; + + public readonly List After = []; + + public new int Count + => base.Count + After.Count; + + public LinkContainer Clone() + { + var ret = new LinkContainer(); + ret.EnsureCapacity(base.Count); + ret.After.EnsureCapacity(After.Count); + ret.AddRange(this); + ret.After.AddRange(After); + return ret; + } + + public bool Reorder(int fromIndex, LinkOrder fromOrder, int toIndex, LinkOrder toOrder) + { + var fromList = fromOrder switch + { + LinkOrder.Before => Before, + LinkOrder.After => After, + _ => throw new ArgumentException("Invalid link order."), + }; + + var toList = toOrder switch + { + LinkOrder.Before => Before, + LinkOrder.After => After, + _ => throw new ArgumentException("Invalid link order."), + }; + + if (fromList == toList) + return fromList.Move(fromIndex, toIndex); + + if (fromIndex < 0 || fromIndex >= fromList.Count) + return false; + + toIndex = Math.Clamp(toIndex, 0, toList.Count); + toList.Insert(toIndex, fromList[fromIndex]); + fromList.RemoveAt(fromIndex); + return true; + } + + public bool Remove(int idx, LinkOrder order) + { + var list = order switch + { + LinkOrder.Before => Before, + LinkOrder.After => After, + _ => throw new ArgumentException("Invalid link order."), + }; + if (idx < 0 || idx >= list.Count) + return false; + + list.RemoveAt(idx); + return true; + } + + public bool ChangeApplicationRules(int idx, LinkOrder order, ApplicationType type, out ApplicationType old) + { + var list = order switch + { + LinkOrder.Before => Before, + LinkOrder.After => After, + _ => throw new ArgumentException("Invalid link order."), + }; + old = list[idx].Type; + if (idx < 0 || idx >= list.Count || old == type) + return false; + + list[idx] = list[idx] with { Type = type }; + return true; + } + + public static bool CanAddLink(Design parent, Design child, LinkOrder order, out string error) + { + if (parent == child) + { + error = $"Can not link {parent.Incognito} with itself."; + return false; + } + + if (parent.Links.Contains(child)) + { + error = $"Design {parent.Incognito} already contains a direct link to {child.Incognito}."; + return false; + } + + if (GetAllLinks(parent).Any(l => l.Link.Link == child && l.Order != order)) + { + error = + $"Adding {child.Incognito} to {parent.Incognito}s links would create a circle, the parent already links to the child in the opposite direction."; + return false; + } + + if (GetAllLinks(child).Any(l => l.Link.Link == parent && l.Order == order)) + { + error = + $"Adding {child.Incognito} to {parent.Incognito}s links would create a circle, the child already links to the parent in the opposite direction."; + return false; + } + + error = string.Empty; + return true; + } + + public static bool AddLink(Design parent, Design child, ApplicationType type, LinkOrder order, out string error) + { + if (!CanAddLink(parent, child, order, out error)) + return false; + + var list = order switch + { + LinkOrder.Before => parent.Links.Before, + LinkOrder.After => parent.Links.After, + _ => null, + }; + + if (list == null) + { + error = $"Order {order} is invalid."; + return false; + } + + type &= ApplicationType.All; + list.Add(new DesignLink(child, type)); + error = string.Empty; + return true; + } + + public bool Contains(Design child) + => Before.Any(l => l.Link == child) || After.Any(l => l.Link == child); + + public bool Remove(Design child) + => Before.RemoveAll(l => l.Link == child) + After.RemoveAll(l => l.Link == child) > 0; + + public static IEnumerable<(DesignLink Link, LinkOrder Order)> GetAllLinks(Design design) + { + var set = new HashSet(design.Links.Count * 4); + return GetAllLinks(new DesignLink(design, ApplicationType.All), LinkOrder.Self, set); + } + + private static IEnumerable<(DesignLink Link, LinkOrder Order)> GetAllLinks(DesignLink design, LinkOrder currentOrder, ISet visited) + { + if (design.Link.Links.Count == 0) + { + if (visited.Add(design.Link)) + yield return (design, currentOrder); + + yield break; + } + + foreach (var link in design.Link.Links.Before + .Where(l => !visited.Contains(l.Link)) + .SelectMany(l => GetAllLinks(l, currentOrder == LinkOrder.After ? LinkOrder.After : LinkOrder.Before, visited))) + yield return link; + + if (visited.Add(design.Link)) + yield return (design, currentOrder); + + foreach (var link in design.Link.Links.After.Where(l => !visited.Contains(l.Link)) + .SelectMany(l => GetAllLinks(l, currentOrder == LinkOrder.Before ? LinkOrder.Before : LinkOrder.After, visited))) + yield return link; + } + + public JObject Serialize() + { + var before = new JArray(); + foreach (var link in Before) + { + before.Add(new JObject + { + ["Design"] = link.Link.Identifier, + ["Type"] = (uint)link.Type, + }); + } + + var after = new JArray(); + foreach (var link in After) + { + after.Add(new JObject + { + ["Design"] = link.Link.Identifier, + ["Type"] = (uint)link.Type, + }); + } + + return new JObject + { + [nameof(Before)] = before, + [nameof(After)] = after, + }; + } +} diff --git a/Glamourer/Designs/Links/MergedDesign.cs b/Glamourer/Designs/Links/MergedDesign.cs new file mode 100644 index 0000000..3d81cda --- /dev/null +++ b/Glamourer/Designs/Links/MergedDesign.cs @@ -0,0 +1,105 @@ +using Glamourer.Interop.Penumbra; +using Glamourer.State; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs.Links; + +public readonly struct WeaponList +{ + private readonly Dictionary> _list = new(4); + + public IEnumerable<(EquipItem, StateSource, JobFlag)> Values + => _list.Values.SelectMany(t => t); + + public void Clear() + => _list.Clear(); + + public bool TryAdd(FullEquipType type, EquipItem item, StateSource source, JobFlag flags) + { + if (!_list.TryGetValue(type, out var list)) + { + list = new List<(EquipItem, StateSource, JobFlag)>(2); + _list.Add(type, list); + } + + var existingFlags = list.Count == 0 ? 0 : list.Select(t => t.Item3).Aggregate((t, existing) => t | existing); + var remainingFlags = flags & ~existingFlags; + + if (remainingFlags == 0) + return false; + + list.Add((item, source, remainingFlags)); + return true; + } + + public bool TryGet(FullEquipType type, JobId id, bool gameStateAllowed, out (EquipItem, StateSource) ret) + { + if (!_list.TryGetValue(type, out var list)) + { + ret = default; + return false; + } + + var flag = (JobFlag)(1ul << id.Id); + + foreach (var (item, source, flags) in list) + { + if (flags.HasFlag(flag) && (gameStateAllowed || source is not StateSource.Game)) + { + ret = (item, source); + return true; + } + } + + ret = default; + return false; + } + + public WeaponList() + { } +} + +public sealed class MergedDesign +{ + public MergedDesign(DesignManager designManager) + { + Design = designManager.CreateTemporary(); + Design.Application = ApplicationCollection.None; + } + + public MergedDesign(DesignBase design) + { + Design = design; + if (design.DoApplyEquip(EquipSlot.MainHand)) + { + var weapon = design.DesignData.Item(EquipSlot.MainHand); + if (weapon.Valid) + Weapons.TryAdd(weapon.Type, weapon, StateSource.Manual, JobFlag.All); + } + + if (design.DoApplyEquip(EquipSlot.OffHand)) + { + var weapon = design.DesignData.Item(EquipSlot.OffHand); + if (weapon.Valid) + Weapons.TryAdd(weapon.Type, weapon, StateSource.Manual, JobFlag.All); + } + + ForcedRedraw = design is IDesignStandIn { ForcedRedraw: true }; + } + + public MergedDesign(Design design) + : this((DesignBase)design) + { + foreach (var (mod, settings) in design.AssociatedMods) + AssociatedMods[mod] = settings; + } + + public readonly DesignBase Design; + public readonly WeaponList Weapons = new(); + public readonly SortedList AssociatedMods = []; + public StateSources Sources = new(); + public bool ForcedRedraw; + public bool ResetAdvancedDyes; + public bool ResetTemporarySettings; +} diff --git a/Glamourer/Designs/MetaIndex.cs b/Glamourer/Designs/MetaIndex.cs new file mode 100644 index 0000000..1842ae3 --- /dev/null +++ b/Glamourer/Designs/MetaIndex.cs @@ -0,0 +1,80 @@ +using Glamourer.Api.Enums; +using Glamourer.State; + +namespace Glamourer.Designs; + +public enum MetaIndex +{ + Wetness = StateIndex.MetaWetness, + HatState = StateIndex.MetaHatState, + VisorState = StateIndex.MetaVisorState, + WeaponState = StateIndex.MetaWeaponState, + ModelId = StateIndex.MetaModelId, + EarState = StateIndex.MetaEarState, +} + +public static class MetaExtensions +{ + public static readonly IReadOnlyList AllRelevant = + [MetaIndex.Wetness, MetaIndex.HatState, MetaIndex.VisorState, MetaIndex.WeaponState, MetaIndex.EarState]; + + public const MetaFlag All = MetaFlag.Wetness | MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState; + + public static MetaFlag ToFlag(this MetaIndex index) + => index switch + { + MetaIndex.Wetness => MetaFlag.Wetness, + MetaIndex.HatState => MetaFlag.HatState, + MetaIndex.VisorState => MetaFlag.VisorState, + MetaIndex.WeaponState => MetaFlag.WeaponState, + MetaIndex.EarState => MetaFlag.EarState, + _ => (MetaFlag)byte.MaxValue, + }; + + public static MetaIndex ToIndex(this MetaFlag index) + => index switch + { + MetaFlag.Wetness => MetaIndex.Wetness, + MetaFlag.HatState => MetaIndex.HatState, + MetaFlag.VisorState => MetaIndex.VisorState, + MetaFlag.WeaponState => MetaIndex.WeaponState, + MetaFlag.EarState => MetaIndex.EarState, + _ => (MetaIndex)byte.MaxValue, + }; + + public static IEnumerable ToIndices(this MetaFlag index) + { + if (index.HasFlag(MetaFlag.Wetness)) + yield return MetaIndex.Wetness; + if (index.HasFlag(MetaFlag.HatState)) + yield return MetaIndex.HatState; + if (index.HasFlag(MetaFlag.VisorState)) + yield return MetaIndex.VisorState; + if (index.HasFlag(MetaFlag.WeaponState)) + yield return MetaIndex.WeaponState; + if (index.HasFlag(MetaFlag.EarState)) + yield return MetaIndex.EarState; + } + + public static string ToName(this MetaIndex index) + => index switch + { + MetaIndex.HatState => "Hat Visible", + MetaIndex.VisorState => "Visor Toggled", + MetaIndex.WeaponState => "Weapon Visible", + MetaIndex.Wetness => "Force Wetness", + MetaIndex.EarState => "Ears Visible", + _ => "Unknown Meta", + }; + + public static string ToTooltip(this MetaIndex index) + => index switch + { + MetaIndex.HatState => "Hide or show the characters head gear.", + MetaIndex.VisorState => "Toggle the visor state of the characters head gear.", + MetaIndex.WeaponState => "Hide or show the characters weapons when not drawn.", + MetaIndex.Wetness => "Force the character to be wet or not.", + MetaIndex.EarState => "Hide or show the characters ears through the head gear. (Viera only)", + _ => string.Empty, + }; +} diff --git a/Glamourer/Designs/Special/QuickSelectedDesign.cs b/Glamourer/Designs/Special/QuickSelectedDesign.cs new file mode 100644 index 0000000..740bb7f --- /dev/null +++ b/Glamourer/Designs/Special/QuickSelectedDesign.cs @@ -0,0 +1,62 @@ +using Glamourer.Automation; +using Glamourer.Gui; +using Glamourer.Interop.Material; +using Glamourer.State; +using Newtonsoft.Json.Linq; +using OtterGui.Services; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs.Special; + +public class QuickSelectedDesign(QuickDesignCombo combo) : IDesignStandIn, IService +{ + public const string SerializedName = "//QuickSelection"; + public const string ResolvedName = "Quick Design Bar Selection"; + + public bool Equals(IDesignStandIn? other) + => other is QuickSelectedDesign; + + public string ResolveName(bool incognito) + => ResolvedName; + + public Design? CurrentDesign + => combo.Design as Design; + + public ref readonly DesignData GetDesignData(in DesignData baseRef) + { + if (combo.Design != null) + return ref combo.Design.GetDesignData(baseRef); + + return ref baseRef; + } + + public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData() + => combo.Design?.GetMaterialData() ?? []; + + public string SerializeName() + => SerializedName; + + public StateSource AssociatedSource() + => StateSource.Manual; + + public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication) + => combo.Design?.AllLinks(newApplication) ?? []; + + public void AddData(JObject jObj) + { } + + public void ParseData(JObject jObj) + { } + + public bool ChangeData(object data) + => false; + + public bool ForcedRedraw + => combo.Design?.ForcedRedraw ?? false; + + public bool ResetAdvancedDyes + => combo.Design?.ResetAdvancedDyes ?? false; + + public bool ResetTemporarySettings + => combo.Design?.ResetTemporarySettings ?? false; +} diff --git a/Glamourer/Designs/Special/RandomDesign.cs b/Glamourer/Designs/Special/RandomDesign.cs new file mode 100644 index 0000000..844f203 --- /dev/null +++ b/Glamourer/Designs/Special/RandomDesign.cs @@ -0,0 +1,102 @@ +using Glamourer.Automation; +using Glamourer.Interop.Material; +using Glamourer.State; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs.Special; + +public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn +{ + public const string SerializedName = "//Random"; + public const string ResolvedName = "Random"; + private Design? _currentDesign; + + public IReadOnlyList Predicates { get; private set; } = []; + public bool ResetOnRedraw { get; set; } = false; + + public string ResolveName(bool _) + => ResolvedName; + + public ref readonly DesignData GetDesignData(in DesignData baseRef) + { + _currentDesign ??= rng.Design(Predicates); + if (_currentDesign == null) + return ref baseRef; + + return ref _currentDesign.GetDesignDataRef(); + } + + public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData() + { + _currentDesign ??= rng.Design(Predicates); + if (_currentDesign == null) + return []; + + return _currentDesign.Materials; + } + + public string SerializeName() + => SerializedName; + + public bool Equals(IDesignStandIn? other) + => other is RandomDesign r + && r.ResetOnRedraw == ResetOnRedraw + && string.Equals(RandomPredicate.GeneratePredicateString(r.Predicates), RandomPredicate.GeneratePredicateString(Predicates), + StringComparison.OrdinalIgnoreCase); + + public StateSource AssociatedSource() + => StateSource.Manual; + + public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication) + { + if (newApplication || ResetOnRedraw) + _currentDesign = rng.Design(Predicates); + else + _currentDesign ??= rng.Design(Predicates); + if (_currentDesign == null) + yield break; + + foreach (var (link, type, jobs) in _currentDesign.AllLinks(newApplication)) + yield return (link, type, jobs); + } + + public void AddData(JObject jObj) + { + jObj["Restrictions"] = RandomPredicate.GeneratePredicateString(Predicates); + jObj["ResetOnRedraw"] = ResetOnRedraw; + } + + public void ParseData(JObject jObj) + { + var restrictions = jObj["Restrictions"]?.ToObject() ?? string.Empty; + Predicates = RandomPredicate.GeneratePredicates(restrictions); + ResetOnRedraw = jObj["ResetOnRedraw"]?.ToObject() ?? false; + } + + public bool ChangeData(object data) + { + if (data is List predicates) + { + Predicates = predicates; + return true; + } + + if (data is bool resetOnRedraw) + { + ResetOnRedraw = resetOnRedraw; + return true; + } + + return false; + } + + public bool ForcedRedraw + => _currentDesign?.ForcedRedraw ?? false; + + public bool ResetAdvancedDyes + => _currentDesign?.ResetAdvancedDyes ?? false; + + public bool ResetTemporarySettings + => _currentDesign?.ResetTemporarySettings ?? false; +} diff --git a/Glamourer/Designs/Special/RandomDesignGenerator.cs b/Glamourer/Designs/Special/RandomDesignGenerator.cs new file mode 100644 index 0000000..b1e1e7c --- /dev/null +++ b/Glamourer/Designs/Special/RandomDesignGenerator.cs @@ -0,0 +1,51 @@ +using OtterGui; +using OtterGui.Services; + +namespace Glamourer.Designs.Special; + +public class RandomDesignGenerator(DesignStorage designs, DesignFileSystem fileSystem, Configuration config) : IService +{ + private readonly Random _rng = new(); + private readonly WeakReference _lastDesign = new(null!, false); + + public Design? Design(IReadOnlyList localDesigns) + { + if (localDesigns.Count is 0) + return null; + + var idx = _rng.Next(0, localDesigns.Count); + if (localDesigns.Count is 1) + { + _lastDesign.SetTarget(localDesigns[idx]); + return localDesigns[idx]; + } + + if (config.PreventRandomRepeats && _lastDesign.TryGetTarget(out var lastDesign)) + while (lastDesign == localDesigns[idx]) + idx = _rng.Next(0, localDesigns.Count); + + var design = localDesigns[idx]; + Glamourer.Log.Verbose($"[Random Design] Chose design {idx + 1} out of {localDesigns.Count}: {design.Incognito}."); + _lastDesign.SetTarget(design); + return design; + } + + public Design? Design() + => Design(designs); + + public Design? Design(IDesignPredicate predicate) + => Design(predicate.Get(designs, fileSystem).ToList()); + + public Design? Design(IReadOnlyList predicates) + { + return predicates.Count switch + { + 0 => Design(), + 1 => Design(predicates[0]), + _ => Design(IDesignPredicate.Get(predicates, designs, fileSystem).ToList()), + }; + } + + public Design? Design(string restrictions) + => Design(RandomPredicate.GeneratePredicates(restrictions)); +} diff --git a/Glamourer/Designs/Special/RandomPredicate.cs b/Glamourer/Designs/Special/RandomPredicate.cs new file mode 100644 index 0000000..ae05f8f --- /dev/null +++ b/Glamourer/Designs/Special/RandomPredicate.cs @@ -0,0 +1,163 @@ +using OtterGui.Classes; + +namespace Glamourer.Designs.Special; + +public interface IDesignPredicate +{ + public bool Invoke(Design design, string lowerName, string identifier, string lowerPath); + + public bool Invoke((Design Design, string LowerName, string Identifier, string LowerPath) args) + => Invoke(args.Design, args.LowerName, args.Identifier, args.LowerPath); + + public IEnumerable Get(IEnumerable designs, DesignFileSystem fileSystem) + => designs.Select(d => Transform(d, fileSystem)) + .Where(Invoke) + .Select(t => t.Design); + + public static IEnumerable Get(IReadOnlyList predicates, IEnumerable designs, DesignFileSystem fileSystem) + => predicates.Count > 0 + ? designs.Select(d => Transform(d, fileSystem)) + .Where(t => predicates.Any(p => p.Invoke(t))) + .Select(t => t.Design) + : designs; + + private static (Design Design, string LowerName, string Identifier, string LowerPath) Transform(Design d, DesignFileSystem fs) + => (d, d.Name.Lower, d.Identifier.ToString(), fs.TryGetValue(d, out var l) ? l.FullName().ToLowerInvariant() : string.Empty); +} + +public static class RandomPredicate +{ + public readonly struct StartsWith(string value) : IDesignPredicate + { + public LowerString Value { get; } = value; + + public bool Invoke(Design design, string lowerName, string identifier, string lowerPath) + => lowerPath.StartsWith(Value.Lower); + + public override string ToString() + => $"/{Value.Text}"; + } + + public readonly struct Contains(string value) : IDesignPredicate + { + public LowerString Value { get; } = value; + + public bool Invoke(Design design, string lowerName, string identifier, string lowerPath) + { + if (lowerName.Contains(Value.Lower)) + return true; + if (identifier.Contains(Value.Lower)) + return true; + if (lowerPath.Contains(Value.Lower)) + return true; + + return false; + } + + public override string ToString() + => Value.Text; + } + + public readonly struct Exact(Exact.Type type, string value) : IDesignPredicate + { + public enum Type : byte + { + Name, + Path, + Identifier, + Tag, + Color, + } + + public Type Which { get; } = type; + public LowerString Value { get; } = value; + + public bool Invoke(Design design, string lowerName, string identifier, string lowerPath) + => Which switch + { + Type.Name => lowerName == Value.Lower, + Type.Path => lowerPath == Value.Lower, + Type.Identifier => identifier == Value.Lower, + Type.Tag => IsContained(Value, design.Tags), + Type.Color => design.Color == Value, + _ => false, + }; + + private static bool IsContained(LowerString value, IEnumerable data) + => data.Any(t => t == value); + + public override string ToString() + => $"\"{Which switch { Type.Name => 'n', Type.Identifier => 'i', Type.Path => 'p', Type.Tag => 't', Type.Color => 'c', _ => '?' }}?{Value.Text}\""; + } + + public static IDesignPredicate CreateSinglePredicate(string restriction) + { + switch (restriction[0]) + { + case '/': return new StartsWith(restriction[1..]); + case '"': + var end = restriction.IndexOf('"', 1); + if (end < 3) + return new Contains(restriction); + + switch (restriction[1], restriction[2]) + { + case ('n', '?'): + case ('N', '?'): + return new Exact(Exact.Type.Name, restriction[3..end]); + case ('p', '?'): + case ('P', '?'): + return new Exact(Exact.Type.Path, restriction[3..end]); + case ('i', '?'): + case ('I', '?'): + return new Exact(Exact.Type.Identifier, restriction[3..end]); + case ('t', '?'): + case ('T', '?'): + return new Exact(Exact.Type.Tag, restriction[3..end]); + case ('c', '?'): + case ('C', '?'): + return new Exact(Exact.Type.Color, restriction[3..end]); + default: return new Contains(restriction); + } + default: return new Contains(restriction); + } + } + + public static List GeneratePredicates(string restrictions) + { + if (restrictions.Length == 0) + return []; + + List predicates = new(1); + if (restrictions[0] is '{') + { + var end = restrictions.IndexOf('}'); + if (end == -1) + { + predicates.Add(CreateSinglePredicate(restrictions)); + } + else + { + restrictions = restrictions[1..end]; + var split = restrictions.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + predicates.AddRange(split.Distinct().Select(CreateSinglePredicate)); + } + } + else + { + predicates.Add(CreateSinglePredicate(restrictions)); + } + + return predicates; + } + + public static string GeneratePredicateString(IReadOnlyCollection predicates) + { + if (predicates.Count == 0) + return string.Empty; + if (predicates.Count == 1) + return predicates.First()!.ToString()!; + + return $"{{{string.Join("; ", predicates)}}}"; + } +} diff --git a/Glamourer/Designs/Special/RevertDesign.cs b/Glamourer/Designs/Special/RevertDesign.cs new file mode 100644 index 0000000..4caf7b6 --- /dev/null +++ b/Glamourer/Designs/Special/RevertDesign.cs @@ -0,0 +1,54 @@ +using Glamourer.Automation; +using Glamourer.Interop.Material; +using Glamourer.State; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs.Special; + +public class RevertDesign : IDesignStandIn +{ + public const string SerializedName = "//Revert"; + public const string ResolvedName = "Revert"; + + public string ResolveName(bool _) + => ResolvedName; + + public ref readonly DesignData GetDesignData(in DesignData baseRef) + => ref baseRef; + + public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData() + => []; + + public string SerializeName() + => SerializedName; + + public bool Equals(IDesignStandIn? other) + => other is RevertDesign; + + public StateSource AssociatedSource() + => StateSource.Game; + + public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool _) + { + yield return (this, ApplicationType.All, JobFlag.All); + } + + public void AddData(JObject jObj) + { } + + public void ParseData(JObject jObj) + { } + + public bool ChangeData(object data) + => false; + + public bool ForcedRedraw + => false; + + public bool ResetAdvancedDyes + => true; + + public bool ResetTemporarySettings + => true; +} diff --git a/Glamourer/EphemeralConfig.cs b/Glamourer/EphemeralConfig.cs new file mode 100644 index 0000000..98dabec --- /dev/null +++ b/Glamourer/EphemeralConfig.cs @@ -0,0 +1,78 @@ +using Dalamud.Interface.ImGuiNotification; +using Glamourer.Gui; +using Glamourer.Services; +using Newtonsoft.Json; +using OtterGui.Classes; +using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; + +namespace Glamourer; + +public class EphemeralConfig : ISavable +{ + public int Version { get; set; } = Configuration.Constants.CurrentVersion; + public bool IncognitoMode { get; set; } = false; + public bool UnlockDetailMode { get; set; } = true; + public bool ShowDesignQuickBar { get; set; } = false; + public bool LockDesignQuickBar { get; set; } = false; + public bool LockMainWindow { get; set; } = false; + public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings; + public Guid SelectedDesign { get; set; } = Guid.Empty; + public Guid SelectedQuickDesign { get; set; } = Guid.Empty; + public int LastSeenVersion { get; set; } = GlamourerChangelog.LastChangelogVersion; + + public float CurrentDesignSelectorWidth { get; set; } = 200f; + public float DesignSelectorMinimumScale { get; set; } = 0.1f; + public float DesignSelectorMaximumScale { get; set; } = 0.5f; + + + [JsonIgnore] + private readonly SaveService _saveService; + + public EphemeralConfig(SaveService saveService) + { + _saveService = saveService; + Load(); + } + + public void Save() + => _saveService.DelaySave(this, TimeSpan.FromSeconds(5)); + + public void Load() + { + static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) + { + Glamourer.Log.Error( + $"Error parsing ephemeral Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); + errorArgs.ErrorContext.Handled = true; + } + + if (!File.Exists(_saveService.FileNames.EphemeralConfigFile)) + return; + + try + { + var text = File.ReadAllText(_saveService.FileNames.EphemeralConfigFile); + JsonConvert.PopulateObject(text, this, new JsonSerializerSettings + { + Error = HandleDeserializationError, + }); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, + "Error reading ephemeral Configuration, reverting to default.", + "Error reading ephemeral Configuration", NotificationType.Error); + } + } + + public string ToFilename(FilenameService fileNames) + => fileNames.EphemeralConfigFile; + + public void Save(StreamWriter writer) + { + using var jWriter = new JsonTextWriter(writer); + jWriter.Formatting = Formatting.Indented; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + serializer.Serialize(jWriter, this); + } +} diff --git a/Glamourer/Events/AutoRedrawChanged.cs b/Glamourer/Events/AutoRedrawChanged.cs new file mode 100644 index 0000000..a8dd03a --- /dev/null +++ b/Glamourer/Events/AutoRedrawChanged.cs @@ -0,0 +1,16 @@ +using OtterGui.Classes; + +namespace Glamourer.Events; + +/// +/// Triggered when the auto-reload gear setting is changed in glamourer configuration. +/// +public sealed class AutoRedrawChanged() + : EventWrapper(nameof(AutoRedrawChanged)) +{ + public enum Priority + { + /// + StateApi = int.MinValue, + } +} \ No newline at end of file diff --git a/Glamourer/Events/AutomationChanged.cs b/Glamourer/Events/AutomationChanged.cs index 4a35e4b..c368899 100644 --- a/Glamourer/Events/AutomationChanged.cs +++ b/Glamourer/Events/AutomationChanged.cs @@ -1,5 +1,4 @@ -using System; -using Glamourer.Automation; +using Glamourer.Automation; using OtterGui.Classes; namespace Glamourer.Events; @@ -12,8 +11,8 @@ namespace Glamourer.Events; /// Parameter is additional data depending on the type of change. /// /// -public sealed class AutomationChanged : EventWrapper, - AutomationChanged.Priority> +public sealed class AutomationChanged() + : EventWrapper(nameof(AutomationChanged)) { public enum Type { @@ -38,6 +37,9 @@ public sealed class AutomationChanged : EventWrapper Change the used base state of a given set. Additional data is prior and new base. [(AutoDesignSet.Base, AutoDesignSet.Base)]. ChangedBase, + /// Change the resetting of temporary settings for a given set. Additional data is the new value. + ChangedTemporarySettingsReset, + /// Add a new associated design to a given set. Additional data is the index it got added at [int]. AddedDesign, @@ -47,7 +49,7 @@ public sealed class AutomationChanged : EventWrapper Move a given associated design in the list of a given set. Additional data is the index that got moved and the index it got moved to [(int, int)]. MovedDesign, - /// Change the linked design in an associated design for a given set. Additional data is the index of the changed associated design, the old linked design and the new linked design [(int, Design, Design)]. + /// Change the linked design in an associated design for a given set. Additional data is the index of the changed associated design, the old linked design and the new linked design [(int, IDesignStandIn, IDesignStandIn)]. ChangedDesign, /// Change the job condition in an associated design for a given set. Additional data is the index of the changed associated design, the old job group and the new job group [(int, JobGroup, JobGroup)]. @@ -55,6 +57,9 @@ public sealed class AutomationChanged : EventWrapper Change the application type in an associated design for a given set. Additional data is the index of the changed associated design, the old type and the new type. [(int, AutoDesign.Type, AutoDesign.Type)]. ChangedType, + + /// Change the additional data for a specific design type. Additional data is the index of the changed associated design and the new data. [(int, object)] + ChangedData, } public enum Priority @@ -63,13 +68,9 @@ public sealed class AutomationChanged : EventWrapper - AutoDesignApplier, + AutoDesignApplier = 0, + + /// + RandomRestrictionDrawer = -1, } - - public AutomationChanged() - : base(nameof(AutomationChanged)) - { } - - public void Invoke(Type type, AutoDesignSet? set, object? data) - => Invoke(this, type, set, data); } diff --git a/Glamourer/Events/BonusSlotUpdating.cs b/Glamourer/Events/BonusSlotUpdating.cs new file mode 100644 index 0000000..3f6e761 --- /dev/null +++ b/Glamourer/Events/BonusSlotUpdating.cs @@ -0,0 +1,25 @@ +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; + +namespace Glamourer.Events; + +/// +/// Triggered when a model flags a bonus slot for an update. +/// +/// Parameter is the model with a flagged slot. +/// Parameter is the bonus slot changed. +/// Parameter is the model values to change the bonus piece to. +/// Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. +/// +/// +public sealed class BonusSlotUpdating() + : EventWrapperRef34(nameof(BonusSlotUpdating)) +{ + public enum Priority + { + /// + StateListener = 0, + } +} diff --git a/Glamourer/Events/DesignChanged.cs b/Glamourer/Events/DesignChanged.cs index c06eb53..04bb46a 100644 --- a/Glamourer/Events/DesignChanged.cs +++ b/Glamourer/Events/DesignChanged.cs @@ -1,5 +1,6 @@ -using System; using Glamourer.Designs; +using Glamourer.Designs.History; +using Glamourer.Gui; using OtterGui.Classes; namespace Glamourer.Events; @@ -12,84 +13,138 @@ namespace Glamourer.Events; /// Parameter is any additional data depending on the type of change. /// /// -public sealed class DesignChanged : EventWrapper, DesignChanged.Priority> +public sealed class DesignChanged() + : EventWrapper(nameof(DesignChanged)) { public enum Type { - /// A new design was created. Data is a potential path to move it to [string?]. + /// A new design was created. Created, - /// An existing design was deleted. Data is null. + /// An existing design was deleted. Deleted, - /// Invoked on full reload. Design and Data are null. + /// Invoked on full reload. ReloadedAll, - /// An existing design was renamed. Data is the prior name [string]. + /// An existing design was renamed. Renamed, - /// An existing design had its description changed. Data is the prior description [string]. + /// An existing design had its description changed. ChangedDescription, - /// An existing design had a new tag added. Data is the new tag and the index it was added at [(string, int)]. + /// An existing design had its associated color changed. + ChangedColor, + + /// An existing design had a new tag added. AddedTag, - /// An existing design had an existing tag removed. Data is the removed tag and the index it had before removal [(string, int)]. + /// An existing design had an existing tag removed. RemovedTag, - /// An existing design had an existing tag renamed. Data is the old name of the tag, the new name of the tag, and the index it had before being resorted [(string, string, int)]. + /// An existing design had an existing tag renamed. ChangedTag, - /// An existing design had a new associated mod added. Data is the Mod and its Settings [(Mod, ModSettings)]. + /// An existing design had a new associated mod added. AddedMod, - /// An existing design had an existing associated mod removed. Data is the Mod and its Settings [(Mod, ModSettings)]. + /// An existing design had an existing associated mod removed. RemovedMod, - /// An existing design had a customization changed. Data is the old value, the new value and the type [(CustomizeValue, CustomizeValue, CustomizeIndex)]. + /// An existing design had an existing associated mod updated. + UpdatedMod, + + /// An existing design had a link to a different design added, removed or moved. + ChangedLink, + + /// An existing design had a customization changed. Customize, - /// An existing design had an equipment piece changed. Data is the old value, the new value and the slot [(EquipItem, EquipItem, EquipSlot)]. + /// An existing design had its entire customize array changed. + EntireCustomize, + + /// An existing design had an equipment piece changed. Equip, - /// An existing design had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand and the new offhand [(EquipItem, EquipItem, EquipItem, EquipItem)]. + /// An existing design had a bonus item changed. + BonusItem, + + /// An existing design had its weapons changed. Weapon, - /// An existing design had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. - Stain, + /// An existing design had a stain changed. + Stains, - /// An existing design changed whether a specific customization is applied. Data is the type of customization [CustomizeIndex]. + /// An existing design had a crest visibility changed. + Crest, + + /// An existing design had a customize parameter changed. + Parameter, + + /// An existing design had an advanced dye row added, changed, or deleted. + Material, + + /// An existing design had an advanced dye rows Revert state changed. + MaterialRevert, + + /// An existing design had changed whether it always forces a redraw or not. + ForceRedraw, + + /// An existing design had changed whether it always resets advanced dyes or not. + ResetAdvancedDyes, + + /// An existing design had changed whether it always resets all prior temporary settings or not. + ResetTemporarySettings, + + /// An existing design changed whether a specific customization is applied. ApplyCustomize, - /// An existing design changed whether a specific equipment is applied. Data is the slot of the equipment [EquipSlot]. + /// An existing design changed whether a specific equipment piece is applied. ApplyEquip, - /// An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot]. + /// An existing design changed whether a specific bonus item is applied. + ApplyBonusItem, + + /// An existing design changed whether a specific stain is applied. ApplyStain, - /// An existing design changed its write protection status. Data is the new value [bool]. + /// An existing design changed whether a specific crest visibility is applied. + ApplyCrest, + + /// An existing design changed whether a specific customize parameter is applied. + ApplyParameter, + + /// An existing design changed whether an advanced dye row is applied. + ApplyMaterial, + + /// An existing design changed its write protection status. WriteProtection, - /// An existing design changed one of the meta flags. Data is the flag, whether it was about their applying and the new value [(MetaFlag, bool, bool)]. + /// An existing design changed its display status for the quick design bar. + QuickDesignBar, + + /// An existing design changed one of the meta flags. Other, } public enum Priority { + /// + DesignLinkManager = 1, + + /// + AutoDesignManager = 1, + /// DesignFileSystem = 0, /// DesignFileSystemSelector = -1, - /// - AutoDesignManager = 1, + /// + DesignCombo = -2, + + /// + EditorHistory = -1000, } - - public DesignChanged() - : base(nameof(DesignChanged)) - { } - - public void Invoke(Type type, Design design, object? data = null) - => Invoke(this, type, design, data); } diff --git a/Glamourer/Events/EquipSlotUpdating.cs b/Glamourer/Events/EquipSlotUpdating.cs new file mode 100644 index 0000000..a2daf85 --- /dev/null +++ b/Glamourer/Events/EquipSlotUpdating.cs @@ -0,0 +1,25 @@ +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; + +namespace Glamourer.Events; + +/// +/// Triggered when a model flags an equipment slot for an update. +/// +/// Parameter is the model with a flagged slot. +/// Parameter is the equipment slot changed. +/// Parameter is the model values to change the equipment piece to. +/// Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. +/// +/// +public sealed class EquipSlotUpdating() + : EventWrapperRef34(nameof(EquipSlotUpdating)) +{ + public enum Priority + { + /// + StateListener = 0, + } +} \ No newline at end of file diff --git a/Glamourer/Events/EquippedGearset.cs b/Glamourer/Events/EquippedGearset.cs new file mode 100644 index 0000000..c4252eb --- /dev/null +++ b/Glamourer/Events/EquippedGearset.cs @@ -0,0 +1,23 @@ +using OtterGui.Classes; + +namespace Glamourer.Events; + +/// +/// Triggered when the player equips a gear set. +/// +/// Parameter is the name of the gear set. +/// Parameter is the id of the gear set. +/// Parameter is the id of the prior gear set. +/// Parameter is the id of the associated glamour. +/// Parameter is the job id of the associated job. +/// +/// +public sealed class EquippedGearset() + : EventWrapper(nameof(EquippedGearset)) +{ + public enum Priority + { + /// + AutoDesignApplier = 0, + } +} diff --git a/Glamourer/Events/GPoseService.cs b/Glamourer/Events/GPoseService.cs index 7403754..44421a0 100644 --- a/Glamourer/Events/GPoseService.cs +++ b/Glamourer/Events/GPoseService.cs @@ -1,11 +1,9 @@ -using System; -using System.Collections.Concurrent; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.Services; using OtterGui.Classes; namespace Glamourer.Events; -public sealed class GPoseService : EventWrapper, GPoseService.Priority> +public sealed class GPoseService : EventWrapper { private readonly IFramework _framework; private readonly IClientState _state; @@ -15,8 +13,8 @@ public sealed class GPoseService : EventWrapper, GPoseService.Prior public enum Priority { - /// - GlamourerIpc = int.MinValue, + /// + StateApi = int.MinValue, } public bool InGPose { get; private set; } @@ -56,9 +54,9 @@ public sealed class GPoseService : EventWrapper, GPoseService.Prior return; InGPose = inGPose; - Invoke(this, InGPose); + Invoke(InGPose); var actions = InGPose ? _onEnter : _onLeave; - foreach (var action in actions) + while (actions.TryDequeue(out var action)) { try { diff --git a/Glamourer/Events/GearsetDataLoaded.cs b/Glamourer/Events/GearsetDataLoaded.cs new file mode 100644 index 0000000..620bdab --- /dev/null +++ b/Glamourer/Events/GearsetDataLoaded.cs @@ -0,0 +1,21 @@ +using OtterGui.Classes; +using Penumbra.GameData.Interop; + +namespace Glamourer.Events; + +/// +/// Triggers when the equipped gearset finished all LoadEquipment, LoadWeapon, and LoadCrest calls. (All Non-MetaData) +/// This defines an endpoint for when the gameState is updated. +/// +/// The model draw object associated with the finished load (Also fired by other players on render) +/// +/// +public sealed class GearsetDataLoaded() + : EventWrapper(nameof(GearsetDataLoaded)) +{ + public enum Priority + { + /// + StateListener = 0, + } +} diff --git a/Glamourer/Events/HeadGearVisibilityChanged.cs b/Glamourer/Events/HeadGearVisibilityChanged.cs index d12cd69..d8f722b 100644 --- a/Glamourer/Events/HeadGearVisibilityChanged.cs +++ b/Glamourer/Events/HeadGearVisibilityChanged.cs @@ -1,6 +1,5 @@ -using System; -using Glamourer.Interop.Structs; using OtterGui.Classes; +using Penumbra.GameData.Interop; namespace Glamourer.Events; @@ -11,22 +10,12 @@ namespace Glamourer.Events; /// Parameter is the new state. /// /// -public sealed class HeadGearVisibilityChanged : EventWrapper>, HeadGearVisibilityChanged.Priority> +public sealed class HeadGearVisibilityChanged() + : EventWrapperRef2(nameof(HeadGearVisibilityChanged)) { public enum Priority { /// StateListener = 0, } - - public HeadGearVisibilityChanged() - : base(nameof(HeadGearVisibilityChanged)) - { } - - public void Invoke(Actor actor, ref bool state) - { - var value = new Ref(state); - Invoke(this, actor, value); - state = value; - } } diff --git a/Glamourer/Events/MovedEquipment.cs b/Glamourer/Events/MovedEquipment.cs index 4548575..9d24a03 100644 --- a/Glamourer/Events/MovedEquipment.cs +++ b/Glamourer/Events/MovedEquipment.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -11,18 +10,12 @@ namespace Glamourer.Events; /// Parameter is an array of slots updated and corresponding item ids and stains. /// /// -public sealed class MovedEquipment : EventWrapper, MovedEquipment.Priority> +public sealed class MovedEquipment() + : EventWrapper<(EquipSlot, uint, StainIds)[], MovedEquipment.Priority>(nameof(MovedEquipment)) { public enum Priority { /// StateListener = 0, } - - public MovedEquipment() - : base(nameof(MovedEquipment)) - { } - - public void Invoke((EquipSlot, uint, StainId)[] items) - => Invoke(this, items); } diff --git a/Glamourer/Events/ObjectUnlocked.cs b/Glamourer/Events/ObjectUnlocked.cs index 7b8c120..b15acd2 100644 --- a/Glamourer/Events/ObjectUnlocked.cs +++ b/Glamourer/Events/ObjectUnlocked.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; namespace Glamourer.Events; @@ -11,7 +10,8 @@ namespace Glamourer.Events; /// Parameter is the timestamp of the unlock. /// /// -public sealed class ObjectUnlocked : EventWrapper, ObjectUnlocked.Priority> +public sealed class ObjectUnlocked() + : EventWrapper(nameof(ObjectUnlocked)) { public enum Type { @@ -25,11 +25,4 @@ public sealed class ObjectUnlocked : EventWrapper Currently used as a hack to make the unlock table dirty in it. If anything else starts using this, rework. UnlockTable = 0, } - - public ObjectUnlocked() - : base(nameof(ObjectUnlocked)) - { } - - public void Invoke(Type type, uint id, DateTimeOffset timestamp) - => Invoke(this, type, id, timestamp); } diff --git a/Glamourer/Events/PenumbraReloaded.cs b/Glamourer/Events/PenumbraReloaded.cs index 40a2527..0975670 100644 --- a/Glamourer/Events/PenumbraReloaded.cs +++ b/Glamourer/Events/PenumbraReloaded.cs @@ -1,4 +1,3 @@ -using System; using OtterGui.Classes; namespace Glamourer.Events; @@ -6,18 +5,18 @@ namespace Glamourer.Events; /// /// Triggered when Penumbra is reloaded. /// -public sealed class PenumbraReloaded : EventWrapper +public sealed class PenumbraReloaded() + : EventWrapper(nameof(PenumbraReloaded)) { public enum Priority { /// ChangeCustomizeService = 0, + + /// + VisorService = 0, + + /// + VieraEarService = 0, } - - public PenumbraReloaded() - : base(nameof(PenumbraReloaded)) - { } - - public void Invoke() - => Invoke(this); } diff --git a/Glamourer/Events/SlotUpdating.cs b/Glamourer/Events/SlotUpdating.cs deleted file mode 100644 index d83822b..0000000 --- a/Glamourer/Events/SlotUpdating.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using Glamourer.Interop.Structs; -using OtterGui.Classes; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Glamourer.Events; - -/// -/// Triggered when a model flags an equipment slot for an update. -/// -/// Parameter is the model with a flagged slot. -/// Parameter is the equipment slot changed. -/// Parameter is the model values to change the equipment piece to. -/// Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. -/// -/// -public sealed class SlotUpdating : EventWrapper, Ref>, SlotUpdating.Priority> -{ - public enum Priority - { - /// - StateListener = 0, - } - - public SlotUpdating() - : base(nameof(SlotUpdating)) - { } - - public void Invoke(Model model, EquipSlot slot, ref CharacterArmor armor, ref ulong returnValue) - { - var value = new Ref(armor); - var @return = new Ref(returnValue); - Invoke(this, model, slot, value, @return); - armor = value; - returnValue = @return; - } -} diff --git a/Glamourer/Events/StateChanged.cs b/Glamourer/Events/StateChanged.cs index 2c5c5c8..2bcc6fe 100644 --- a/Glamourer/Events/StateChanged.cs +++ b/Glamourer/Events/StateChanged.cs @@ -1,8 +1,9 @@ -using System; +using Glamourer.Api.Enums; +using Glamourer.Designs.History; using Glamourer.Interop.Structs; using Glamourer.State; using OtterGui.Classes; -using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; namespace Glamourer.Events; @@ -15,55 +16,18 @@ namespace Glamourer.Events; /// Parameter is any additional data depending on the type of change. /// /// -public sealed class StateChanged : EventWrapper, StateChanged.Priority> +public sealed class StateChanged() + : EventWrapper(nameof(StateChanged)) { - public enum Type - { - /// A characters saved state had the model id changed. This means everything may have changed. Data is the old model id and the new model id. [(uint, uint)] - Model, - - /// A characters saved state had multiple customization values changed. TData is the old customize array and the applied changes. [(Customize, CustomizeFlag)] - EntireCustomize, - - /// A characters saved state had a customization value changed. Data is the old value, the new value and the type. [(CustomizeValue, CustomizeValue, CustomizeIndex)]. - Customize, - - /// A characters saved state had an equipment piece changed. Data is the old value, the new value and the slot [(EquipItem, EquipItem, EquipSlot)]. - Equip, - - /// A characters saved state had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand and the new offhand [(EquipItem, EquipItem, EquipItem, EquipItem)]. - Weapon, - - /// A characters saved state had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. - Stain, - - /// A characters saved state had a design applied. This means everything may have changed. Data is the applied design. [DesignBase] - Design, - - /// A characters saved state had its state reset to its game values. This means everything may have changed. Data is null. - Reset, - - /// A characters saved state had a meta toggle changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. - Other, - } - - public enum Source : byte - { - Game, - Manual, - Fixed, - Ipc, - } - public enum Priority { + /// GlamourerIpc = int.MinValue, + + /// + PenumbraAutoRedraw = 0, + + /// + EditorHistory = -1000, } - - public StateChanged() - : base(nameof(StateChanged)) - { } - - public void Invoke(Type type, Source source, ActorState state, ActorData actors, object? data = null) - => Invoke(this, type, source, state, actors, data); } diff --git a/Glamourer/Events/StateFinalized.cs b/Glamourer/Events/StateFinalized.cs new file mode 100644 index 0000000..0ccaa8b --- /dev/null +++ b/Glamourer/Events/StateFinalized.cs @@ -0,0 +1,24 @@ +using Glamourer.Api; +using Glamourer.Api.Enums; +using Glamourer.Interop.Structs; +using OtterGui.Classes; +using Penumbra.GameData.Interop; + +namespace Glamourer.Events; + +/// +/// Triggered when a set of grouped changes finishes being applied to a Glamourer state. +/// +/// Parameter is the operation that finished updating the saved state. +/// Parameter is the existing actors using this saved state. +/// +/// +public sealed class StateFinalized() + : EventWrapper(nameof(StateFinalized)) +{ + public enum Priority + { + /// + StateApi = int.MinValue, + } +} diff --git a/Glamourer/Events/TabSelected.cs b/Glamourer/Events/TabSelected.cs index f43fee0..82a6abb 100644 --- a/Glamourer/Events/TabSelected.cs +++ b/Glamourer/Events/TabSelected.cs @@ -1,5 +1,4 @@ -using System; -using Glamourer.Designs; +using Glamourer.Designs; using Glamourer.Gui; using OtterGui.Classes; @@ -12,8 +11,8 @@ namespace Glamourer.Events; /// Parameter is the design to select if the tab is the designs tab. /// /// -public sealed class TabSelected : EventWrapper, - TabSelected.Priority> +public sealed class TabSelected() + : EventWrapper(nameof(TabSelected)) { public enum Priority { @@ -23,11 +22,4 @@ public sealed class TabSelected : EventWrapper MainWindow = 1, } - - public TabSelected() - : base(nameof(TabSelected)) - { } - - public void Invoke(MainWindow.TabType type, Design? design) - => Invoke(this, type, design); } diff --git a/Glamourer/Events/VieraEarStateChanged.cs b/Glamourer/Events/VieraEarStateChanged.cs new file mode 100644 index 0000000..65730b8 --- /dev/null +++ b/Glamourer/Events/VieraEarStateChanged.cs @@ -0,0 +1,22 @@ +using OtterGui.Classes; +using Penumbra.GameData.Interop; + +namespace Glamourer.Events; + +/// +/// Triggered when the state of viera ear visibility for any draw object is changed. +/// +/// Parameter is the model with a changed viera ear visibility state. +/// Parameter is the new state. +/// Parameter is whether to call the original function. +/// +/// +public sealed class VieraEarStateChanged() + : EventWrapperRef2(nameof(VieraEarStateChanged)) +{ + public enum Priority + { + /// + StateListener = 0, + } +} diff --git a/Glamourer/Events/VisorStateChanged.cs b/Glamourer/Events/VisorStateChanged.cs index 0cd83d1..03b7336 100644 --- a/Glamourer/Events/VisorStateChanged.cs +++ b/Glamourer/Events/VisorStateChanged.cs @@ -1,6 +1,5 @@ -using System; -using Glamourer.Interop.Structs; using OtterGui.Classes; +using Penumbra.GameData.Interop; namespace Glamourer.Events; @@ -12,22 +11,12 @@ namespace Glamourer.Events; /// Parameter is whether to call the original function. /// /// -public sealed class VisorStateChanged : EventWrapper>, VisorStateChanged.Priority> +public sealed class VisorStateChanged() + : EventWrapperRef3(nameof(VisorStateChanged)) { public enum Priority { /// StateListener = 0, } - - public VisorStateChanged() - : base(nameof(VisorStateChanged)) - { } - - public void Invoke(Model model, ref bool state) - { - var value = new Ref(state); - Invoke(this, model, value); - state = value; - } -} +} \ No newline at end of file diff --git a/Glamourer/Events/WeaponLoading.cs b/Glamourer/Events/WeaponLoading.cs index 1224e7f..fda0b2f 100644 --- a/Glamourer/Events/WeaponLoading.cs +++ b/Glamourer/Events/WeaponLoading.cs @@ -1,7 +1,6 @@ -using System; -using Glamourer.Interop.Structs; using OtterGui.Classes; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.Events; @@ -14,7 +13,8 @@ namespace Glamourer.Events; /// Parameter is the model values to change the weapon to. /// /// -public sealed class WeaponLoading : EventWrapper>, WeaponLoading.Priority> +public sealed class WeaponLoading() + : EventWrapperRef3(nameof(WeaponLoading)) { public enum Priority { @@ -24,15 +24,4 @@ public sealed class WeaponLoading : EventWrapper AutoDesignApplier = -1, } - - public WeaponLoading() - : base(nameof(WeaponLoading)) - { } - - public void Invoke(Actor actor, EquipSlot slot, ref CharacterWeapon weapon) - { - var value = new Ref(weapon); - Invoke(this, actor, slot, value); - weapon = value; - } } diff --git a/Glamourer/Events/WeaponVisibilityChanged.cs b/Glamourer/Events/WeaponVisibilityChanged.cs index 561c793..f75fa68 100644 --- a/Glamourer/Events/WeaponVisibilityChanged.cs +++ b/Glamourer/Events/WeaponVisibilityChanged.cs @@ -1,6 +1,5 @@ -using System; -using Glamourer.Interop.Structs; using OtterGui.Classes; +using Penumbra.GameData.Interop; namespace Glamourer.Events; @@ -11,22 +10,11 @@ namespace Glamourer.Events; /// Parameter is the new state. /// /// -public sealed class WeaponVisibilityChanged : EventWrapper>, WeaponVisibilityChanged.Priority> +public sealed class WeaponVisibilityChanged() : EventWrapperRef2(nameof(WeaponVisibilityChanged)) { public enum Priority { /// StateListener = 0, } - - public WeaponVisibilityChanged() - : base(nameof(WeaponVisibilityChanged)) - { } - - public void Invoke(Actor actor, ref bool state) - { - var value = new Ref(state); - Invoke(this, actor, value); - state = value; - } } diff --git a/Glamourer/GameData/ColorParameters.cs b/Glamourer/GameData/ColorParameters.cs new file mode 100644 index 0000000..1942804 --- /dev/null +++ b/Glamourer/GameData/ColorParameters.cs @@ -0,0 +1,54 @@ +using Dalamud.Plugin.Services; +using Penumbra.String.Functions; + +namespace Glamourer.GameData; + +/// Parse the Human.cmp file as a list of 4-byte integer values to obtain colors. +public class ColorParameters : IReadOnlyList +{ + private readonly uint[] _rgbaColors; + + /// Get a slice of the colors starting at and containing colors. + public ReadOnlySpan GetSlice(int offset, int count) + => _rgbaColors.AsSpan(offset, count); + + public unsafe ColorParameters(IDataManager gameData, IPluginLog log) + { + try + { + var file = gameData.GetFile("chara/xls/charamake/human.cmp")!; + // Just copy all the data into an uint array. + _rgbaColors = new uint[file.Data.Length >> 2]; + fixed (byte* ptr1 = file.Data) + { + fixed (uint* ptr2 = _rgbaColors) + { + MemoryUtility.MemCpyUnchecked(ptr2, ptr1, file.Data.Length); + } + } + } + catch (Exception e) + { + log.Error("READ THIS\n======== Could not obtain the human.cmp file which is necessary for color sets.\n" + + "======== This usually indicates an error with your index files caused by TexTools modifications.\n" + + "======== If you have used TexTools before, you will probably need to start over in it to use Glamourer.", e); + _rgbaColors = []; + } + } + + /// + public IEnumerator GetEnumerator() + => (IEnumerator)_rgbaColors.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + /// + public int Count + => _rgbaColors.Length; + + /// + public uint this[int index] + => _rgbaColors[index]; +} diff --git a/Glamourer.GameData/Customization/CustomizeData.cs b/Glamourer/GameData/CustomizeData.cs similarity index 51% rename from Glamourer.GameData/Customization/CustomizeData.cs rename to Glamourer/GameData/CustomizeData.cs index 8e9f914..62828ae 100644 --- a/Glamourer.GameData/Customization/CustomizeData.cs +++ b/Glamourer/GameData/CustomizeData.cs @@ -1,28 +1,36 @@ -using System; -using System.Runtime.InteropServices; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; -namespace Glamourer.Customization; +namespace Glamourer.GameData; -// Any customization value can be represented in 8 bytes by its ID, -// a byte value, an optional value-id and an optional icon or color. +/// +/// Any customization value can be represented in 8 bytes by its ID, +/// a byte value, an optional value-id and an optional icon or color. +/// [StructLayout(LayoutKind.Explicit)] public readonly struct CustomizeData : IEquatable { + /// The index of the option this value is for. [FieldOffset(0)] public readonly CustomizeIndex Index; + /// The value for the option. [FieldOffset(1)] public readonly CustomizeValue Value; + /// The internal ID for sheets. [FieldOffset(2)] public readonly ushort CustomizeId; + /// An ID for an associated icon. [FieldOffset(4)] public readonly uint IconId; + /// An ID for an associated color. [FieldOffset(4)] public readonly uint Color; + /// Construct a CustomizeData from single data values. public CustomizeData(CustomizeIndex index, CustomizeValue value, uint data = 0, ushort customizeId = 0) { Index = index; @@ -32,14 +40,23 @@ public readonly struct CustomizeData : IEquatable CustomizeId = customizeId; } + /// public bool Equals(CustomizeData other) => Index == other.Index && Value.Value == other.Value.Value && CustomizeId == other.CustomizeId; + /// public override bool Equals(object? obj) => obj is CustomizeData other && Equals(other); + /// public override int GetHashCode() => HashCode.Combine((int)Index, Value.Value, CustomizeId); + + public static bool operator ==(CustomizeData left, CustomizeData right) + => left.Equals(right); + + public static bool operator !=(CustomizeData left, CustomizeData right) + => !(left == right); } diff --git a/Glamourer/GameData/CustomizeManager.cs b/Glamourer/GameData/CustomizeManager.cs new file mode 100644 index 0000000..9e065b4 --- /dev/null +++ b/Glamourer/GameData/CustomizeManager.cs @@ -0,0 +1,96 @@ +using Dalamud.Interface.Textures; +using Dalamud.Plugin.Services; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Race = Penumbra.GameData.Enums.Race; + +namespace Glamourer.GameData; + +/// Generate everything about customization per tribe and gender. +public class CustomizeManager : IAsyncDataContainer +{ + /// All races except for Unknown + public static readonly IReadOnlyList Races = ((Race[])Enum.GetValues(typeof(Race))).Skip(1).ToArray(); + + /// All tribes except for Unknown + public static readonly IReadOnlyList Clans = ((SubRace[])Enum.GetValues(typeof(SubRace))).Skip(1).ToArray(); + + /// Two genders. + public static readonly IReadOnlyList Genders = + [ + Gender.Male, + Gender.Female, + ]; + + /// Every tribe and gender has a separate set of available customizations. + public CustomizeSet GetSet(SubRace race, Gender gender) + { + if (!Finished) + Awaiter.Wait(); + return _customizationSets[ToIndex(race, gender)]; + } + + /// Get specific icons. + public ISharedImmediateTexture GetIcon(uint id) + => _icons.TextureProvider.GetFromGameIcon(id); + + /// Iterate over all supported genders and clans. + public static IEnumerable<(SubRace Clan, Gender Gender)> AllSets() + { + foreach (var clan in Clans) + { + yield return (clan, Gender.Male); + yield return (clan, Gender.Female); + } + } + + public CustomizeManager(ITextureProvider textures, IDataManager gameData, IPluginLog log, NpcCustomizeSet npcCustomizeSet) + { + _icons = new TextureCache(gameData, textures); + var stopwatch = new Stopwatch(); + var tmpTask = Task.Run(() => + { + stopwatch.Start(); + return new CustomizeSetFactory(gameData, log, _icons, npcCustomizeSet); + }); + var setTasks = AllSets().Select(p + => tmpTask.ContinueWith(t => _customizationSets[ToIndex(p.Clan, p.Gender)] = t.Result.CreateSet(p.Clan, p.Gender))); + Awaiter = Task.WhenAll(setTasks).ContinueWith(_ => + { + // This is far too hard to estimate sensibly. + TotalCount = 0; + Memory = 0; + Time = stopwatch.ElapsedMilliseconds; + }); + } + + /// + public Task Awaiter { get; } + + /// + public bool Finished + => Awaiter.IsCompletedSuccessfully; + + private readonly TextureCache _icons; + private static readonly int ListSize = Clans.Count * Genders.Count; + private readonly CustomizeSet[] _customizationSets = new CustomizeSet[ListSize]; + + /// Get the index for the given pair of tribe and gender. + private static int ToIndex(SubRace race, Gender gender) + { + var idx = ((int)race - 1) * Genders.Count + (gender == Gender.Female ? 1 : 0); + if (idx < 0 || idx >= ListSize) + throw new Exception($"Invalid customization requested for {race} {gender}."); + + return idx; + } + + public long Time { get; private set; } + public long Memory { get; private set; } + + public string Name + => nameof(CustomizeManager); + + public int TotalCount { get; private set; } +} diff --git a/Glamourer/GameData/CustomizeParameterData.cs b/Glamourer/GameData/CustomizeParameterData.cs new file mode 100644 index 0000000..3a04938 --- /dev/null +++ b/Glamourer/GameData/CustomizeParameterData.cs @@ -0,0 +1,303 @@ +using FFXIVClientStructs.FFXIV.Shader; + +namespace Glamourer.GameData; + +public struct CustomizeParameterData +{ + public Vector4 DecalColor; + public Vector4 LipDiffuse; + public Vector3 SkinDiffuse; + public Vector3 SkinSpecular; + public Vector3 HairDiffuse; + public Vector3 HairSpecular; + public Vector3 HairHighlight; + public Vector3 LeftEye; + public float LeftLimbalIntensity; + public Vector3 RightEye; + public float RightLimbalIntensity; + public Vector3 FeatureColor; + public float FacePaintUvMultiplier; + public float FacePaintUvOffset; + public float MuscleTone; + + public CustomizeParameterValue this[CustomizeParameterFlag flag] + { + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + readonly get + { + return flag switch + { + CustomizeParameterFlag.SkinDiffuse => new CustomizeParameterValue(SkinDiffuse), + CustomizeParameterFlag.MuscleTone => new CustomizeParameterValue(MuscleTone), + CustomizeParameterFlag.SkinSpecular => new CustomizeParameterValue(SkinSpecular), + CustomizeParameterFlag.LipDiffuse => new CustomizeParameterValue(LipDiffuse), + CustomizeParameterFlag.HairDiffuse => new CustomizeParameterValue(HairDiffuse), + CustomizeParameterFlag.HairSpecular => new CustomizeParameterValue(HairSpecular), + CustomizeParameterFlag.HairHighlight => new CustomizeParameterValue(HairHighlight), + CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(LeftEye), + CustomizeParameterFlag.LeftLimbalIntensity => new CustomizeParameterValue(LeftLimbalIntensity), + CustomizeParameterFlag.RightEye => new CustomizeParameterValue(RightEye), + CustomizeParameterFlag.RightLimbalIntensity => new CustomizeParameterValue(RightLimbalIntensity), + CustomizeParameterFlag.FeatureColor => new CustomizeParameterValue(FeatureColor), + CustomizeParameterFlag.DecalColor => new CustomizeParameterValue(DecalColor), + CustomizeParameterFlag.FacePaintUvMultiplier => new CustomizeParameterValue(FacePaintUvMultiplier), + CustomizeParameterFlag.FacePaintUvOffset => new CustomizeParameterValue(FacePaintUvOffset), + _ => CustomizeParameterValue.Zero, + }; + } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + set => Set(flag, value); + } + + public bool Set(CustomizeParameterFlag flag, CustomizeParameterValue value) + { + return flag switch + { + CustomizeParameterFlag.SkinDiffuse => SetIfDifferent(ref SkinDiffuse, value.InternalTriple), + CustomizeParameterFlag.MuscleTone => SetIfDifferent(ref MuscleTone, value.Single), + CustomizeParameterFlag.SkinSpecular => SetIfDifferent(ref SkinSpecular, value.InternalTriple), + CustomizeParameterFlag.LipDiffuse => SetIfDifferent(ref LipDiffuse, value.InternalQuadruple), + CustomizeParameterFlag.HairDiffuse => SetIfDifferent(ref HairDiffuse, value.InternalTriple), + CustomizeParameterFlag.HairSpecular => SetIfDifferent(ref HairSpecular, value.InternalTriple), + CustomizeParameterFlag.HairHighlight => SetIfDifferent(ref HairHighlight, value.InternalTriple), + CustomizeParameterFlag.LeftEye => SetIfDifferent(ref LeftEye, value.InternalTriple), + CustomizeParameterFlag.LeftLimbalIntensity => SetIfDifferent(ref LeftLimbalIntensity, value.Single), + CustomizeParameterFlag.RightEye => SetIfDifferent(ref RightEye, value.InternalTriple), + CustomizeParameterFlag.RightLimbalIntensity => SetIfDifferent(ref RightLimbalIntensity, value.Single), + CustomizeParameterFlag.FeatureColor => SetIfDifferent(ref FeatureColor, value.InternalTriple), + CustomizeParameterFlag.DecalColor => SetIfDifferent(ref DecalColor, value.InternalQuadruple), + CustomizeParameterFlag.FacePaintUvMultiplier => SetIfDifferent(ref FacePaintUvMultiplier, value.Single), + CustomizeParameterFlag.FacePaintUvOffset => SetIfDifferent(ref FacePaintUvOffset, value.Single), + _ => false, + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public readonly void Apply(ref CustomizeParameter parameters, CustomizeParameterFlag flags = CustomizeParameterExtensions.All) + { + parameters.SkinColor = (flags & (CustomizeParameterFlag.SkinDiffuse | CustomizeParameterFlag.MuscleTone)) switch + { + 0 => parameters.SkinColor, + CustomizeParameterFlag.SkinDiffuse => new CustomizeParameterValue(SkinDiffuse, parameters.SkinColor.W).XivQuadruple, + CustomizeParameterFlag.MuscleTone => parameters.SkinColor with { W = MuscleTone }, + _ => new CustomizeParameterValue(SkinDiffuse, MuscleTone).XivQuadruple, + }; + + parameters.LeftColor = (flags & (CustomizeParameterFlag.LeftEye | CustomizeParameterFlag.LeftLimbalIntensity)) switch + { + 0 => parameters.LeftColor, + CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(LeftEye, parameters.LeftColor.W).XivQuadruple, + CustomizeParameterFlag.LeftLimbalIntensity => parameters.LeftColor with { W = LeftLimbalIntensity }, + _ => new CustomizeParameterValue(LeftEye, LeftLimbalIntensity).XivQuadruple, + }; + + parameters.RightColor = (flags & (CustomizeParameterFlag.RightEye | CustomizeParameterFlag.RightLimbalIntensity)) switch + { + 0 => parameters.RightColor, + CustomizeParameterFlag.RightEye => new CustomizeParameterValue(RightEye, parameters.RightColor.W).XivQuadruple, + CustomizeParameterFlag.RightLimbalIntensity => parameters.RightColor with { W = RightLimbalIntensity }, + _ => new CustomizeParameterValue(RightEye, RightLimbalIntensity).XivQuadruple, + }; + + if (flags.HasFlag(CustomizeParameterFlag.SkinSpecular)) + parameters.SkinFresnelValue0 = new CustomizeParameterValue(SkinSpecular).XivQuadruple; + if (flags.HasFlag(CustomizeParameterFlag.HairDiffuse)) + { + // Vector3 is 0x10 byte for some reason. + var triple = new CustomizeParameterValue(HairDiffuse).XivTriple; + parameters.MainColor.X = triple.X; + parameters.MainColor.Y = triple.Y; + parameters.MainColor.Z = triple.Z; + } + + if (flags.HasFlag(CustomizeParameterFlag.HairSpecular)) + parameters.HairFresnelValue0 = new CustomizeParameterValue(HairSpecular).XivTriple; + if (flags.HasFlag(CustomizeParameterFlag.HairHighlight)) + { + // Vector3 is 0x10 byte for some reason. + var triple = new CustomizeParameterValue(HairHighlight).XivTriple; + parameters.MeshColor.X = triple.X; + parameters.MeshColor.Y = triple.Y; + parameters.MeshColor.Z = triple.Z; + } + + if (flags.HasFlag(CustomizeParameterFlag.FacePaintUvMultiplier)) + GetUvMultiplierWrite(ref parameters) = FacePaintUvMultiplier; + if (flags.HasFlag(CustomizeParameterFlag.FacePaintUvOffset)) + GetUvOffsetWrite(ref parameters) = FacePaintUvOffset; + if (flags.HasFlag(CustomizeParameterFlag.LipDiffuse)) + parameters.LipColor = new CustomizeParameterValue(LipDiffuse).XivQuadruple; + if (flags.HasFlag(CustomizeParameterFlag.FeatureColor)) + parameters.OptionColor = new CustomizeParameterValue(FeatureColor).XivTriple; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public readonly void Apply(ref DecalParameters parameters, CustomizeParameterFlag flags = CustomizeParameterExtensions.All) + { + if (flags.HasFlag(CustomizeParameterFlag.DecalColor)) + parameters.Color = new CustomizeParameterValue(DecalColor).XivQuadruple; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public readonly void ApplySingle(ref CustomizeParameter parameters, CustomizeParameterFlag flag) + { + switch (flag) + { + case CustomizeParameterFlag.SkinDiffuse: + parameters.SkinColor = new CustomizeParameterValue(SkinDiffuse, parameters.SkinColor.W).XivQuadruple; + break; + case CustomizeParameterFlag.MuscleTone: + parameters.SkinColor.W = MuscleTone; + break; + case CustomizeParameterFlag.SkinSpecular: + parameters.SkinFresnelValue0 = new CustomizeParameterValue(SkinSpecular).XivQuadruple; + break; + case CustomizeParameterFlag.LipDiffuse: + parameters.LipColor = new CustomizeParameterValue(LipDiffuse).XivQuadruple; + break; + case CustomizeParameterFlag.HairDiffuse: + // Vector3 is 0x10 byte for some reason. + var triple1 = new CustomizeParameterValue(HairDiffuse).XivTriple; + parameters.MainColor.X = triple1.X; + parameters.MainColor.Y = triple1.Y; + parameters.MainColor.Z = triple1.Z; + break; + case CustomizeParameterFlag.HairSpecular: + parameters.HairFresnelValue0 = new CustomizeParameterValue(HairSpecular).XivTriple; + break; + case CustomizeParameterFlag.HairHighlight: + // Vector3 is 0x10 byte for some reason. + var triple2 = new CustomizeParameterValue(HairHighlight).XivTriple; + parameters.MeshColor.X = triple2.X; + parameters.MeshColor.Y = triple2.Y; + parameters.MeshColor.Z = triple2.Z; + break; + case CustomizeParameterFlag.LeftEye: + parameters.LeftColor = new CustomizeParameterValue(LeftEye, parameters.LeftColor.W).XivQuadruple; + break; + case CustomizeParameterFlag.RightEye: + parameters.RightColor = new CustomizeParameterValue(RightEye, parameters.RightColor.W).XivQuadruple; + break; + case CustomizeParameterFlag.FeatureColor: + parameters.OptionColor = new CustomizeParameterValue(FeatureColor).XivTriple; + break; + case CustomizeParameterFlag.FacePaintUvMultiplier: + GetUvMultiplierWrite(ref parameters) = FacePaintUvMultiplier; + break; + case CustomizeParameterFlag.FacePaintUvOffset: + GetUvOffsetWrite(ref parameters) = FacePaintUvOffset; + break; + case CustomizeParameterFlag.LeftLimbalIntensity: + parameters.LeftColor.W = LeftLimbalIntensity; + break; + case CustomizeParameterFlag.RightLimbalIntensity: + parameters.RightColor.W = RightLimbalIntensity; + break; + } + } + + public static CustomizeParameterData FromParameters(in CustomizeParameter parameter, in DecalParameters decal) + => new() + { + FacePaintUvOffset = GetUvOffset(parameter), + FacePaintUvMultiplier = GetUvMultiplier(parameter), + MuscleTone = parameter.SkinColor.W, + SkinDiffuse = new CustomizeParameterValue(parameter.SkinColor).InternalTriple, + SkinSpecular = new CustomizeParameterValue(parameter.SkinFresnelValue0).InternalTriple, + LipDiffuse = new CustomizeParameterValue(parameter.LipColor).InternalQuadruple, + HairDiffuse = new CustomizeParameterValue(parameter.MainColor).InternalTriple, + HairSpecular = new CustomizeParameterValue(parameter.HairFresnelValue0).InternalTriple, + HairHighlight = new CustomizeParameterValue(parameter.MeshColor).InternalTriple, + LeftEye = new CustomizeParameterValue(parameter.LeftColor).InternalTriple, + LeftLimbalIntensity = new CustomizeParameterValue(parameter.LeftColor.W).Single, + RightEye = new CustomizeParameterValue(parameter.RightColor).InternalTriple, + RightLimbalIntensity = new CustomizeParameterValue(parameter.RightColor.W).Single, + FeatureColor = new CustomizeParameterValue(parameter.OptionColor).InternalTriple, + DecalColor = FromParameter(decal), + }; + + public static CustomizeParameterValue FromParameter(in CustomizeParameter parameter, CustomizeParameterFlag flag) + => flag switch + { + CustomizeParameterFlag.SkinDiffuse => new CustomizeParameterValue(parameter.SkinColor), + CustomizeParameterFlag.MuscleTone => new CustomizeParameterValue(parameter.SkinColor.W), + CustomizeParameterFlag.SkinSpecular => new CustomizeParameterValue(parameter.SkinFresnelValue0), + CustomizeParameterFlag.LipDiffuse => new CustomizeParameterValue(parameter.LipColor), + CustomizeParameterFlag.HairDiffuse => new CustomizeParameterValue(parameter.MainColor), + CustomizeParameterFlag.HairSpecular => new CustomizeParameterValue(parameter.HairFresnelValue0), + CustomizeParameterFlag.HairHighlight => new CustomizeParameterValue(parameter.MeshColor), + CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(parameter.LeftColor), + CustomizeParameterFlag.RightEye => new CustomizeParameterValue(parameter.RightColor), + CustomizeParameterFlag.FeatureColor => new CustomizeParameterValue(parameter.OptionColor), + CustomizeParameterFlag.FacePaintUvMultiplier => new CustomizeParameterValue(GetUvMultiplier(parameter)), + CustomizeParameterFlag.FacePaintUvOffset => new CustomizeParameterValue(GetUvOffset(parameter)), + _ => CustomizeParameterValue.Zero, + }; + + public static Vector4 FromParameter(in DecalParameters parameter) + => new CustomizeParameterValue(parameter.Color).InternalQuadruple; + + private static bool SetIfDifferent(ref Vector3 val, Vector3 @new) + { + if (@new == val) + return false; + + val = @new; + return true; + } + + private static bool SetIfDifferent(ref float val, float @new) + { + if (@new == val) + return false; + + val = @new; + return true; + } + + private static bool SetIfDifferent(ref Vector4 val, Vector4 @new) + { + if (@new == val) + return false; + + val = @new; + return true; + } + + + private static unsafe float GetUvOffset(in CustomizeParameter parameter) + { + // TODO CS Update + fixed (CustomizeParameter* ptr = ¶meter) + { + return ((float*)ptr)[23]; + } + } + + private static unsafe ref float GetUvOffsetWrite(ref CustomizeParameter parameter) + { + // TODO CS Update + fixed (CustomizeParameter* ptr = ¶meter) + { + return ref ((float*)ptr)[23]; + } + } + + private static unsafe float GetUvMultiplier(in CustomizeParameter parameter) + { + // TODO CS Update + fixed (CustomizeParameter* ptr = ¶meter) + { + return ((float*)ptr)[15]; + } + } + + private static unsafe ref float GetUvMultiplierWrite(ref CustomizeParameter parameter) + { + // TODO CS Update + fixed (CustomizeParameter* ptr = ¶meter) + { + return ref ((float*)ptr)[15]; + } + } +} diff --git a/Glamourer/GameData/CustomizeParameterFlag.cs b/Glamourer/GameData/CustomizeParameterFlag.cs new file mode 100644 index 0000000..ff804d4 --- /dev/null +++ b/Glamourer/GameData/CustomizeParameterFlag.cs @@ -0,0 +1,74 @@ +namespace Glamourer.GameData; + +[Flags] +public enum CustomizeParameterFlag : ushort +{ + SkinDiffuse = 0x0001, + MuscleTone = 0x0002, + SkinSpecular = 0x0004, + LipDiffuse = 0x0008, + HairDiffuse = 0x0010, + HairSpecular = 0x0020, + HairHighlight = 0x0040, + LeftEye = 0x0080, + RightEye = 0x0100, + FeatureColor = 0x0200, + FacePaintUvMultiplier = 0x0400, + FacePaintUvOffset = 0x0800, + DecalColor = 0x1000, + LeftLimbalIntensity = 0x2000, + RightLimbalIntensity = 0x4000, +} + +public static class CustomizeParameterExtensions +{ + // Speculars are not available anymore. + public const CustomizeParameterFlag All = (CustomizeParameterFlag)0x7FDB; + + public const CustomizeParameterFlag RgbTriples = All + & ~(RgbaQuadruples | Percentages | Values); + + public const CustomizeParameterFlag RgbaQuadruples = CustomizeParameterFlag.DecalColor | CustomizeParameterFlag.LipDiffuse; + + public const CustomizeParameterFlag Percentages = CustomizeParameterFlag.MuscleTone + | CustomizeParameterFlag.LeftLimbalIntensity + | CustomizeParameterFlag.RightLimbalIntensity; + + public const CustomizeParameterFlag Values = CustomizeParameterFlag.FacePaintUvOffset | CustomizeParameterFlag.FacePaintUvMultiplier; + + public static readonly IReadOnlyList AllFlags = [.. Enum.GetValues().Where(f => All.HasFlag(f))]; + public static readonly IReadOnlyList RgbaFlags = AllFlags.Where(f => RgbaQuadruples.HasFlag(f)).ToArray(); + public static readonly IReadOnlyList RgbFlags = AllFlags.Where(f => RgbTriples.HasFlag(f)).ToArray(); + public static readonly IReadOnlyList PercentageFlags = AllFlags.Where(f => Percentages.HasFlag(f)).ToArray(); + public static readonly IReadOnlyList ValueFlags = AllFlags.Where(f => Values.HasFlag(f)).ToArray(); + + public static int Count(this CustomizeParameterFlag flag) + => RgbaQuadruples.HasFlag(flag) ? 4 : RgbTriples.HasFlag(flag) ? 3 : 1; + + public static IEnumerable Iterate(this CustomizeParameterFlag flags) + => AllFlags.Where(f => flags.HasFlag(f)); + + public static int ToInternalIndex(this CustomizeParameterFlag flag) + => BitOperations.TrailingZeroCount((uint)flag); + + public static string ToName(this CustomizeParameterFlag flag) + => flag switch + { + CustomizeParameterFlag.SkinDiffuse => "Skin Color", + CustomizeParameterFlag.MuscleTone => "Muscle Tone", + CustomizeParameterFlag.SkinSpecular => "Skin Shine", + CustomizeParameterFlag.LipDiffuse => "Lip Color", + CustomizeParameterFlag.HairDiffuse => "Hair Color", + CustomizeParameterFlag.HairSpecular => "Hair Shine", + CustomizeParameterFlag.HairHighlight => "Hair Highlights", + CustomizeParameterFlag.LeftEye => "Left Eye Color", + CustomizeParameterFlag.RightEye => "Right Eye Color", + CustomizeParameterFlag.FeatureColor => "Feature Color", + CustomizeParameterFlag.FacePaintUvMultiplier => "Multiplier for Face Paint", + CustomizeParameterFlag.FacePaintUvOffset => "Offset of Face Paint", + CustomizeParameterFlag.DecalColor => "Face Paint Color", + CustomizeParameterFlag.LeftLimbalIntensity => "Left Limbal Ring Intensity", + CustomizeParameterFlag.RightLimbalIntensity => "Right Limbal Ring Intensity", + _ => string.Empty, + }; +} diff --git a/Glamourer/GameData/CustomizeParameterValue.cs b/Glamourer/GameData/CustomizeParameterValue.cs new file mode 100644 index 0000000..87ab851 --- /dev/null +++ b/Glamourer/GameData/CustomizeParameterValue.cs @@ -0,0 +1,72 @@ +namespace Glamourer.GameData; + +public readonly struct CustomizeParameterValue +{ + public static readonly CustomizeParameterValue Zero = default; + + private readonly Vector4 _data; + + public CustomizeParameterValue(Vector4 data) + => _data = data; + + public CustomizeParameterValue(Vector3 data, float w = 0) + => _data = new Vector4(data, w); + + public CustomizeParameterValue(FFXIVClientStructs.FFXIV.Common.Math.Vector4 data) + => _data = new Vector4(Root(data.X), Root(data.Y), Root(data.Z), data.W); + + public CustomizeParameterValue(FFXIVClientStructs.FFXIV.Common.Math.Vector3 data) + => _data = new Vector4(Root(data.X), Root(data.Y), Root(data.Z), 0); + + public CustomizeParameterValue(float value, float y = 0, float z = 0, float w = 0) + => _data = new Vector4(value, y, z, w); + + public Vector3 InternalTriple + => new(_data.X, _data.Y, _data.Z); + + public float Single + => _data.X; + + public Vector4 InternalQuadruple + => _data; + + public FFXIVClientStructs.FFXIV.Common.Math.Vector4 XivQuadruple + => new(Square(_data.X), Square(_data.Y), Square(_data.Z), _data.W); + + public FFXIVClientStructs.FFXIV.Common.Math.Vector3 XivTriple + => new(Square(_data.X), Square(_data.Y), Square(_data.Z)); + + private static float Square(float x) + => x < 0 ? -x * x : x * x; + + private static float Root(float x) + => x < 0 ? -(float)Math.Sqrt(-x) : (float)Math.Sqrt(x); + + public float this[int idx] + => _data[idx]; + + public override string ToString() + => _data.ToString(); +} + +public static class VectorExtensions +{ + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static bool NearEqual(this Vector3 lhs, Vector3 rhs, float eps = 1e-9f) + => (lhs - rhs).LengthSquared() < eps; + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static bool NearEqual(this Vector4 lhs, Vector4 rhs, float eps = 1e-9f) + => (lhs - rhs).LengthSquared() < eps; + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static bool NearEqual(this CustomizeParameterValue lhs, CustomizeParameterValue rhs, float eps = 1e-9f) + => NearEqual(lhs.InternalQuadruple, rhs.InternalQuadruple, eps); + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static bool NearEqual(this float lhs, float rhs, float eps = 1e-5f) + { + var diff = lhs - rhs; + return diff < 0 ? diff > -eps : diff < eps; + } +} diff --git a/Glamourer.GameData/Customization/CustomizationSet.cs b/Glamourer/GameData/CustomizeSet.cs similarity index 78% rename from Glamourer.GameData/Customization/CustomizationSet.cs rename to Glamourer/GameData/CustomizeSet.cs index b958fdc..8795c19 100644 --- a/Glamourer.GameData/Customization/CustomizationSet.cs +++ b/Glamourer/GameData/CustomizeSet.cs @@ -1,28 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using OtterGui; +using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Race = Penumbra.GameData.Enums.Race; -namespace Glamourer.Customization; +namespace Glamourer.GameData; -// Each Subrace and Gender combo has a customization set. -// This describes the available customizations, their types and their names. -public class CustomizationSet +/// +/// Each SubRace and Gender combo has a customization set. +/// This describes the available customizations, their types and their names. +/// +public class CustomizeSet { - internal CustomizationSet(SubRace clan, Gender gender) + private readonly NpcCustomizeSet _npcCustomizations; + + internal CustomizeSet(NpcCustomizeSet npcCustomizations, SubRace clan, Gender gender) { - Gender = gender; - Clan = clan; - Race = clan.ToRace(); - SettingAvailable = 0; + _npcCustomizations = npcCustomizations; + Gender = gender; + Clan = clan; + Race = clan.ToRace(); + SettingAvailable = 0; } public Gender Gender { get; } public SubRace Clan { get; } public Race Race { get; } + public string Name { get; internal init; } = string.Empty; + public CustomizeFlag SettingAvailable { get; internal set; } internal void SetAvailable(CustomizeIndex index) @@ -32,14 +38,14 @@ public class CustomizationSet => SettingAvailable.HasFlag(index.ToFlag()); // Meta - public IReadOnlyList OptionName { get; internal set; } = null!; + public IReadOnlyList OptionName { get; internal init; } = null!; public string Option(CustomizeIndex index) => OptionName[(int)index]; - public IReadOnlyList Voices { get; internal init; } = null!; - public IReadOnlyList Types { get; internal set; } = null!; - public IReadOnlyDictionary Order { get; internal set; } = null!; + public IReadOnlyList Voices { get; internal init; } = null!; + public IReadOnlyList Types { get; internal set; } = null!; + public IReadOnlyDictionary Order { get; internal set; } = null!; // Always list selector. @@ -83,6 +89,7 @@ public class CustomizationSet { if (IsAvailable(index)) return DataByValue(index, value, out custom, face) >= 0 + || _npcCustomizations.CheckValue(index, value) || NpcOptions.Any(t => t.Type == index && t.Value == value); custom = null; @@ -94,12 +101,76 @@ public class CustomizationSet { var type = Types[(int)index]; - int GetInteger0(out CustomizeData? custom) + return type switch { - if (value < Count(index)) + MenuType.ListSelector => GetInteger0(out custom), + MenuType.List1Selector => GetInteger1(out custom), + MenuType.IconSelector => index switch { - custom = new CustomizeData(index, value, 0, value.Value); - return value.Value; + CustomizeIndex.Face => Get(Faces, HrothgarFaceHack(value), out custom), + CustomizeIndex.Hairstyle => Get((face = HrothgarFaceHack(face)).Value < HairByFace.Count ? HairByFace[face.Value] : HairStyles, + value, out custom), + CustomizeIndex.TailShape => Get(TailEarShapes, value, out custom), + CustomizeIndex.FacePaint => Get(FacePaints, value, out custom), + CustomizeIndex.LipColor => Get(LipColorsDark, value, out custom), + _ => Invalid(out custom), + }, + MenuType.ColorPicker => index switch + { + CustomizeIndex.SkinColor => Get(SkinColors, value, out custom), + CustomizeIndex.EyeColorLeft => Get(EyeColors, value, out custom), + CustomizeIndex.EyeColorRight => Get(EyeColors, value, out custom), + CustomizeIndex.HairColor => Get(HairColors, value, out custom), + CustomizeIndex.HighlightsColor => Get(HighlightColors, value, out custom), + CustomizeIndex.TattooColor => Get(TattooColors, value, out custom), + CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom), + CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom), + _ => Invalid(out custom), + }, + MenuType.DoubleColorPicker => index switch + { + CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom), + CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom), + _ => Invalid(out custom), + }, + MenuType.IconCheckmark => GetBool(index, value, out custom), + MenuType.Percentage => GetInteger0(out custom), + MenuType.Checkmark => GetBool(index, value, out custom), + _ => Invalid(out custom), + }; + + int Get(IEnumerable list, CustomizeValue v, out CustomizeData? output) + { + var (val, idx) = list.Cast().WithIndex().FirstOrDefault(p => p.Value!.Value.Value == v); + if (val == null) + { + output = null; + return -1; + } + + output = val; + return idx; + } + + static int Invalid(out CustomizeData? custom) + { + custom = null; + return -1; + } + + static int GetBool(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom) + { + if (value == CustomizeValue.Zero) + { + custom = new CustomizeData(index, CustomizeValue.Zero); + return 0; + } + + var (_, mask) = index.ToByteAndMask(); + if (value.Value == mask) + { + custom = new CustomizeData(index, new CustomizeValue(mask), 0, 1); + return 1; } custom = null; @@ -118,81 +189,17 @@ public class CustomizationSet return -1; } - static int GetBool(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom) + int GetInteger0(out CustomizeData? custom) { - if (value == CustomizeValue.Zero) + if (value < Count(index)) { - custom = new CustomizeData(index, CustomizeValue.Zero, 0, 0); - return 0; - } - - var (_, mask) = index.ToByteAndMask(); - if (value.Value == mask) - { - custom = new CustomizeData(index, new CustomizeValue(mask), 0, 1); - return 1; + custom = new CustomizeData(index, value, 0, value.Value); + return value.Value; } custom = null; return -1; } - - static int Invalid(out CustomizeData? custom) - { - custom = null; - return -1; - } - - int Get(IEnumerable list, CustomizeValue v, out CustomizeData? output) - { - var (val, idx) = list.Cast().WithIndex().FirstOrDefault(p => p.Item1!.Value.Value == v); - if (val == null) - { - output = null; - return -1; - } - - output = val; - return idx; - } - - return type switch - { - CharaMakeParams.MenuType.ListSelector => GetInteger0(out custom), - CharaMakeParams.MenuType.List1Selector => GetInteger1(out custom), - CharaMakeParams.MenuType.IconSelector => index switch - { - CustomizeIndex.Face => Get(Faces, HrothgarFaceHack(value), out custom), - CustomizeIndex.Hairstyle => Get((face = HrothgarFaceHack(face)).Value < HairByFace.Count ? HairByFace[face.Value] : HairStyles, - value, out custom), - CustomizeIndex.TailShape => Get(TailEarShapes, value, out custom), - CustomizeIndex.FacePaint => Get(FacePaints, value, out custom), - CustomizeIndex.LipColor => Get(LipColorsDark, value, out custom), - _ => Invalid(out custom), - }, - CharaMakeParams.MenuType.ColorPicker => index switch - { - CustomizeIndex.SkinColor => Get(SkinColors, value, out custom), - CustomizeIndex.EyeColorLeft => Get(EyeColors, value, out custom), - CustomizeIndex.EyeColorRight => Get(EyeColors, value, out custom), - CustomizeIndex.HairColor => Get(HairColors, value, out custom), - CustomizeIndex.HighlightsColor => Get(HighlightColors, value, out custom), - CustomizeIndex.TattooColor => Get(TattooColors, value, out custom), - CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom), - CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom), - _ => Invalid(out custom), - }, - CharaMakeParams.MenuType.DoubleColorPicker => index switch - { - CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom), - CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom), - _ => Invalid(out custom), - }, - CharaMakeParams.MenuType.IconCheckmark => GetBool(index, value, out custom), - CharaMakeParams.MenuType.Percentage => GetInteger0(out custom), - CharaMakeParams.MenuType.Checkmark => GetBool(index, value, out custom), - _ => Invalid(out custom), - }; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] @@ -207,10 +214,10 @@ public class CustomizationSet switch (Types[(int)index]) { - case CharaMakeParams.MenuType.Percentage: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx); - case CharaMakeParams.MenuType.ListSelector: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx); - case CharaMakeParams.MenuType.List1Selector: return new CustomizeData(index, (CustomizeValue)(idx + 1), 0, (ushort)idx); - case CharaMakeParams.MenuType.Checkmark: return new CustomizeData(index, CustomizeValue.Bool(idx != 0), 0, (ushort)idx); + case MenuType.Percentage: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx); + case MenuType.ListSelector: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx); + case MenuType.List1Selector: return new CustomizeData(index, (CustomizeValue)(idx + 1), 0, (ushort)idx); + case MenuType.Checkmark: return new CustomizeData(index, CustomizeValue.Bool(idx != 0), 0, (ushort)idx); } return index switch @@ -240,22 +247,9 @@ public class CustomizationSet } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public CharaMakeParams.MenuType Type(CustomizeIndex index) + public MenuType Type(CustomizeIndex index) => Types[(int)index]; - internal static IReadOnlyDictionary ComputeOrder(CustomizationSet set) - { - var ret = Enum.GetValues().ToArray(); - ret[(int)CustomizeIndex.TattooColor] = CustomizeIndex.EyeColorLeft; - ret[(int)CustomizeIndex.EyeColorLeft] = CustomizeIndex.EyeColorRight; - ret[(int)CustomizeIndex.EyeColorRight] = CustomizeIndex.TattooColor; - - var dict = ret.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray()); - foreach (var type in Enum.GetValues()) - dict.TryAdd(type, Array.Empty()); - return dict; - } - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public int Count(CustomizeIndex index) => Count(index, CustomizeValue.Zero); @@ -268,9 +262,9 @@ public class CustomizationSet return Type(index) switch { - CharaMakeParams.MenuType.Percentage => 101, - CharaMakeParams.MenuType.IconCheckmark => 2, - CharaMakeParams.MenuType.Checkmark => 2, + MenuType.Percentage => 101, + MenuType.IconCheckmark => 2, + MenuType.Checkmark => 2, _ => index switch { CustomizeIndex.Face => Faces.Count, @@ -300,3 +294,10 @@ public class CustomizationSet private CustomizeValue HrothgarFaceHack(CustomizeValue value) => Race == Race.Hrothgar && value.Value is > 4 and < 9 ? value - 4 : value; } + +public static class CustomizationSetExtensions +{ + /// Return only the available customizations in this set and Clan or Gender. + public static CustomizeFlag FixApplication(this CustomizeFlag flag, CustomizeSet set) + => flag & (set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.BodyType); +} diff --git a/Glamourer/GameData/CustomizeSetFactory.cs b/Glamourer/GameData/CustomizeSetFactory.cs new file mode 100644 index 0000000..77a6973 --- /dev/null +++ b/Glamourer/GameData/CustomizeSetFactory.cs @@ -0,0 +1,446 @@ +using Dalamud.Game; +using Dalamud.Plugin.Services; +using Lumina.Excel; +using Lumina.Excel.Sheets; +using OtterGui.Classes; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Race = Penumbra.GameData.Enums.Race; + +namespace Glamourer.GameData; + +internal class CustomizeSetFactory( + IDataManager _gameData, + IPluginLog _log, + TextureCache _icons, + NpcCustomizeSet _npcCustomizeSet, + ColorParameters _colors) +{ + public CustomizeSetFactory(IDataManager gameData, IPluginLog log, TextureCache icons, NpcCustomizeSet npcCustomizeSet) + : this(gameData, log, icons, npcCustomizeSet, new ColorParameters(gameData, log)) + { } + + /// Create the set of all available customization options for a given clan and gender. + public CustomizeSet CreateSet(SubRace race, Gender gender) + { + var (skin, hair) = GetSkinHairColors(race, gender); + var row = _charaMakeSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; + var hrothgar = race.ToRace() == Race.Hrothgar; + // Create the initial set with all the easily accessible parameters available for anyone. + var set = new CustomizeSet(_npcCustomizeSet, race, gender) + { + Name = GetName(race, gender), + Voices = row.VoiceStruct, + HairStyles = GetHairStyles(race, gender), + HairColors = hair, + SkinColors = skin, + EyeColors = _eyeColorPicker, + HighlightColors = _highlightPicker, + TattooColors = _tattooColorPicker, + LipColorsDark = hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark, + LipColorsLight = hrothgar ? [] : _lipColorPickerLight, + FacePaintColorsDark = _facePaintColorPickerDark, + FacePaintColorsLight = _facePaintColorPickerLight, + Faces = GetFaces(row), + NumEyebrows = GetListSize(row, CustomizeIndex.Eyebrows), + NumEyeShapes = GetListSize(row, CustomizeIndex.EyeShape), + NumNoseShapes = GetListSize(row, CustomizeIndex.Nose), + NumJawShapes = GetListSize(row, CustomizeIndex.Jaw), + NumMouthShapes = GetListSize(row, CustomizeIndex.Mouth), + FacePaints = GetFacePaints(race, gender), + TailEarShapes = GetTailEarShapes(row), + OptionName = GetOptionNames(row), + Types = GetMenuTypes(row), + }; + SetPostProcessing(set, row); + return set; + } + + /// Some data can not be set independently of the rest, so we need a post-processing step to finalize. + private void SetPostProcessing(CustomizeSet set, in CharaMakeType row) + { + SetAvailability(set, row); + SetFacialFeatures(set, row); + SetHairByFace(set); + SetNpcData(set, set.Clan, set.Gender); + SetOrder(set); + } + + /// Given a customize set with filled data, find all customizations used by valid NPCs that are not regularly available. + private void SetNpcData(CustomizeSet set, SubRace race, Gender gender) + { + var customizeIndices = new[] + { + CustomizeIndex.Face, + CustomizeIndex.Hairstyle, + CustomizeIndex.LipColor, + CustomizeIndex.SkinColor, + CustomizeIndex.TailShape, + }; + + var npcCustomizations = new HashSet<(CustomizeIndex, CustomizeValue)>() + { + (CustomizeIndex.Height, CustomizeValue.Max), + }; + _npcCustomizeSet.Awaiter.Wait(); + foreach (var customize in _npcCustomizeSet.Select(s => s.Customize) + .Where(c => c.Clan == race && c.Gender == gender && c.BodyType.Value == 1)) + { + foreach (var customizeIndex in customizeIndices) + { + var value = customize[customizeIndex]; + if (value == CustomizeValue.Zero) + continue; + + if (set.DataByValue(customizeIndex, value, out _, customize.Face) >= 0) + continue; + + npcCustomizations.Add((customizeIndex, value)); + } + } + + set.NpcOptions = npcCustomizations.OrderBy(p => p.Item1).ThenBy(p => p.Item2.Value).ToArray(); + } + + private readonly ColorParameters _colorParameters = new(_gameData, _log); + private readonly ExcelSheet _customizeSheet = _gameData.GetExcelSheet(ClientLanguage.English); + private readonly ExcelSheet _lobbySheet = _gameData.GetExcelSheet(ClientLanguage.English); + private readonly ExcelSheet _hairSheet = _gameData.GetExcelSheet(ClientLanguage.English, "HairMakeType"); + private readonly ExcelSheet _tribeSheet = _gameData.GetExcelSheet(ClientLanguage.English); + + // Those color pickers are shared between all races. + private readonly CustomizeData[] _highlightPicker = CreateColors(_colors, CustomizeIndex.HighlightsColor, 256, 192); + private readonly CustomizeData[] _lipColorPickerDark = CreateColors(_colors, CustomizeIndex.LipColor, 512, 96); + private readonly CustomizeData[] _lipColorPickerLight = CreateColors(_colors, CustomizeIndex.LipColor, 1024, 96, true); + private readonly CustomizeData[] _eyeColorPicker = CreateColors(_colors, CustomizeIndex.EyeColorLeft, 0, 192); + private readonly CustomizeData[] _facePaintColorPickerDark = CreateColors(_colors, CustomizeIndex.FacePaintColor, 640, 96); + private readonly CustomizeData[] _facePaintColorPickerLight = CreateColors(_colors, CustomizeIndex.FacePaintColor, 1152, 96, true); + private readonly CustomizeData[] _tattooColorPicker = CreateColors(_colors, CustomizeIndex.TattooColor, 0, 192); + + private readonly ExcelSheet _charaMakeSheet = _gameData.Excel.GetSheet(); + + /// Obtain available skin and hair colors for the given clan and gender. + private (CustomizeData[] Skin, CustomizeData[] Hair) GetSkinHairColors(SubRace race, Gender gender) + { + if (race is > SubRace.Veena or SubRace.Unknown) + throw new ArgumentOutOfRangeException(nameof(race), race, null); + + var gv = gender == Gender.Male ? 0 : 1; + var idx = ((int)race * 2 + gv) * 5 + 3; + + return (CreateColors(_colorParameters, CustomizeIndex.SkinColor, idx << 8, 192), + CreateColors(_colorParameters, CustomizeIndex.HairColor, (idx + 1) << 8, 192)); + } + + /// Obtain the gender-specific clan name. + private string GetName(SubRace race, Gender gender) + => gender switch + { + Gender.Male => _tribeSheet.TryGetRow((uint)race, out var row) ? row.Masculine.ExtractText() : race.ToName(), + Gender.Female => _tribeSheet.TryGetRow((uint)race, out var row) ? row.Feminine.ExtractText() : race.ToName(), + _ => "Unknown", + }; + + /// Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender. + private CustomizeData[] GetHairStyles(SubRace race, Gender gender) + { + var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender); + // Unknown30 is the number of available hairstyles. + var numHairs = row.ReadUInt8Column(30); + var hairList = new List(numHairs); + // Hairstyles can be found starting at Unknown66. + for (var i = 0; i < numHairs; ++i) + { + // Hairs start at Unknown66. + var customizeIdx = row.ReadUInt32Column(66 + i * 9); + if (customizeIdx == uint.MaxValue) + continue; + + // Hair Row from CustomizeSheet might not be set in case of unlockable hair. + if (!_customizeSheet.TryGetRow(customizeIdx, out var hairRow)) + hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)i, customizeIdx)); + else if (_icons.IconExists(hairRow.Icon)) + hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon, + (ushort)hairRow.RowId)); + } + + return [.. hairList.OrderBy(h => h.Value.Value)]; + } + + /// Specific icons for tails or ears. + private CustomizeData[] GetTailEarShapes(CharaMakeType row) + => ExtractValues(row, CustomizeIndex.TailShape); + + /// Specific icons for faces. + private CustomizeData[] GetFaces(CharaMakeType row) + => ExtractValues(row, CustomizeIndex.Face); + + /// Specific icons for Hrothgar patterns. + private CustomizeData[] HrothgarFurPattern(CharaMakeType row) + => ExtractValues(row, CustomizeIndex.LipColor); + + private CustomizeData[] ExtractValues(CharaMakeType row, CustomizeIndex type) + { + var data = row.CharaMakeStruct.FirstOrNull(m => m.Customize == type.ToByteAndMask().ByteIdx); + return data?.SubMenuParam.Take(data.Value.SubMenuNum).Select((v, i) => FromValueAndIndex(type, v, i)).ToArray() ?? []; + } + + /// Get face paints from the hair sheet via reflection since there are also unlockable face paints. + private CustomizeData[] GetFacePaints(SubRace race, Gender gender) + { + var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender); + // Number of available face paints is at Unknown37. + var numPaints = row.ReadUInt8Column(37); + var paintList = new List(numPaints); + + for (var i = 0; i < numPaints; ++i) + { + // Face paints start at Unknown73. + var customizeIdx = row.ReadUInt32Column(73 + i * 9); + if (customizeIdx == uint.MaxValue) + continue; + + // Face paint Row from CustomizeSheet might not be set in case of unlockable face paints. + if (_customizeSheet.TryGetRow(customizeIdx, out var paintRow)) + paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon, + (ushort)paintRow.RowId)); + else + paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)i, customizeIdx)); + } + + return [.. paintList.OrderBy(p => p.Value.Value)]; + } + + /// Get List sizes. + private static int GetListSize(CharaMakeType row, CustomizeIndex index) + { + var gameId = index.ToByteAndMask().ByteIdx; + var menu = row.CharaMakeStruct.FirstOrNull(m => m.Customize == gameId); + return menu?.SubMenuNum ?? 0; + } + + /// Get generic Features. + private CustomizeData FromValueAndIndex(CustomizeIndex id, uint value, int index) + => _customizeSheet.TryGetRow(value, out var row) + ? new CustomizeData(id, (CustomizeValue)row.FeatureID, row.Icon, (ushort)row.RowId) + : new CustomizeData(id, (CustomizeValue)(index + 1), value); + + /// Create generic color sets from the parameters. + private static CustomizeData[] CreateColors(ColorParameters colorParameters, CustomizeIndex index, int offset, int num, + bool light = false) + { + var ret = new CustomizeData[num]; + var idx = 0; + foreach (var value in colorParameters.GetSlice(offset, num)) + { + ret[idx] = new CustomizeData(index, (CustomizeValue)(light ? 128 + idx : idx), value, (ushort)(offset + idx)); + ++idx; + } + + return ret; + } + + /// Set the specific option names for the given set of parameters. + private string[] GetOptionNames(CharaMakeType row) + { + var nameArray = Enum.GetValues().Select(c => + { + // Find the first menu that corresponds to the Id. + var byteId = c.ToByteAndMask().ByteIdx; + var menu = row.CharaMakeStruct.FirstOrNull(m => m.Customize == byteId); + if (menu == null) + { + // If none exists and the id corresponds to highlights, set the Highlights name. + if (c == CustomizeIndex.Highlights) + return string.Intern(_lobbySheet.TryGetRow(237, out var text) ? text.Text.ExtractText() : "Highlights"); + + // Otherwise there is an error and we use the default name. + return c.ToDefaultName(); + } + + // Otherwise all is normal, get the menu name or if it does not work the default name. + return string.Intern(_lobbySheet.TryGetRow(menu.Value.Menu.RowId, out var textRow) + ? textRow.Text.ExtractText() + : c.ToDefaultName()); + }).ToArray(); + + // Add names for both eye colors. + nameArray[(int)CustomizeIndex.EyeColorLeft] = CustomizeIndex.EyeColorLeft.ToDefaultName(); + nameArray[(int)CustomizeIndex.EyeColorRight] = CustomizeIndex.EyeColorRight.ToDefaultName(); + nameArray[(int)CustomizeIndex.FacialFeature1] = CustomizeIndex.FacialFeature1.ToDefaultName(); + nameArray[(int)CustomizeIndex.FacialFeature2] = CustomizeIndex.FacialFeature2.ToDefaultName(); + nameArray[(int)CustomizeIndex.FacialFeature3] = CustomizeIndex.FacialFeature3.ToDefaultName(); + nameArray[(int)CustomizeIndex.FacialFeature4] = CustomizeIndex.FacialFeature4.ToDefaultName(); + nameArray[(int)CustomizeIndex.FacialFeature5] = CustomizeIndex.FacialFeature5.ToDefaultName(); + nameArray[(int)CustomizeIndex.FacialFeature6] = CustomizeIndex.FacialFeature6.ToDefaultName(); + nameArray[(int)CustomizeIndex.FacialFeature7] = CustomizeIndex.FacialFeature7.ToDefaultName(); + nameArray[(int)CustomizeIndex.LegacyTattoo] = CustomizeIndex.LegacyTattoo.ToDefaultName(); + nameArray[(int)CustomizeIndex.SmallIris] = CustomizeIndex.SmallIris.ToDefaultName(); + nameArray[(int)CustomizeIndex.Lipstick] = CustomizeIndex.Lipstick.ToDefaultName(); + nameArray[(int)CustomizeIndex.FacePaintReversed] = CustomizeIndex.FacePaintReversed.ToDefaultName(); + return nameArray; + } + + /// Get the manu types for all available options. + private static MenuType[] GetMenuTypes(CharaMakeType row) + { + // Set up the menu types for all customizations. + return Enum.GetValues().Select(c => + { + // Those types are not correctly given in the menu, so special case them to color pickers. + switch (c) + { + case CustomizeIndex.HighlightsColor: + case CustomizeIndex.EyeColorLeft: + case CustomizeIndex.EyeColorRight: + case CustomizeIndex.FacePaintColor: + return MenuType.ColorPicker; + case CustomizeIndex.BodyType: return MenuType.Nothing; + case CustomizeIndex.FacePaintReversed: + case CustomizeIndex.Highlights: + case CustomizeIndex.SmallIris: + case CustomizeIndex.Lipstick: + return MenuType.Checkmark; + case CustomizeIndex.FacialFeature1: + case CustomizeIndex.FacialFeature2: + case CustomizeIndex.FacialFeature3: + case CustomizeIndex.FacialFeature4: + case CustomizeIndex.FacialFeature5: + case CustomizeIndex.FacialFeature6: + case CustomizeIndex.FacialFeature7: + case CustomizeIndex.LegacyTattoo: + return MenuType.IconCheckmark; + } + + var gameId = c.ToByteAndMask().ByteIdx; + // Otherwise find the first menu corresponding to the id. + // If there is none, assume a list. + var menu = row.CharaMakeStruct.FirstOrNull(m => m.Customize == gameId); + var ret = (MenuType)(menu?.SubMenuType ?? (byte)MenuType.ListSelector); + if (c is CustomizeIndex.TailShape && ret is MenuType.ListSelector) + ret = MenuType.List1Selector; + return ret; + }).ToArray(); + } + + /// Set the availability of options according to actual availability. + private static void SetAvailability(CustomizeSet set, CharaMakeType row) + { + Set(true, CustomizeIndex.Height); + Set(set.Faces.Count > 0, CustomizeIndex.Face); + Set(true, CustomizeIndex.Hairstyle); + Set(true, CustomizeIndex.Highlights); + Set(true, CustomizeIndex.SkinColor); + Set(true, CustomizeIndex.EyeColorRight); + Set(true, CustomizeIndex.HairColor); + Set(true, CustomizeIndex.HighlightsColor); + Set(true, CustomizeIndex.TattooColor); + Set(set.NumEyebrows > 0, CustomizeIndex.Eyebrows); + Set(true, CustomizeIndex.EyeColorLeft); + Set(set.NumEyeShapes > 0, CustomizeIndex.EyeShape); + Set(set.NumNoseShapes > 0, CustomizeIndex.Nose); + Set(set.NumJawShapes > 0, CustomizeIndex.Jaw); + Set(set.NumMouthShapes > 0, CustomizeIndex.Mouth); + Set(set.LipColorsDark.Count > 0, CustomizeIndex.LipColor); + Set(GetListSize(row, CustomizeIndex.MuscleMass) > 0, CustomizeIndex.MuscleMass); + Set(set.TailEarShapes.Count > 0, CustomizeIndex.TailShape); + Set(GetListSize(row, CustomizeIndex.BustSize) > 0, CustomizeIndex.BustSize); + Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaint); + Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaintColor); + Set(true, CustomizeIndex.FacialFeature1); + Set(true, CustomizeIndex.FacialFeature2); + Set(true, CustomizeIndex.FacialFeature3); + Set(true, CustomizeIndex.FacialFeature4); + Set(true, CustomizeIndex.FacialFeature5); + Set(true, CustomizeIndex.FacialFeature6); + Set(true, CustomizeIndex.FacialFeature7); + Set(true, CustomizeIndex.LegacyTattoo); + Set(true, CustomizeIndex.SmallIris); + Set(set.Race != Race.Hrothgar, CustomizeIndex.Lipstick); + Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaintReversed); + return; + + void Set(bool available, CustomizeIndex flag) + { + if (available) + set.SetAvailable(flag); + } + } + + internal static void SetOrder(CustomizeSet set) + { + var ret = Enum.GetValues().ToArray(); + ret[(int)CustomizeIndex.TattooColor] = CustomizeIndex.EyeColorLeft; + ret[(int)CustomizeIndex.EyeColorLeft] = CustomizeIndex.EyeColorRight; + ret[(int)CustomizeIndex.EyeColorRight] = CustomizeIndex.TattooColor; + + var dict = ret.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray()); + foreach (var type in Enum.GetValues()) + dict.TryAdd(type, []); + set.Order = dict; + } + + /// Set hairstyles per face for Hrothgar and make it simple for non-Hrothgar. + private void SetHairByFace(CustomizeSet set) + { + if (set.Race != Race.Hrothgar) + { + set.HairByFace = Enumerable.Repeat(set.HairStyles, set.Faces.Count + 1).ToArray(); + return; + } + + var tmp = new IReadOnlyList[set.Faces.Count + 1]; + tmp[0] = set.HairStyles; + + for (var i = 1; i <= set.Faces.Count; ++i) + { + tmp[i] = set.HairStyles.Where(Valid).ToArray(); + continue; + + bool Valid(CustomizeData c) + { + var data = _customizeSheet.TryGetRow(c.CustomizeId, out var customize) ? customize.Unknown0 : 0; + return data == 0 || data == i + set.Faces.Count; + } + } + + set.HairByFace = tmp; + } + + /// + /// Create a list of lists of facial features and the legacy tattoo. + /// Facial Features are bools in a bitfield, so we supply an "off" and an "on" value for simplicity of use. + /// + private static void SetFacialFeatures(CustomizeSet set, in CharaMakeType row) + { + var count = set.Faces.Count; + set.FacialFeature1 = new List<(CustomizeData, CustomizeData)>(count); + set.LegacyTattoo = Create(CustomizeIndex.LegacyTattoo, 137905); + + var tmp = Enumerable.Repeat(0, 7).Select(_ => new (CustomizeData, CustomizeData)[count + 1]).ToArray(); + for (var i = 0; i < count; ++i) + { + var data = row.FacialFeatureOption[i]; + tmp[0][i + 1] = Create(CustomizeIndex.FacialFeature1, (uint)data.Option1); + tmp[1][i + 1] = Create(CustomizeIndex.FacialFeature2, (uint)data.Option2); + tmp[2][i + 1] = Create(CustomizeIndex.FacialFeature3, (uint)data.Option3); + tmp[3][i + 1] = Create(CustomizeIndex.FacialFeature4, (uint)data.Option4); + tmp[4][i + 1] = Create(CustomizeIndex.FacialFeature5, (uint)data.Option5); + tmp[5][i + 1] = Create(CustomizeIndex.FacialFeature6, (uint)data.Option6); + tmp[6][i + 1] = Create(CustomizeIndex.FacialFeature7, (uint)data.Option7); + } + + set.FacialFeature1 = tmp[0]; + set.FacialFeature2 = tmp[1]; + set.FacialFeature3 = tmp[2]; + set.FacialFeature4 = tmp[3]; + set.FacialFeature5 = tmp[4]; + set.FacialFeature6 = tmp[5]; + set.FacialFeature7 = tmp[6]; + return; + + static (CustomizeData, CustomizeData) Create(CustomizeIndex i, uint data) + => (new CustomizeData(i, CustomizeValue.Zero, data), new CustomizeData(i, CustomizeValue.Max, data, 1)); + } +} diff --git a/Glamourer/GameData/DecalParameters.cs b/Glamourer/GameData/DecalParameters.cs new file mode 100644 index 0000000..de5231f --- /dev/null +++ b/Glamourer/GameData/DecalParameters.cs @@ -0,0 +1,8 @@ +using Vector4 = FFXIVClientStructs.FFXIV.Common.Math.Vector4; + +namespace Glamourer.GameData; + +public struct DecalParameters +{ + public Vector4 Color; +} diff --git a/Glamourer/GameData/MenuType.cs b/Glamourer/GameData/MenuType.cs new file mode 100644 index 0000000..a1d727b --- /dev/null +++ b/Glamourer/GameData/MenuType.cs @@ -0,0 +1,14 @@ +namespace Glamourer.GameData; + +public enum MenuType +{ + ListSelector = 0, + IconSelector = 1, + ColorPicker = 2, + DoubleColorPicker = 3, + IconCheckmark = 4, + Percentage = 5, + Checkmark = 6, // custom + Nothing = 7, // custom + List1Selector = 8, // custom, 1-indexed lists +} diff --git a/Glamourer/GameData/NpcCustomizeSet.cs b/Glamourer/GameData/NpcCustomizeSet.cs new file mode 100644 index 0000000..725f80f --- /dev/null +++ b/Glamourer/GameData/NpcCustomizeSet.cs @@ -0,0 +1,356 @@ +using Dalamud.Plugin.Services; +using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Lumina.Excel.Sheets; +using OtterGui.Services; +using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.GameData; + +/// Contains a set of all human NPC appearances with their names. +public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList +{ + /// + public string Name + => nameof(NpcCustomizeSet); + + /// + public long Time { get; private set; } + + /// + public long Memory { get; private set; } + + /// + public int TotalCount + => _data.Count; + + /// + public Task Awaiter { get; } + + /// + public bool Finished + => Awaiter.IsCompletedSuccessfully; + + /// The list of data. + private readonly List _data = []; + + private readonly BitArray _hairColors = new(256); + private readonly BitArray _eyeColors = new(256); + private readonly BitArray _facepaintColors = new(256); + private readonly BitArray _tattooColors = new(256); + private readonly BitArray _facepaints = new(128); + + public bool CheckValue(CustomizeIndex type, CustomizeValue value) + => type switch + { + CustomizeIndex.HairColor => _hairColors[value.Value], + CustomizeIndex.HighlightsColor => _hairColors[value.Value], + CustomizeIndex.EyeColorLeft => _eyeColors[value.Value], + CustomizeIndex.EyeColorRight => _eyeColors[value.Value], + CustomizeIndex.FacePaintColor => _facepaintColors[value.Value], + CustomizeIndex.TattooColor => _tattooColors[value.Value], + CustomizeIndex.FacePaint when value.Value < 128 => _facepaints[value.Value], + _ => false, + }; + + /// Create the data when ready. + public NpcCustomizeSet(IDataManager data, DictENpc eNpcs, DictBNpc bNpcs, DictBNpcNames bNpcNames) + { + var waitTask = Task.WhenAll(eNpcs.Awaiter, bNpcs.Awaiter, bNpcNames.Awaiter); + Awaiter = waitTask.ContinueWith(_ => + { + var watch = Stopwatch.StartNew(); + var eNpcTask = Task.Run(() => CreateEnpcData(data, eNpcs)); + var bNpcTask = Task.Run(() => CreateBnpcData(data, bNpcs, bNpcNames)); + FilterAndOrderNpcData(eNpcTask.Result, bNpcTask.Result); + Time = watch.ElapsedMilliseconds; + }) + .ContinueWith(_ => CheckFacepaintFiles(data, _facepaints)); + } + + /// Create data from event NPCs. + private static List CreateEnpcData(IDataManager data, DictENpc eNpcs) + { + var enpcSheet = data.GetExcelSheet(); + var list = new List(eNpcs.Count); + + // Go through all event NPCs already collected into a dictionary. + foreach (var (id, name) in eNpcs) + { + // We only accept NPCs with valid names. + if (!enpcSheet.TryGetRow(id.Id, out var row) || name.IsNullOrWhitespace()) + continue; + + // Check if the customization is a valid human. + var (valid, customize) = FromEnpcBase(row); + if (!valid) + continue; + + var ret = new NpcData + { + Name = name, + Customize = customize, + ModelId = row.ModelChara.RowId, + Id = id, + Kind = ObjectKind.EventNpc, + }; + + // Event NPCs have a reference to NpcEquip but also contain the appearance in their own row. + // Prefer the NpcEquip reference if it is set and the own does not appear to be set, otherwise use the own. + if (row.NpcEquip.RowId != 0 && row.NpcEquip.Value is { } equip && row is { ModelBody: 0, ModelLegs: 0 }) + ApplyNpcEquip(ref ret, equip); + else + ApplyNpcEquip(ref ret, row); + + list.Add(ret); + } + + return list; + } + + /// Create data from battle NPCs. + private static List CreateBnpcData(IDataManager data, DictBNpc bNpcs, DictBNpcNames bNpcNames) + { + var bnpcSheet = data.GetExcelSheet(); + var list = new List(bnpcSheet.Count); + + // We go through all battle NPCs in the sheet because the dictionary refers to names. + foreach (var baseRow in bnpcSheet) + { + // Only accept humans. + if (baseRow.ModelChara.Value.Type != 1) + continue; + + var bnpcNameIds = bNpcNames[baseRow.RowId]; + // Only accept battle NPCs with known associated names. + if (bnpcNameIds.Count == 0) + continue; + + // Check if the customization is a valid human. + var (valid, customize) = FromBnpcCustomize(baseRow.BNpcCustomize.Value); + if (!valid) + continue; + + var equip = baseRow.NpcEquip.Value; + var ret = new NpcData + { + Customize = customize, + ModelId = baseRow.ModelChara.RowId, + Id = baseRow.RowId, + Kind = ObjectKind.BattleNpc, + }; + ApplyNpcEquip(ref ret, equip); + // Add the appearance for each associated name. + foreach (var bnpcNameId in bnpcNameIds) + { + if (bNpcs.TryGetValue(bnpcNameId.Id, out var name) && !name.IsNullOrWhitespace()) + list.Add(ret with { Name = name }); + } + } + + return list; + } + + /// Given the battle NPC and event NPC lists, order and deduplicate entries. + private void FilterAndOrderNpcData(IReadOnlyCollection eNpcEquip, IReadOnlyCollection bNpcEquip) + { + _data.Clear(); + // This is a maximum since we deduplicate. + _data.EnsureCapacity(eNpcEquip.Count + bNpcEquip.Count); + // Convert the NPCs to a dictionary of lists grouped by name. + var groups = eNpcEquip.Concat(bNpcEquip).GroupBy(d => d.Name).ToDictionary(g => g.Key, g => g.ToList()); + // Iterate through the sorted list. + foreach (var (_, duplicates) in groups.OrderBy(kvp => kvp.Key)) + { + // Remove any duplicate entries for a name with identical data. + for (var i = 0; i < duplicates.Count; ++i) + { + var current = duplicates[i]; + _hairColors[current.Customize[CustomizeIndex.HairColor].Value] = true; + _hairColors[current.Customize[CustomizeIndex.HighlightsColor].Value] = true; + _eyeColors[current.Customize[CustomizeIndex.EyeColorLeft].Value] = true; + _eyeColors[current.Customize[CustomizeIndex.EyeColorRight].Value] = true; + _facepaintColors[current.Customize[CustomizeIndex.FacePaintColor].Value] = true; + _tattooColors[current.Customize[CustomizeIndex.TattooColor].Value] = true; + for (var j = 0; j < i; ++j) + { + if (current.DataEquals(duplicates[j])) + { + duplicates.RemoveAt(i--); + break; + } + } + } + + // If there is only a single entry, add that. + if (duplicates.Count == 1) + { + _data.Add(duplicates[0]); + Memory += 96; + } + else + { + _data.AddRange(duplicates); + Memory += 96 * duplicates.Count; + } + } + + // Sort non-alphanumeric entries at the end instead of the beginning. + var lastWeird = _data.FindIndex(d => char.IsAsciiLetterOrDigit(d.Name[0])); + if (lastWeird != -1) + { + _data.AddRange(_data.Take(lastWeird)); + _data.RemoveRange(0, lastWeird); + } + + // Reduce memory footprint. + _data.TrimExcess(); + } + + /// Apply equipment from a NpcEquip row. + private static void ApplyNpcEquip(ref NpcData data, NpcEquip row) + { + data.Set(0, row.ModelHead | (row.DyeHead.RowId << 24) | ((ulong)row.Dye2Head.RowId << 32)); + data.Set(1, row.ModelBody | (row.DyeBody.RowId << 24) | ((ulong)row.Dye2Body.RowId << 32)); + data.Set(2, row.ModelHands | (row.DyeHands.RowId << 24) | ((ulong)row.Dye2Hands.RowId << 32)); + data.Set(3, row.ModelLegs | (row.DyeLegs.RowId << 24) | ((ulong)row.Dye2Legs.RowId << 32)); + data.Set(4, row.ModelFeet | (row.DyeFeet.RowId << 24) | ((ulong)row.Dye2Feet.RowId << 32)); + data.Set(5, row.ModelEars | (row.DyeEars.RowId << 24) | ((ulong)row.Dye2Ears.RowId << 32)); + data.Set(6, row.ModelNeck | (row.DyeNeck.RowId << 24) | ((ulong)row.Dye2Neck.RowId << 32)); + data.Set(7, row.ModelWrists | (row.DyeWrists.RowId << 24) | ((ulong)row.Dye2Wrists.RowId << 32)); + data.Set(8, row.ModelRightRing | (row.DyeRightRing.RowId << 24) | ((ulong)row.Dye2RightRing.RowId << 32)); + data.Set(9, row.ModelLeftRing | (row.DyeLeftRing.RowId << 24) | ((ulong)row.Dye2LeftRing.RowId << 32)); + data.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.RowId << 48) | ((ulong)row.Dye2MainHand.RowId << 56)); + data.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.RowId << 48) | ((ulong)row.Dye2OffHand.RowId << 56)); + data.VisorToggled = row.Visor; + } + + /// Apply equipment from a ENpcBase Row row. + private static void ApplyNpcEquip(ref NpcData data, ENpcBase row) + { + data.Set(0, row.ModelHead | (row.DyeHead.RowId << 24) | ((ulong)row.Dye2Head.RowId << 32)); + data.Set(1, row.ModelBody | (row.DyeBody.RowId << 24) | ((ulong)row.Dye2Body.RowId << 32)); + data.Set(2, row.ModelHands | (row.DyeHands.RowId << 24) | ((ulong)row.Dye2Hands.RowId << 32)); + data.Set(3, row.ModelLegs | (row.DyeLegs.RowId << 24) | ((ulong)row.Dye2Legs.RowId << 32)); + data.Set(4, row.ModelFeet | (row.DyeFeet.RowId << 24) | ((ulong)row.Dye2Feet.RowId << 32)); + data.Set(5, row.ModelEars | (row.DyeEars.RowId << 24) | ((ulong)row.Dye2Ears.RowId << 32)); + data.Set(6, row.ModelNeck | (row.DyeNeck.RowId << 24) | ((ulong)row.Dye2Neck.RowId << 32)); + data.Set(7, row.ModelWrists | (row.DyeWrists.RowId << 24) | ((ulong)row.Dye2Wrists.RowId << 32)); + data.Set(8, row.ModelRightRing | (row.DyeRightRing.RowId << 24) | ((ulong)row.Dye2RightRing.RowId << 32)); + data.Set(9, row.ModelLeftRing | (row.DyeLeftRing.RowId << 24) | ((ulong)row.Dye2LeftRing.RowId << 32)); + data.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.RowId << 48) | ((ulong)row.Dye2MainHand.RowId << 56)); + data.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.RowId << 48) | ((ulong)row.Dye2OffHand.RowId << 56)); + data.VisorToggled = row.Visor; + } + + /// Obtain customizations from a BNpcCustomize row and check if the human is valid. + private static (bool, CustomizeArray) FromBnpcCustomize(BNpcCustomize bnpcCustomize) + { + var customize = new CustomizeArray(); + customize.SetByIndex(0, (CustomizeValue)(byte)bnpcCustomize.Race.RowId); + customize.SetByIndex(1, (CustomizeValue)bnpcCustomize.Gender); + customize.SetByIndex(2, (CustomizeValue)bnpcCustomize.BodyType); + customize.SetByIndex(3, (CustomizeValue)bnpcCustomize.Height); + customize.SetByIndex(4, (CustomizeValue)(byte)bnpcCustomize.Tribe.RowId); + customize.SetByIndex(5, (CustomizeValue)bnpcCustomize.Face); + customize.SetByIndex(6, (CustomizeValue)bnpcCustomize.HairStyle); + customize.SetByIndex(7, (CustomizeValue)bnpcCustomize.HairHighlight); + customize.SetByIndex(8, (CustomizeValue)bnpcCustomize.SkinColor); + customize.SetByIndex(9, (CustomizeValue)bnpcCustomize.EyeHeterochromia); + customize.SetByIndex(10, (CustomizeValue)bnpcCustomize.HairColor); + customize.SetByIndex(11, (CustomizeValue)bnpcCustomize.HairHighlightColor); + customize.SetByIndex(12, (CustomizeValue)bnpcCustomize.FacialFeature); + customize.SetByIndex(13, (CustomizeValue)bnpcCustomize.FacialFeatureColor); + customize.SetByIndex(14, (CustomizeValue)bnpcCustomize.Eyebrows); + customize.SetByIndex(15, (CustomizeValue)bnpcCustomize.EyeColor); + customize.SetByIndex(16, (CustomizeValue)bnpcCustomize.EyeShape); + customize.SetByIndex(17, (CustomizeValue)bnpcCustomize.Nose); + customize.SetByIndex(18, (CustomizeValue)bnpcCustomize.Jaw); + customize.SetByIndex(19, (CustomizeValue)bnpcCustomize.Mouth); + customize.SetByIndex(20, (CustomizeValue)bnpcCustomize.LipColor); + customize.SetByIndex(21, (CustomizeValue)bnpcCustomize.BustOrTone1); + customize.SetByIndex(22, (CustomizeValue)bnpcCustomize.ExtraFeature1); + customize.SetByIndex(23, (CustomizeValue)bnpcCustomize.ExtraFeature2OrBust); + customize.SetByIndex(24, (CustomizeValue)bnpcCustomize.FacePaint); + customize.SetByIndex(25, (CustomizeValue)bnpcCustomize.FacePaintColor); + + if (!CustomizeManager.Races.Contains(customize.Race) + || !CustomizeManager.Clans.Contains(customize.Clan) + || !CustomizeManager.Genders.Contains(customize.Gender)) + return (false, CustomizeArray.Default); + + return (true, customize); + } + + /// Obtain customizations from a ENpcBase row and check if the human is valid. + private static (bool, CustomizeArray) FromEnpcBase(ENpcBase enpcBase) + { + if (enpcBase.ModelChara.ValueNullable?.Type != 1) + return (false, CustomizeArray.Default); + + var customize = new CustomizeArray(); + customize.SetByIndex(0, (CustomizeValue)(byte)enpcBase.Race.RowId); + customize.SetByIndex(1, (CustomizeValue)enpcBase.Gender); + customize.SetByIndex(2, (CustomizeValue)enpcBase.BodyType); + customize.SetByIndex(3, (CustomizeValue)enpcBase.Height); + customize.SetByIndex(4, (CustomizeValue)(byte)enpcBase.Tribe.RowId); + customize.SetByIndex(5, (CustomizeValue)enpcBase.Face); + customize.SetByIndex(6, (CustomizeValue)enpcBase.HairStyle); + customize.SetByIndex(7, (CustomizeValue)enpcBase.HairHighlight); + customize.SetByIndex(8, (CustomizeValue)enpcBase.SkinColor); + customize.SetByIndex(9, (CustomizeValue)enpcBase.EyeHeterochromia); + customize.SetByIndex(10, (CustomizeValue)enpcBase.HairColor); + customize.SetByIndex(11, (CustomizeValue)enpcBase.HairHighlightColor); + customize.SetByIndex(12, (CustomizeValue)enpcBase.FacialFeature); + customize.SetByIndex(13, (CustomizeValue)enpcBase.FacialFeatureColor); + customize.SetByIndex(14, (CustomizeValue)enpcBase.Eyebrows); + customize.SetByIndex(15, (CustomizeValue)enpcBase.EyeColor); + customize.SetByIndex(16, (CustomizeValue)enpcBase.EyeShape); + customize.SetByIndex(17, (CustomizeValue)enpcBase.Nose); + customize.SetByIndex(18, (CustomizeValue)enpcBase.Jaw); + customize.SetByIndex(19, (CustomizeValue)enpcBase.Mouth); + customize.SetByIndex(20, (CustomizeValue)enpcBase.LipColor); + customize.SetByIndex(21, (CustomizeValue)enpcBase.BustOrTone1); + customize.SetByIndex(22, (CustomizeValue)enpcBase.ExtraFeature1); + customize.SetByIndex(23, (CustomizeValue)enpcBase.ExtraFeature2OrBust); + customize.SetByIndex(24, (CustomizeValue)enpcBase.FacePaint); + customize.SetByIndex(25, (CustomizeValue)enpcBase.FacePaintColor); + + if (!CustomizeManager.Races.Contains(customize.Race) + || !CustomizeManager.Clans.Contains(customize.Clan) + || !CustomizeManager.Genders.Contains(customize.Gender)) + return (false, CustomizeArray.Default); + + return (true, customize); + } + + /// Check decal files for existence. + private static void CheckFacepaintFiles(IDataManager data, BitArray facepaints) + { + for (byte i = 0; i < 128; ++i) + { + var path = GamePaths.Tex.FaceDecal(i); + if (data.FileExists(path)) + facepaints[i] = true; + } + } + + /// + public IEnumerator GetEnumerator() + => _data.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + /// + public int Count + => _data.Count; + + /// + public NpcData this[int index] + => _data[index]; +} diff --git a/Glamourer/GameData/NpcData.cs b/Glamourer/GameData/NpcData.cs new file mode 100644 index 0000000..0076bb6 --- /dev/null +++ b/Glamourer/GameData/NpcData.cs @@ -0,0 +1,112 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.GameData.Structs; + +namespace Glamourer.GameData; + +/// A struct containing everything to replicate the appearance of a human NPC. +public unsafe struct NpcData +{ + /// The name of the NPC. + public string Name; + + /// The customizations of the NPC. + public CustomizeArray Customize; + + /// The equipment appearance of the NPC, 10 * CharacterArmor. + private fixed byte _equip[CharacterArmor.Size * 10]; + + /// The mainhand weapon appearance of the NPC. + public CharacterWeapon Mainhand; + + /// The offhand weapon appearance of the NPC. + public CharacterWeapon Offhand; + + /// The data ID of the NPC, either event NPC or battle NPC name. + public NpcId Id; + + /// The Model ID of the NPC. + public uint ModelId; + + /// Whether the NPCs visor is toggled. + public bool VisorToggled; + + /// Whether the NPC is an event NPC or a battle NPC. + public ObjectKind Kind; + + /// Obtain the equipment as CharacterArmors. + public ReadOnlySpan Equip + { + get + { + fixed (byte* ptr = _equip) + { + return new ReadOnlySpan((CharacterArmor*)ptr, 10); + } + } + } + + /// Write all the gear appearance to a single string. + public string WriteGear() + { + var sb = new StringBuilder(128); + var span = Equip; + for (var i = 0; i < 10; ++i) + { + sb.Append(span[i].Set.Id.ToString("D4")) + .Append('-') + .Append(span[i].Variant.Id.ToString("D3")); + foreach (var stain in span[i].Stains) + sb.Append('-').Append(stain.Id.ToString("D3")); + } + + sb.Append(Mainhand.Skeleton.Id.ToString("D4")) + .Append('-') + .Append(Mainhand.Weapon.Id.ToString("D4")) + .Append('-') + .Append(Mainhand.Variant.Id.ToString("D3")); + foreach (var stain in Mainhand.Stains) + sb.Append('-').Append(stain.Id.ToString("D3")); + sb.Append(", ") + .Append(Offhand.Skeleton.Id.ToString("D4")) + .Append('-') + .Append(Offhand.Weapon.Id.ToString("D4")) + .Append('-') + .Append(Offhand.Variant.Id.ToString("D3")); + foreach (var stain in Mainhand.Stains) + sb.Append('-').Append(stain.Id.ToString("D3")); + return sb.ToString(); + } + + /// Set an equipment piece to a given value. + internal void Set(int idx, ulong value) + { + fixed (byte* ptr = _equip) + { + ((ulong*)ptr)[idx] = value; + } + } + + /// Check if the appearance data, excluding ID and Name, of two NpcData is equal. + public bool DataEquals(in NpcData other) + { + if (ModelId != other.ModelId) + return false; + + if (VisorToggled != other.VisorToggled) + return false; + + if (!Customize.Equals(other.Customize)) + return false; + + if (!Mainhand.Equals(other.Mainhand)) + return false; + + if (!Offhand.Equals(other.Offhand)) + return false; + + fixed (byte* ptr1 = _equip, ptr2 = other._equip) + { + return new ReadOnlySpan(ptr1, 40).SequenceEqual(new ReadOnlySpan(ptr2, 40)); + } + } +} diff --git a/Glamourer/Glamourer.cs b/Glamourer/Glamourer.cs index 31942da..33c67d5 100644 --- a/Glamourer/Glamourer.cs +++ b/Glamourer/Glamourer.cs @@ -1,11 +1,15 @@ -using System.Reflection; -using Dalamud.Plugin; +using Dalamud.Plugin; +using Glamourer.Api; +using Glamourer.Automation; +using Glamourer.Designs; using Glamourer.Gui; using Glamourer.Interop; using Glamourer.Services; -using Microsoft.Extensions.DependencyInjection; +using Glamourer.State; using OtterGui.Classes; using OtterGui.Log; +using OtterGui.Services; +using Penumbra.GameData.Interop; namespace Glamourer; @@ -22,19 +26,26 @@ public class Glamourer : IDalamudPlugin public static readonly Logger Log = new(); public static MessageService Messager { get; private set; } = null!; + public static DynamisIpc Dynamis { get; private set; } = null!; - private readonly ServiceProvider _services; + private readonly ServiceManager _services; - public Glamourer(DalamudPluginInterface pluginInterface) + public Glamourer(IDalamudPluginInterface pluginInterface) { try { - _services = ServiceManager.CreateProvider(pluginInterface, Log); - Messager = _services.GetRequiredService(); - _services.GetRequiredService(); // initialize ui. - _services.GetRequiredService(); // initialize commands. - _services.GetRequiredService(); - _services.GetRequiredService(); + _services = StaticServiceManager.CreateProvider(pluginInterface, Log, this); + Messager = _services.GetService(); + Dynamis = _services.GetService(); + _services.EnsureRequiredServices(); + + _services.GetService(); + _services.GetService(); + _services.GetService(); + _services.GetService(); // Initialize State Listener. + _services.GetService(); // initialize ui. + _services.GetService(); // initialize commands. + _services.GetService(); // initialize IPC. Log.Information($"Glamourer v{Version} loaded successfully."); } catch @@ -44,6 +55,96 @@ public class Glamourer : IDalamudPlugin } } + public string GatherSupportInformation() + { + var sb = new StringBuilder(10240); + var config = _services.GetService(); + sb.AppendLine("**Settings**"); + sb.Append($"> **`Plugin Version: `** {Version}\n"); + sb.Append($"> **`Commit Hash: `** {CommitHash}\n"); + sb.Append($"> **`Enable Auto Designs: `** {config.EnableAutoDesigns}\n"); + sb.Append($"> **`Gear Protection: `** {config.UseRestrictedGearProtection}\n"); + sb.Append($"> **`Item Restriction: `** {config.UnlockedItemMode}\n"); + sb.Append($"> **`Keep Manual Changes: `** {config.RespectManualOnAutomationUpdate}\n"); + sb.Append($"> **`Auto-Reload Gear: `** {config.AutoRedrawEquipOnChanges}\n"); + sb.Append($"> **`Revert on Zone Change:`** {config.RevertManualChangesOnZoneChange}\n"); + sb.Append($"> **`Festival Easter-Eggs: `** {config.DisableFestivals}\n"); + sb.Append($"> **`Apply Entire Weapon: `** {config.ChangeEntireItem}\n"); + sb.Append($"> **`Apply Associated Mods:`** {config.AlwaysApplyAssociatedMods}\n"); + sb.Append($"> **`Attach to PCP: `** {config.AttachToPcp}\n"); + sb.Append($"> **`Hidden Panels: `** {config.HideDesignPanel}\n"); + sb.Append($"> **`Show QDB: `** {config.Ephemeral.ShowDesignQuickBar}\n"); + sb.Append($"> **`QDB Hotkey: `** {config.ToggleQuickDesignBar}\n"); + sb.Append($"> **`Smaller Equip Display:`** {config.SmallEquip}\n"); + sb.Append($"> **`Debug Mode: `** {config.DebugMode}\n"); + sb.Append($"> **`Cheat Codes: `** {(ulong)_services.GetService().AllEnabled:X8}\n"); + sb.AppendLine("**Plugins**"); + GatherRelevantPlugins(sb); + var designManager = _services.GetService(); + var autoManager = _services.GetService(); + var stateManager = _services.GetService(); + var objectManager = _services.GetService(); + var currentPlayer = objectManager.PlayerData.Identifier; + var states = stateManager.Where(kvp => objectManager.ContainsKey(kvp.Key)).ToList(); + + sb.AppendLine("**Statistics**"); + sb.Append($"> **`Current Player: `** {(currentPlayer.IsValid ? currentPlayer.Incognito(null) : "None")}\n"); + sb.Append($"> **`Saved Designs: `** {designManager.Designs.Count}\n"); + sb.Append($"> **`Automation Sets: `** {autoManager.Count} ({autoManager.Count(set => set.Enabled)} Enabled)\n"); + sb.Append( + $"> **`Actor States: `** {stateManager.Count} ({states.Count} Visible, {stateManager.Values.Count(s => s.IsLocked)} Locked)\n"); + + var enabledAutomation = autoManager.Where(s => s.Enabled).ToList(); + if (enabledAutomation.Count > 0) + { + sb.AppendLine("**Enabled Automation**"); + foreach (var set in enabledAutomation) + { + sb.Append( + $"> **`{set.Identifiers.First().Incognito(null) + ':',-24}`** {(set.Name.Length >= 2 ? $"{set.Name.AsSpan(0, 2)}..." : set.Name)} ({set.Designs.Count} {(set.Designs.Count == 1 ? "Design" : "Designs")})\n"); + } + } + + if (states.Count > 0) + { + sb.AppendLine("**State**"); + foreach (var (ident, state) in states) + { + var sources = Enum.GetValues().Select(s => (0, s)).ToArray(); + foreach (var source in StateIndex.All.Select(s => state.Sources[s])) + ++sources[(int)source].Item1; + foreach (var material in state.Materials.Values) + ++sources[(int)material.Value.Source].Item1; + var sourcesString = string.Join(", ", sources.Where(s => s.Item1 > 0).Select(s => $"{s.s} {s.Item1}")); + sb.Append( + $"> **`{ident.Incognito(null) + ':',-24}`** {(state.IsLocked ? "Locked, " : string.Empty)}Job {state.LastJob.Id}, Zone {state.LastTerritory}, Materials {state.Materials.Values.Count}, {sourcesString}\n"); + } + } + + return sb.ToString(); + } + + + private void GatherRelevantPlugins(StringBuilder sb) + { + ReadOnlySpan relevantPlugins = + [ + "Penumbra", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", + "LoporritSync", "GagSpeak", "ProjectGagSpeak", "RoleplayingVoiceDalamud", + ]; + var plugins = _services.GetService().InstalledPlugins + .GroupBy(p => p.InternalName) + .ToDictionary(g => g.Key, g => + { + var item = g.OrderByDescending(p => p.IsLoaded).ThenByDescending(p => p.Version).First(); + return (item.IsLoaded, item.Version, item.Name); + }); + foreach (var plugin in relevantPlugins) + { + if (plugins.TryGetValue(plugin, out var data)) + sb.Append($"> **`{data.Name + ':',-22}`** {data.Version}{(data.IsLoaded ? string.Empty : " (Disabled)")}\n"); + } + } public void Dispose() => _services?.Dispose(); diff --git a/Glamourer/Glamourer.csproj b/Glamourer/Glamourer.csproj index 7ecb3b6..560621d 100644 --- a/Glamourer/Glamourer.csproj +++ b/Glamourer/Glamourer.csproj @@ -1,95 +1,34 @@ - + - net7.0-windows - preview - x64 Glamourer Glamourer - 1.0.0.2 - 1.0.0.2 - SoftOtter + 9.0.0.1 + 9.0.0.1 Glamourer - Copyright © 2023 - true - Library + Copyright © 2025 4 - true - enable bin\$(Configuration)\ - $(MSBuildWarningsAsMessages);MSB3277 - true - false - false - - - - true - full - false - DEBUG;TRACE - - - - pdbonly - true - TRACE - - - - OnOutputUpdated + + + PreserveNewest + - - $(AppData)\XIVLauncher\addon\Hooks\dev\ - - - - - $(DalamudLibPath)Dalamud.dll - False - - - $(DalamudLibPath)FFXIVClientStructs.dll - False - - - $(DalamudLibPath)ImGui.NET.dll - False - - - $(DalamudLibPath)ImGuiScene.dll - False - - - $(DalamudLibPath)Lumina.dll - False - - - $(DalamudLibPath)Lumina.Excel.dll - False - - - $(DalamudLibPath)Newtonsoft.Json.dll - False - - - + - - + - - + @@ -108,21 +47,13 @@ - - + + + $(GitCommitHash) - - - - PreserveNewest - - - - - \ No newline at end of file diff --git a/Glamourer/Glamourer.json b/Glamourer/Glamourer.json index 148951b..e2dbf8b 100644 --- a/Glamourer/Glamourer.json +++ b/Glamourer/Glamourer.json @@ -5,10 +5,10 @@ "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": "1.0.1.0", + "AssemblyVersion": "9.0.0.1", "RepoUrl": "https://github.com/Ottermandias/Glamourer", "ApplicableVersion": "any", - "DalamudApiLevel": 9, + "DalamudApiLevel": 14, "ImageUrls": null, "IconUrl": "https://raw.githubusercontent.com/Ottermandias/Glamourer/master/images/icon.png" } \ No newline at end of file diff --git a/Glamourer/GlobalUsings.cs b/Glamourer/GlobalUsings.cs new file mode 100644 index 0000000..03d2082 --- /dev/null +++ b/Glamourer/GlobalUsings.cs @@ -0,0 +1,19 @@ +global using System; +global using System.Collections; +global using System.Collections.Concurrent; +global using System.Collections.Generic; +global using System.Diagnostics; +global using System.Diagnostics.CodeAnalysis; +global using System.Globalization; +global using System.IO; +global using System.IO.Compression; +global using System.Linq; +global using System.Numerics; +global using System.Reflection; +global using System.Runtime.CompilerServices; +global using System.Runtime.InteropServices; +global using System.Security.Cryptography; +global using System.Text; +global using System.Text.RegularExpressions; +global using System.Threading; +global using System.Threading.Tasks; diff --git a/Glamourer/Gui/Colors.cs b/Glamourer/Gui/Colors.cs index a665eee..b2713eb 100644 --- a/Glamourer/Gui/Colors.cs +++ b/Glamourer/Gui/Colors.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using Dalamud.Bindings.ImGui; namespace Glamourer.Gui; @@ -21,32 +21,58 @@ public enum ColorId FavoriteStarOn, FavoriteStarHovered, FavoriteStarOff, + QuickDesignButton, + QuickDesignFrame, + QuickDesignBg, + TriStateCheck, + TriStateCross, + TriStateNeutral, + BattleNpc, + EventNpc, + ModdedItemMarker, + ContainsItemsEnabled, + ContainsItemsDisabled, + AdvancedDyeActive, } public static class Colors { + public const uint SelectedRed = 0xFF2020D0; + public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) => color switch { // @formatter:off - ColorId.NormalDesign => (0xFFFFFFFF, "Normal Design", "A design with no specific traits." ), - ColorId.CustomizationDesign => (0xFFC000C0, "Customization Design", "A design that only changes customizations on a character." ), - ColorId.StateDesign => (0xFF00C0C0, "State Design", "A design that does not change equipment or customizations on a character." ), - ColorId.EquipmentDesign => (0xFF00C000, "Equipment Design", "A design that only changes equipment on a character." ), - ColorId.ActorAvailable => (0xFF18C018, "Actor Available", "The header in the Actor tab panel if the currently selected actor exists in the game world at least once." ), - ColorId.ActorUnavailable => (0xFF1818C0, "Actor Unavailable", "The Header in the Actor tab panel if the currently selected actor does not exist in the game world." ), - ColorId.FolderExpanded => (0xFFFFF0C0, "Expanded Design Folder", "A design folder that is currently expanded." ), - ColorId.FolderCollapsed => (0xFFFFF0C0, "Collapsed Design Folder", "A design folder that is currently collapsed." ), - ColorId.FolderLine => (0xFFFFF0C0, "Expanded Design Folder Line", "The line signifying which descendants belong to an expanded design folder." ), - ColorId.EnabledAutoSet => (0xFFA0F0A0, "Enabled Automation Set", "An automation set that is currently enabled. Only one set can be enabled for each identifier at once." ), - ColorId.DisabledAutoSet => (0xFF808080, "Disabled Automation Set", "An automation set that is currently disabled." ), - ColorId.AutomationActorAvailable => (0xFFFFFFFF, "Automation Actor Available", "A character associated with the given automated design set is currently visible." ), - ColorId.AutomationActorUnavailable => (0xFF808080, "Automation Actor Unavailable", "No character associated with the given automated design set is currently visible." ), - ColorId.HeaderButtons => (0xFFFFF0C0, "Header Buttons", "The text and border color of buttons in the header, like the Incognito toggle." ), - ColorId.FavoriteStarOn => (0xFF40D0D0, "Favored Item", "The color of the star for favored items and of the border in the unlock overview tab." ), - ColorId.FavoriteStarHovered => (0xFFD040D0, "Favorite Star Hovered", "The color of the star for favored items when it is hovered." ), - ColorId.FavoriteStarOff => (0x20808080, "Favorite Star Outline", "The color of the star for items that are not favored when it is not hovered." ), - _ => (0x00000000, string.Empty, string.Empty ), + ColorId.NormalDesign => (0xFFFFFFFF, "Normal Design", "A design with no specific traits." ), + ColorId.CustomizationDesign => (0xFFC000C0, "Customization Design", "A design that only changes customizations on a character." ), + ColorId.StateDesign => (0xFF00C0C0, "State Design", "A design that does not change equipment or customizations on a character." ), + ColorId.EquipmentDesign => (0xFF00C000, "Equipment Design", "A design that only changes equipment on a character." ), + ColorId.ActorAvailable => (0xFF18C018, "Actor Available", "The header in the Actor tab panel if the currently selected actor exists in the game world at least once." ), + ColorId.ActorUnavailable => (0xFF1818C0, "Actor Unavailable", "The Header in the Actor tab panel if the currently selected actor does not exist in the game world." ), + ColorId.FolderExpanded => (0xFFFFF0C0, "Expanded Design Folder", "A design folder that is currently expanded." ), + ColorId.FolderCollapsed => (0xFFFFF0C0, "Collapsed Design Folder", "A design folder that is currently collapsed." ), + ColorId.FolderLine => (0xFFFFF0C0, "Expanded Design Folder Line", "The line signifying which descendants belong to an expanded design folder." ), + ColorId.EnabledAutoSet => (0xFFA0F0A0, "Enabled Automation Set", "An automation set that is currently enabled. Only one set can be enabled for each identifier at once." ), + ColorId.DisabledAutoSet => (0xFF808080, "Disabled Automation Set", "An automation set that is currently disabled." ), + ColorId.AutomationActorAvailable => (0xFFFFFFFF, "Automation Actor Available", "A character associated with the given automated design set is currently visible." ), + ColorId.AutomationActorUnavailable => (0xFF808080, "Automation Actor Unavailable", "No character associated with the given automated design set is currently visible." ), + ColorId.HeaderButtons => (0xFFFFF0C0, "Header Buttons", "The text and border color of buttons in the header, like the Incognito toggle." ), + ColorId.FavoriteStarOn => (0xFF40D0D0, "Favored Item", "The color of the star for favored items and of the border in the unlock overview tab." ), + ColorId.FavoriteStarHovered => (0xFFD040D0, "Favorite Star Hovered", "The color of the star for favored items when it is hovered." ), + ColorId.FavoriteStarOff => (0x20808080, "Favorite Star Outline", "The color of the star for items that are not favored when it is not hovered." ), + ColorId.QuickDesignButton => (0x900A0A0A, "Quick Design Bar Button Background", "The color of button frames in the quick design bar." ), + ColorId.QuickDesignFrame => (0x90383838, "Quick Design Bar Combo Background", "The color of the combo background in the quick design bar." ), + ColorId.QuickDesignBg => (0x00F0F0F0, "Quick Design Bar Window Background", "The color of the window background in the quick design bar." ), + ColorId.TriStateCheck => (0xFF00D000, "Checkmark in Tri-State Checkboxes", "The color of the checkmark indicating positive change in tri-state checkboxes." ), + ColorId.TriStateCross => (0xFF0000D0, "Cross in Tri-State Checkboxes", "The color of the cross indicating negative change in tri-state checkboxes." ), + ColorId.TriStateNeutral => (0xFFD0D0D0, "Dot in Tri-State Checkboxes", "The color of the dot indicating no change in tri-state checkboxes." ), + ColorId.BattleNpc => (0xFFFFFFFF, "Battle NPC in NPC Tab", "The color of the names of battle NPCs in the NPC tab that do not have a more specific color assigned." ), + ColorId.EventNpc => (0xFFFFFFFF, "Event NPC in NPC Tab", "The color of the names of event NPCs in the NPC tab that do not have a more specific color assigned." ), + ColorId.ModdedItemMarker => (0xFFFF20FF, "Modded Item Marker", "The color of dot in the unlocks overview tab signaling that the item is modded in the currently selected Penumbra collection." ), + ColorId.ContainsItemsEnabled => (0xFFA0F0A0, "Enabled Mod Contains Design Items", "The color of enabled mods in the associated mod dropdown menu when they contain items used in this design." ), + ColorId.ContainsItemsDisabled => (0x80A0F0A0, "Disabled Mod Contains Design Items", "The color of disabled mods in the associated mod dropdown menu when they contain items used in this design." ), + ColorId.AdvancedDyeActive => (0xFF58DDFF, "Advanced Dyes Active", "The highlight color for the advanced dye button and marker if any advanced dyes are active for this slot." ), + _ => (0x00000000, string.Empty, string.Empty ), // @formatter:on }; diff --git a/Glamourer/Gui/ConvenienceRevertButtons.cs b/Glamourer/Gui/ConvenienceRevertButtons.cs deleted file mode 100644 index bf8bd11..0000000 --- a/Glamourer/Gui/ConvenienceRevertButtons.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Numerics; -using Dalamud.Interface; -using Glamourer.Automation; -using Glamourer.Events; -using Glamourer.Interop; -using Glamourer.State; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; - -namespace Glamourer.Gui; - -public class ConvenienceRevertButtons -{ - private readonly StateManager _stateManager; - private readonly AutoDesignApplier _autoDesignApplier; - private readonly ObjectManager _objects; - private readonly Configuration _config; - - - public ConvenienceRevertButtons(StateManager stateManager, AutoDesignApplier autoDesignApplier, ObjectManager objects, - Configuration config) - { - _stateManager = stateManager; - _autoDesignApplier = autoDesignApplier; - _objects = objects; - _config = config; - } - - public void DrawButtons(float yPos) - { - _objects.Update(); - var (playerIdentifier, playerData) = _objects.PlayerData; - - string? error = null; - if (!playerIdentifier.IsValid || !playerData.Valid) - error = "No player character available."; - - if (!_stateManager.TryGetValue(playerIdentifier, out var state)) - error = "The player character was not modified by Glamourer yet."; - else if (state.IsLocked) - error = "The state of the player character is currently locked."; - - var buttonSize = new Vector2(ImGui.GetFrameHeight()); - var spacing = ImGui.GetStyle().ItemInnerSpacing; - ImGui.SetCursorPos(new Vector2(ImGui.GetWindowContentRegionMax().X - 2 * buttonSize.X - spacing.X, yPos - 1)); - - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.RedoAlt.ToIconString(), buttonSize, - error ?? "Revert the player character to its game state.", error != null, true)) - _stateManager.ResetState(state, StateChanged.Source.Manual); - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.SyncAlt.ToIconString(), buttonSize, - error ?? "Revert the player character to its automation state.", error != null && _config.EnableAutoDesigns, true)) - foreach (var actor in playerData.Objects) - { - _autoDesignApplier.ReapplyAutomation(actor, playerIdentifier, state); - _stateManager.ReapplyState(actor); - } - } -} diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs index bd7599b..4f463d6 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs @@ -1,27 +1,105 @@ -using System.Numerics; -using Dalamud.Interface; +using Dalamud.Interface; using Dalamud.Interface.Utility; -using Glamourer.Customization; -using ImGuiNET; +using Glamourer.GameData; +using Dalamud.Bindings.ImGui; +using OtterGui; using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Text.EndObjects; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using System; namespace Glamourer.Gui.Customization; public partial class CustomizationDrawer { - private const string ColorPickerPopupName = "ColorPicker"; + private const string ColorPickerPopupName = "ColorPicker"; + private CustomizeValue _draggedColorValue; + private CustomizeIndex _draggedColorType; + + + private void DrawDragDropSource(CustomizeIndex index, CustomizeData custom) + { + using var dragDropSource = ImUtf8.DragDropSource(); + if (!dragDropSource) + return; + + if (!DragDropSource.SetPayload("##colorDragDrop"u8)) + _draggedColorValue = _customize[index]; + ImUtf8.Text( + $"Dragging {(custom.Color == 0 ? $"{_currentOption} (NPC)" : _currentOption)} #{_draggedColorValue.Value}..."); + _draggedColorType = index; + } + + private void DrawDragDropTarget(CustomizeIndex index) + { + using var dragDropTarget = ImUtf8.DragDropTarget(); + if (!dragDropTarget.Success || !dragDropTarget.IsDropping("##colorDragDrop"u8)) + return; + + var idx = _set.DataByValue(_draggedColorType, _draggedColorValue, out var draggedData, _customize.Face); + var bestMatch = _draggedColorValue; + if (draggedData.HasValue) + { + var draggedColor = draggedData.Value.Color; + var targetData = _set.Data(index, idx); + if (targetData.Color != draggedColor) + { + var bestDiff = Diff(targetData.Color, draggedColor); + var count = _set.Count(index); + for (var i = 0; i < count; ++i) + { + targetData = _set.Data(index, i); + if (targetData.Color == draggedColor) + { + UpdateValue(_draggedColorValue); + return; + } + + var diff = Diff(targetData.Color, draggedColor); + if (diff >= bestDiff) + continue; + + bestDiff = diff; + bestMatch = (CustomizeValue)i; + } + } + } + + UpdateValue(bestMatch); + return; + + static uint Diff(uint color1, uint color2) + { + var r = (color1 & 0xFF) - (color2 & 0xFF); + var g = ((color1 >> 8) & 0xFF) - ((color2 >> 8) & 0xFF); + var b = ((color1 >> 16) & 0xFF) - ((color2 >> 16) & 0xFF); + return 30 * r * r + 59 * g * g + 11 * b * b; + } + } private void DrawColorPicker(CustomizeIndex index) { - using var _ = SetId(index); + using var id = SetId(index); var (current, custom) = GetCurrentCustomization(index); var color = ImGui.ColorConvertU32ToFloat4(current < 0 ? ImGui.GetColorU32(ImGuiCol.FrameBg) : custom.Color); - using (var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, current < 0)) + using (_ = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, current < 0)) { - if (ImGui.ColorButton($"{_customize[index].Value}##color", color, ImGuiColorEditFlags.None, _framedIconSize)) + if (ImGui.ColorButton($"{_customize[index].Value}##color", color, ImGuiColorEditFlags.NoDragDrop, _framedIconSize)) + { ImGui.OpenPopup(ColorPickerPopupName); + } + else if (current >= 0 && !_locked && CaptureMouseWheel(ref current, 0, _currentCount)) + { + var data = _set.Data(_currentIndex, current, _customize.Face); + UpdateValue(data.Value); + } + + DrawDragDropSource(index, custom); + DrawDragDropTarget(index); } var npc = false; @@ -37,7 +115,7 @@ public partial class CustomizationDrawer ImGui.SameLine(); - using (var group = ImRaii.Group()) + using (_ = ImRaii.Group()) { DataInputInt(current, npc); if (_withApply) @@ -50,10 +128,10 @@ public partial class CustomizationDrawer ImGui.TextUnformatted(custom.Color == 0 ? $"{_currentOption} (NPC)" : _currentOption); } - DrawColorPickerPopup(); + DrawColorPickerPopup(current); } - private void DrawColorPickerPopup() + private void DrawColorPickerPopup(int current) { using var popup = ImRaii.Popup(ColorPickerPopupName, ImGuiWindowFlags.AlwaysAutoResize); if (!popup) @@ -64,12 +142,19 @@ public partial class CustomizationDrawer for (var i = 0; i < _currentCount; ++i) { var custom = _set.Data(_currentIndex, i, _customize[CustomizeIndex.Face]); - if (ImGui.ColorButton(custom.Value.ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color))) + if (ImGui.ColorButton(custom.Value.ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color)) && !_locked) { UpdateValue(custom.Value); ImGui.CloseCurrentPopup(); } + if (i == current) + { + var size = ImGui.GetItemRectSize(); + ImGui.GetWindowDrawList() + .AddCircleFilled(ImGui.GetItemRectMin() + size / 2, size.X / 4, ImGuiUtil.ContrastColorBw(custom.Color)); + } + if (i % 8 != 7) ImGui.SameLine(); } @@ -80,7 +165,7 @@ public partial class CustomizationDrawer { var current = _set.DataByValue(index, _customize[index], out var custom, _customize.Face); if (_set.IsAvailable(index) && current < 0) - return (current, new CustomizeData(index, _customize[index], 0, 0)); + return (current, new CustomizeData(index, _customize[index])); return (current, custom!.Value); } diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs b/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs index 9cfe301..26e9002 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs @@ -1,12 +1,9 @@ -using System; -using System.Collections.Specialized; -using System.Linq; -using Dalamud.Interface; -using Glamourer.Customization; -using ImGuiNET; +using Dalamud.Interface; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Customization; @@ -18,8 +15,6 @@ public partial class CustomizationDrawer ImGui.SameLine(); using var group = ImRaii.Group(); DrawRaceCombo(); - var gender = _service.AwaitedService.GetName(CustomName.Gender); - var clan = _service.AwaitedService.GetName(CustomName.Clan); if (_withApply) { using var disabled = ImRaii.Disabled(_locked); @@ -34,19 +29,18 @@ public partial class CustomizationDrawer } ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted($"{gender} & {clan}"); + ImGui.TextUnformatted("Gender & Clan"); } private void DrawGenderSelector() { - using (var disabled = ImRaii.Disabled(_locked || _lockedRedraw)) + using (ImRaii.Disabled(_locked || _lockedRedraw)) { var icon = _customize.Gender switch { - Gender.Male when _customize.Race is Race.Hrothgar => FontAwesomeIcon.MarsDouble, - Gender.Male => FontAwesomeIcon.Mars, - Gender.Female => FontAwesomeIcon.Venus, - _ => FontAwesomeIcon.Question, + Gender.Male => FontAwesomeIcon.Mars, + Gender.Female => FontAwesomeIcon.Venus, + _ => FontAwesomeIcon.Question, }; if (ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, @@ -61,7 +55,7 @@ public partial class CustomizationDrawer private void DrawRaceCombo() { - using (var disabled = ImRaii.Disabled(_locked || _lockedRedraw)) + using (ImRaii.Disabled(_locked || _lockedRedraw)) { ImGui.SetNextItemWidth(_raceSelectorWidth); using (var combo = ImRaii.Combo("##subRaceCombo", _service.ClanName(_customize.Clan, _customize.Gender))) @@ -78,4 +72,20 @@ public partial class CustomizationDrawer if (_lockedRedraw && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) ImGui.SetTooltip("The race can not be changed as this requires a redraw of the character, which is not supported for this actor."); } + + private void DrawBodyType() + { + if (_customize.BodyType.Value == 1) + return; + + var label = _lockedRedraw + ? $"Body Type {_customize.BodyType.Value}" + : $"Reset Body Type {_customize.BodyType.Value} to Default"; + if (!ImGuiUtil.DrawDisabledButton(label, new Vector2(_raceSelectorWidth + _framedIconSize.X + ImGui.GetStyle().ItemSpacing.X, 0), + string.Empty, _lockedRedraw)) + return; + + Changed |= CustomizeFlag.BodyType; + _customize.BodyType = (CustomizeValue)1; + } } diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs index 31d593d..8599f8c 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs @@ -1,8 +1,12 @@ -using System.Numerics; -using Glamourer.Customization; -using ImGuiNET; +using Dalamud.Interface.Textures.TextureWraps; +using Glamourer.GameData; +using Glamourer.Unlocks; +using Dalamud.Bindings.ImGui; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Customization; @@ -12,12 +16,13 @@ public partial class CustomizationDrawer private void DrawIconSelector(CustomizeIndex index) { - using var _ = SetId(index); + using var id = SetId(index); using var bigGroup = ImRaii.Group(); var label = _currentOption; - var current = _set.DataByValue(index, _currentByte, out var custom, _customize.Face); - var npc = false; + var current = _set.DataByValue(index, _currentByte, out var custom, _customize.Face); + var originalCurrent = current; + var npc = false; if (current < 0) { label = $"{_currentOption} (NPC)"; @@ -26,17 +31,26 @@ public partial class CustomizationDrawer npc = true; } - var icon = _service.AwaitedService.GetIcon(custom!.Value.IconId); - using (var disabled = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw)) + var icon = _service.Manager.GetIcon(custom!.Value.IconId); + var hasIcon = icon.TryGetWrap(out var wrap, out _); + using (_ = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw)) { - if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) + if (ImGui.ImageButton(wrap?.Handle ?? icon.GetWrapOrEmpty().Handle, _iconSize)) + { ImGui.OpenPopup(IconSelectorPopup); + } + else if (originalCurrent >= 0 && CaptureMouseWheel(ref current, 0, _currentCount)) + { + var data = _set.Data(_currentIndex, current, _customize.Face); + UpdateValue(data.Value); + } } - ImGuiUtil.HoverIconTooltip(icon, _iconSize); + if (hasIcon) + ImGuiUtil.HoverIconTooltip(wrap!, _iconSize); ImGui.SameLine(); - using (var group = ImRaii.Group()) + using (_ = ImRaii.Group()) { DataInputInt(current, npc); if (_lockedRedraw && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) @@ -52,10 +66,10 @@ public partial class CustomizationDrawer ImGui.TextUnformatted(label); } - DrawIconPickerPopup(); + DrawIconPickerPopup(current); } - private void DrawIconPickerPopup() + private void DrawIconPickerPopup(int current) { using var popup = ImRaii.Popup(IconSelectorPopup, ImGuiWindowFlags.AlwaysAutoResize); if (!popup) @@ -66,16 +80,30 @@ public partial class CustomizationDrawer for (var i = 0; i < _currentCount; ++i) { var custom = _set.Data(_currentIndex, i, _customize.Face); - var icon = _service.AwaitedService.GetIcon(custom.IconId); + var icon = _service.Manager.GetIcon(custom.IconId); using (var _ = ImRaii.Group()) { - if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) + var isFavorite = _favorites.Contains(_set.Gender, _set.Clan, _currentIndex, custom.Value); + using var frameColor = current == i + ? ImRaii.PushColor(ImGuiCol.Button, Colors.SelectedRed) + : ImRaii.PushColor(ImGuiCol.Button, ColorId.FavoriteStarOn.Value(), isFavorite); + var hasIcon = icon.TryGetWrap(out var wrap, out var _); + + if (ImGui.ImageButton(wrap?.Handle ?? icon.GetWrapOrEmpty().Handle, _iconSize)) { UpdateValue(custom.Value); ImGui.CloseCurrentPopup(); } - ImGuiUtil.HoverIconTooltip(icon, _iconSize); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + if (isFavorite) + _favorites.Remove(_set.Gender, _set.Clan, _currentIndex, custom.Value); + else + _favorites.TryAdd(_set.Gender, _set.Clan, _currentIndex, custom.Value); + + if (hasIcon) + ImGuiUtil.HoverIconTooltip(wrap!, _iconSize, + FavoriteManager.TypeAllowed(_currentIndex) ? "Right-Click to toggle favorite." : string.Empty); var text = custom.Value.ToString(); var textWidth = ImGui.CalcTextSize(text).X; @@ -110,8 +138,32 @@ public partial class CustomizationDrawer ImGui.SameLine(); ApplyCheckbox(CustomizeIndex.FacialFeature4); } + else + { + ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); + } + + var oldValue = _customize.AtIndex(_currentIndex.ToByteAndMask().ByteIdx); + var tmp = (int)oldValue.Value; + ImGui.SetNextItemWidth(_inputIntSize); + if (ImGui.InputInt("##text", ref tmp, 1, 1)) + { + tmp = Math.Clamp(tmp, 0, byte.MaxValue); + if (tmp != oldValue.Value) + { + _customize.SetByIndex(_currentIndex.ToByteAndMask().ByteIdx, (CustomizeValue)tmp); + var changes = (byte)tmp ^ oldValue.Value; + Changed |= ((changes & 0x01) == 0x01 ? CustomizeFlag.FacialFeature1 : 0) + | ((changes & 0x02) == 0x02 ? CustomizeFlag.FacialFeature2 : 0) + | ((changes & 0x04) == 0x04 ? CustomizeFlag.FacialFeature3 : 0) + | ((changes & 0x08) == 0x08 ? CustomizeFlag.FacialFeature4 : 0) + | ((changes & 0x10) == 0x10 ? CustomizeFlag.FacialFeature5 : 0) + | ((changes & 0x20) == 0x20 ? CustomizeFlag.FacialFeature6 : 0) + | ((changes & 0x40) == 0x40 ? CustomizeFlag.FacialFeature7 : 0) + | ((changes & 0x80) == 0x80 ? CustomizeFlag.LegacyTattoo : 0); + } + } - PercentageInputInt(); if (_set.DataByValue(CustomizeIndex.Face, _customize.Face, out _, _customize.Face) < 0) { ImGui.SameLine(); @@ -124,7 +176,7 @@ public partial class CustomizationDrawer ImGui.AlignTextToFramePadding(); using (var _ = ImRaii.Enabled()) { - ImGui.TextUnformatted(_set.Option(CustomizeIndex.LegacyTattoo)); + ImGui.TextUnformatted("Facial Features & Tattoos"); } if (_withApply) @@ -142,25 +194,36 @@ public partial class CustomizationDrawer private void DrawMultiIcons() { - var options = _set.Order[CharaMakeParams.MenuType.IconCheckmark]; + var options = _set.Order[MenuType.IconCheckmark]; using var group = ImRaii.Group(); var face = _set.DataByValue(CustomizeIndex.Face, _customize.Face, out _, _customize.Face) < 0 ? _set.Faces[0].Value : _customize.Face; foreach (var (featureIdx, idx) in options.WithIndex()) { - using var id = SetId(featureIdx); - var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero; - var feature = _set.Data(featureIdx, 0, face); - var icon = featureIdx == CustomizeIndex.LegacyTattoo - ? _legacyTattoo ?? _service.AwaitedService.GetIcon(feature.IconId) - : _service.AwaitedService.GetIcon(feature.IconId); - if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X, - Vector4.Zero, enabled ? Vector4.One : _redTint)) + using var id = SetId(featureIdx); + var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero; + var feature = _set.Data(featureIdx, 0, face); + bool hasIcon; + IDalamudTextureWrap? wrap; + var icon = _service.Manager.GetIcon(feature.IconId); + if (featureIdx is CustomizeIndex.LegacyTattoo) + { + wrap = _legacyTattoo; + hasIcon = wrap != null; + } + else + { + hasIcon = icon.TryGetWrap(out wrap, out _); + } + + if (ImGui.ImageButton(wrap?.Handle ?? icon.GetWrapOrEmpty().Handle, _iconSize, Vector2.Zero, Vector2.One, + (int)ImGui.GetStyle().FramePadding.X, Vector4.Zero, enabled ? Vector4.One : _redTint)) { _customize.Set(featureIdx, enabled ? CustomizeValue.Zero : CustomizeValue.Max); Changed |= _currentFlag; } - ImGuiUtil.HoverIconTooltip(icon, _iconSize); + if (hasIcon) + ImGuiUtil.HoverIconTooltip(wrap!, _iconSize); if (idx % 4 != 3) ImGui.SameLine(); } diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs index a43ae9e..ec5523f 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs @@ -1,10 +1,9 @@ -using System; -using System.Numerics; -using Glamourer.Customization; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; +using OtterGuiInternal; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Customization; @@ -30,13 +29,37 @@ public partial class CustomizationDrawer ImGui.SameLine(); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(_currentOption); + if (_currentIndex is CustomizeIndex.Height) + DrawHeight(); + } + + private void DrawHeight() + { + if (_config.HeightDisplayType is HeightDisplayType.None) + return; + + var height = _heightService.Height(_customize); + ImGui.SameLine(); + + var heightString = _config.HeightDisplayType switch + { + HeightDisplayType.Centimetre => FormattableString.Invariant($"({height * 100:F1} cm)"), + HeightDisplayType.Metre => FormattableString.Invariant($"({height:F2} m)"), + HeightDisplayType.Wrong => FormattableString.Invariant($"({height * 100 / 2.539:F1} in)"), + HeightDisplayType.WrongFoot => $"({(int)(height * 100 / 2.539 / 12)}'{(int)(height * 100 / 2.539) % 12}'')", + HeightDisplayType.Corgi => FormattableString.Invariant($"({height * 100 / 40.0:F1} Corgis)"), + HeightDisplayType.OlympicPool => FormattableString.Invariant($"({height / 3.0:F3} Pools)"), + _ => FormattableString.Invariant($"({height})"), + }; + ImGui.TextUnformatted(heightString); } private void DrawPercentageSlider() { var tmp = (int)_currentByte.Value; ImGui.SetNextItemWidth(_comboSelectorSize); - if (ImGui.SliderInt("##slider", ref tmp, 0, _currentCount - 1, "%i", ImGuiSliderFlags.AlwaysClamp)) + if (ImGui.SliderInt("##slider", ref tmp, 0, _currentCount - 1, "%i", ImGuiSliderFlags.AlwaysClamp) + || CaptureMouseWheel(ref tmp, 0, _currentCount)) UpdateValue((CustomizeValue)tmp); } @@ -44,11 +67,10 @@ public partial class CustomizationDrawer { var tmp = (int)_currentByte.Value; ImGui.SetNextItemWidth(_inputIntSize); + var cap = ImGui.GetIO().KeyCtrl ? byte.MaxValue : _currentCount - 1; if (ImGui.InputInt("##text", ref tmp, 1, 1)) { - var newValue = (CustomizeValue)(ImGui.GetIO().KeyCtrl - ? Math.Clamp(tmp, 0, byte.MaxValue) - : Math.Clamp(tmp, 0, _currentCount - 1)); + var newValue = (CustomizeValue)Math.Clamp(tmp, 0, cap); UpdateValue(newValue); } @@ -75,6 +97,10 @@ public partial class CustomizationDrawer else if (ImGui.GetIO().KeyCtrl) UpdateValue((CustomizeValue)value); } + else + { + CheckWheel(); + } if (!_withApply) ImGuiUtil.HoverTooltip("Hold Control to force updates with invalid/unknown options at your own risk."); @@ -83,35 +109,51 @@ public partial class CustomizationDrawer if (ImGuiUtil.DrawDisabledButton("-", new Vector2(ImGui.GetFrameHeight()), "Select the previous available option in order.", currentIndex <= 0)) UpdateValue(_set.Data(_currentIndex, currentIndex - 1, _customize.Face).Value); + else + CheckWheel(); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("+", new Vector2(ImGui.GetFrameHeight()), "Select the next available option in order.", currentIndex >= _currentCount - 1 || npc)) UpdateValue(_set.Data(_currentIndex, currentIndex + 1, _customize.Face).Value); + else + CheckWheel(); + return; + + void CheckWheel() + { + if (currentIndex < 0 || !CaptureMouseWheel(ref currentIndex, 0, _currentCount)) + return; + + var data = _set.Data(_currentIndex, currentIndex, _customize.Face); + UpdateValue(data.Value); + } } private void DrawListSelector(CustomizeIndex index, bool indexedBy1) { - using var _ = SetId(index); + using var id = SetId(index); using var bigGroup = ImRaii.Group(); - using var disabled = ImRaii.Disabled(_locked); - if (indexedBy1) + using (_ = ImRaii.Disabled(_locked)) { - ListCombo1(); - ImGui.SameLine(); - ListInputInt1(); - } - else - { - ListCombo0(); - ImGui.SameLine(); - ListInputInt0(); - } + if (indexedBy1) + { + ListCombo1(); + ImGui.SameLine(); + ListInputInt1(); + } + else + { + ListCombo0(); + ImGui.SameLine(); + ListInputInt0(); + } - if (_withApply) - { - ImGui.SameLine(); - ApplyCheckbox(); + if (_withApply) + { + ImGui.SameLine(); + ApplyCheckbox(); + } } ImGui.SameLine(); @@ -122,29 +164,31 @@ public partial class CustomizationDrawer private void ListCombo0() { ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale); - var current = _currentByte.Value; - using var combo = ImRaii.Combo("##combo", $"{_currentOption} #{current + 1}"); - - if (!combo) - return; - - for (var i = 0; i < _currentCount; ++i) + var current = (int)_currentByte.Value; + using (var combo = ImRaii.Combo("##combo", $"{_currentOption} #{current + 1}")) { - if (ImGui.Selectable($"{_currentOption} #{i + 1}##combo", i == current)) - UpdateValue((CustomizeValue)i); + if (combo) + + for (var i = 0; i < _currentCount; ++i) + { + if (ImGui.Selectable($"{_currentOption} #{i + 1}##combo", i == current)) + UpdateValue((CustomizeValue)i); + } } + + if (CaptureMouseWheel(ref current, 0, _currentCount)) + UpdateValue((CustomizeValue)current); } private void ListInputInt0() { var tmp = _currentByte.Value + 1; ImGui.SetNextItemWidth(_inputIntSize); + var cap = ImGui.GetIO().KeyCtrl ? byte.MaxValue + 1 : _currentCount; if (ImGui.InputInt("##text", ref tmp, 1, 1)) { - var newValue = (CustomizeValue)(ImGui.GetIO().KeyCtrl - ? Math.Clamp(tmp, 1, byte.MaxValue + 1) - : Math.Clamp(tmp, 1, _currentCount)); - UpdateValue(newValue - 1); + var newValue = Math.Clamp(tmp, 1, cap); + UpdateValue((CustomizeValue)(newValue - 1)); } ImGuiUtil.HoverTooltip($"Input Range: [1, {_currentCount}]\n" @@ -154,28 +198,29 @@ public partial class CustomizationDrawer private void ListCombo1() { ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale); - var current = _currentByte.Value; - using var combo = ImRaii.Combo("##combo", $"{_currentOption} #{current}"); - - if (!combo) - return; - - for (var i = 1; i <= _currentCount; ++i) + var current = (int)_currentByte.Value; + using (var combo = ImRaii.Combo("##combo", $"{_currentOption} #{current}")) { - if (ImGui.Selectable($"{_currentOption} #{i}##combo", i == current)) - UpdateValue((CustomizeValue)i); + if (combo) + for (var i = 1; i <= _currentCount; ++i) + { + if (ImGui.Selectable($"{_currentOption} #{i}##combo", i == current)) + UpdateValue((CustomizeValue)i); + } } + + if (CaptureMouseWheel(ref current, 1, _currentCount)) + UpdateValue((CustomizeValue)current); } private void ListInputInt1() { var tmp = (int)_currentByte.Value; ImGui.SetNextItemWidth(_inputIntSize); + var (offset, cap) = ImGui.GetIO().KeyCtrl ? (0, byte.MaxValue) : (1, _currentCount); if (ImGui.InputInt("##text", ref tmp, 1, 1)) { - var newValue = (CustomizeValue)(ImGui.GetIO().KeyCtrl - ? Math.Clamp(tmp, 0, byte.MaxValue) - : Math.Clamp(tmp, 1, _currentCount)); + var newValue = (CustomizeValue)Math.Clamp(tmp, offset, cap); UpdateValue(newValue); } @@ -183,6 +228,26 @@ public partial class CustomizationDrawer + "Hold Control to force updates with invalid/unknown options at your own risk."); } + private static bool CaptureMouseWheel(ref int value, int offset, int cap) + { + if (!ImGui.IsItemHovered() || !ImGui.GetIO().KeyCtrl) + return false; + + ImGuiInternal.ItemSetUsingMouseWheel(); + + var mw = (int)ImGui.GetIO().MouseWheel; + if (mw == 0) + return false; + + value -= offset; + value = mw switch + { + < 0 => offset + (value + cap + mw) % cap, + _ => offset + (value + mw) % cap, + }; + return true; + } + // Draw a customize checkbox. private void DrawCheckbox(CustomizeIndex idx) { @@ -192,14 +257,14 @@ public partial class CustomizationDrawer { switch (UiHelpers.DrawMetaToggle(_currentIndex.ToDefaultName(), tmp, _currentApply, out var newValue, out var newApply, _locked)) { - case DataChange.Item: + case (true, false): _customize.Set(idx, newValue ? CustomizeValue.Max : CustomizeValue.Zero); Changed |= _currentFlag; break; - case DataChange.ApplyItem: + case (false, true): ChangeApply = newApply ? ChangeApply | _currentFlag : ChangeApply & ~_currentFlag; break; - case DataChange.Item | DataChange.ApplyItem: + case (true, true): ChangeApply = newApply ? ChangeApply | _currentFlag : ChangeApply & ~_currentFlag; _customize.Set(idx, newValue ? CustomizeValue.Max : CustomizeValue.Zero); Changed |= _currentFlag; @@ -208,7 +273,7 @@ public partial class CustomizationDrawer } else { - using (var disabled = ImRaii.Disabled(_locked)) + using (_ = ImRaii.Disabled(_locked)) { if (ImGui.Checkbox("##toggle", ref tmp)) { @@ -230,7 +295,7 @@ public partial class CustomizationDrawer private void ApplyCheckbox(CustomizeIndex index) { - SetId(index); + using var id = SetId(index); if (UiHelpers.DrawCheckbox("##apply", $"Apply the {_currentOption} customization in this design.", _currentApply, out _, _locked)) ToggleApply(); } diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.cs b/Glamourer/Gui/Customization/CustomizationDrawer.cs index 364b469..349891c 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.cs @@ -1,36 +1,36 @@ -using System; -using System.Numerics; -using System.Reflection; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Plugin; -using Glamourer.Customization; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Plugin.Services; +using Glamourer.GameData; using Glamourer.Services; -using ImGuiNET; +using Glamourer.Unlocks; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; -using CustomizeData = Penumbra.GameData.Structs.CustomizeData; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Customization; -public partial class CustomizationDrawer : IDisposable +public partial class CustomizationDrawer( + ITextureProvider textures, + CustomizeService _service, + Configuration _config, + FavoriteManager _favorites, + HeightService _heightService) + : IDisposable { - private readonly CodeService _codes; - private readonly Configuration _config; - - private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f); - private readonly IDalamudTextureWrap? _legacyTattoo; + private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f); + private readonly IDalamudTextureWrap? _legacyTattoo = GetLegacyTattooIcon(textures); private Exception? _terminate; - private Customize _customize; - private CustomizationSet _set = null!; + private CustomizeArray _customize = CustomizeArray.Default; + private CustomizeSet _set = null!; - public Customize Customize + public CustomizeArray Customize => _customize; - public CustomizeFlag CurrentFlag { get; private set; } public CustomizeFlag Changed { get; private set; } public CustomizeFlag ChangeApply { get; private set; } @@ -46,34 +46,19 @@ public partial class CustomizationDrawer : IDisposable private float _raceSelectorWidth; private bool _withApply; - private readonly CustomizationService _service; - - public CustomizationDrawer(DalamudPluginInterface pi, CustomizationService service, CodeService codes, Configuration config) - { - _service = service; - _codes = codes; - _config = config; - _legacyTattoo = GetLegacyTattooIcon(pi); - _customize = Customize.Default; - } - public void Dispose() - { - _legacyTattoo?.Dispose(); - } + => _legacyTattoo?.Dispose(); - public bool Draw(Customize current, bool locked, bool lockedRedraw) + public bool Draw(CustomizeArray current, bool locked, bool lockedRedraw) { - CurrentFlag = CustomizeFlagExtensions.All; - _withApply = false; + _withApply = false; Init(current, locked, lockedRedraw); return DrawInternal(); } - public bool Draw(Customize current, CustomizeFlag apply, bool locked, bool lockedRedraw) + public bool Draw(CustomizeArray current, CustomizeFlag apply, bool locked, bool lockedRedraw) { - CurrentFlag = CustomizeFlagExtensions.All; ChangeApply = apply; _initialApply = apply; _withApply = !_config.HideApplyCheckmarks; @@ -81,12 +66,12 @@ public partial class CustomizationDrawer : IDisposable return DrawInternal(); } - private void Init(Customize current, bool locked, bool lockedRedraw) + private void Init(CustomizeArray current, bool locked, bool lockedRedraw) { UpdateSizes(); - _terminate = null; - Changed = 0; - _customize.Load(current); + _terminate = null; + Changed = 0; + _customize = current; _locked = locked; _lockedRedraw = lockedRedraw; } @@ -125,40 +110,33 @@ public partial class CustomizationDrawer : IDisposable Changed |= _currentFlag; } - public bool DrawWetnessState(bool currentValue, out bool newValue, bool locked) - => UiHelpers.DrawCheckbox("Force Wetness", "Force the character to be wet or not.", currentValue, out newValue, locked); - - public DataChange DrawWetnessState(bool currentValue, bool currentApply, out bool newValue, out bool newApply, bool locked) - => UiHelpers.DrawMetaToggle("Force Wetness", currentValue, currentApply, out newValue, out newApply, locked); - private bool DrawInternal() { using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _spacing); try { - if (_codes.EnabledArtisan) - return DrawArtisan(); - DrawRaceGenderSelector(); - _set = _service.AwaitedService.GetList(_customize.Clan, _customize.Gender); + DrawBodyType(); - foreach (var id in _set.Order[CharaMakeParams.MenuType.Percentage]) + _set = _service.Manager.GetSet(_customize.Clan, _customize.Gender); + + foreach (var id in _set.Order[MenuType.Percentage]) PercentageSelector(id); - Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.IconSelector], DrawIconSelector, ImGui.SameLine); + Functions.IteratePairwise(_set.Order[MenuType.IconSelector], DrawIconSelector, ImGui.SameLine); DrawMultiIconSelector(); - foreach (var id in _set.Order[CharaMakeParams.MenuType.ListSelector]) + foreach (var id in _set.Order[MenuType.ListSelector]) DrawListSelector(id, false); - foreach (var id in _set.Order[CharaMakeParams.MenuType.List1Selector]) + foreach (var id in _set.Order[MenuType.List1Selector]) DrawListSelector(id, true); - Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.ColorPicker], DrawColorPicker, ImGui.SameLine); + Functions.IteratePairwise(_set.Order[MenuType.ColorPicker], DrawColorPicker, ImGui.SameLine); - Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.Checkmark], DrawCheckbox, + Functions.IteratePairwise(_set.Order[MenuType.Checkmark], DrawCheckbox, () => ImGui.SameLine(_comboSelectorSize - _framedIconSize.X + ImGui.GetStyle().WindowPadding.X)); return Changed != 0 || ChangeApply != _initialApply; } @@ -172,43 +150,18 @@ public partial class CustomizationDrawer : IDisposable } } - private unsafe bool DrawArtisan() - { - for (var i = 0; i < CustomizeData.Size; ++i) - { - using var id = ImRaii.PushId(i); - int value = _customize.Data.Data[i]; - ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt(string.Empty, ref value, 0, 0)) - { - var newValue = (byte)Math.Clamp(value, 0, byte.MaxValue); - if (newValue != _customize.Data.Data[i]) - foreach (var flag in Enum.GetValues()) - { - var (j, mask) = flag.ToByteAndMask(); - if (j == i) - Changed |= flag.ToFlag(); - } - - _customize.Data.Data[i] = newValue; - } - } - - return Changed != 0; - } - private void UpdateSizes() { - _spacing = ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X }; - _iconSize = new Vector2(ImGui.GetTextLineHeight() * 2 + ImGui.GetStyle().ItemSpacing.Y + 2 * ImGui.GetStyle().FramePadding.Y); - _framedIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding; - _inputIntSize = 2 * _framedIconSize.X + 1 * _spacing.X; + _spacing = ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X }; + _iconSize = new Vector2(ImGui.GetTextLineHeight() * 2 + _spacing.Y + 2 * ImGui.GetStyle().FramePadding.Y); + _framedIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding; + _inputIntSize = 2 * _framedIconSize.X + 1 * _spacing.X; _inputIntSizeNoButtons = _inputIntSize - 2 * _spacing.X - 2 * ImGui.GetFrameHeight(); - _comboSelectorSize = 4 * _framedIconSize.X + 3 * _spacing.X; - _raceSelectorWidth = _inputIntSize + _comboSelectorSize - _framedIconSize.X; + _comboSelectorSize = 4 * _framedIconSize.X + 3 * _spacing.X; + _raceSelectorWidth = _inputIntSize + _comboSelectorSize - _framedIconSize.X; } - private static IDalamudTextureWrap? GetLegacyTattooIcon(DalamudPluginInterface pi) + private static IDalamudTextureWrap? GetLegacyTattooIcon(ITextureProvider textures) { using var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("Glamourer.LegacyTattoo.raw"); if (resource == null) @@ -217,7 +170,7 @@ public partial class CustomizationDrawer : IDisposable var rawImage = new byte[resource.Length]; var length = resource.Read(rawImage, 0, (int)resource.Length); return length == resource.Length - ? pi.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4) + ? textures.CreateFromRaw(RawImageSpecification.Rgba32(192, 192), rawImage, "Glamourer.LegacyTattoo") : null; } } diff --git a/Glamourer/Gui/Customization/CustomizeParameterDrawData.cs b/Glamourer/Gui/Customization/CustomizeParameterDrawData.cs new file mode 100644 index 0000000..421caed --- /dev/null +++ b/Glamourer/Gui/Customization/CustomizeParameterDrawData.cs @@ -0,0 +1,50 @@ +using Glamourer.Designs; +using Glamourer.GameData; +using Glamourer.State; + +namespace Glamourer.Gui.Customization; + +public struct CustomizeParameterDrawData(CustomizeParameterFlag flag, in DesignData data) +{ + private IDesignEditor _editor = null!; + private object _object = null!; + public readonly CustomizeParameterFlag Flag = flag; + public bool Locked; + public bool DisplayApplication; + public bool AllowRevert; + + public readonly void ChangeParameter(CustomizeParameterValue value) + => _editor.ChangeCustomizeParameter(_object, Flag, value, ApplySettings.Manual); + + public readonly void ChangeApplyParameter(bool value) + { + var manager = (DesignManager)_editor; + var design = (Design)_object; + manager.ChangeApplyParameter(design, Flag, value); + } + + public CustomizeParameterValue CurrentValue = data.Parameters[flag]; + public CustomizeParameterValue GameValue; + public bool CurrentApply; + + public static CustomizeParameterDrawData FromDesign(DesignManager manager, Design design, CustomizeParameterFlag flag) + => new(flag, design.DesignData) + { + _editor = manager, + _object = design, + Locked = design.WriteProtected(), + DisplayApplication = true, + CurrentApply = design.DoApplyParameter(flag), + }; + + public static CustomizeParameterDrawData FromState(StateManager manager, ActorState state, CustomizeParameterFlag flag) + => new(flag, state.ModelData) + { + _editor = manager, + _object = state, + Locked = state.IsLocked, + DisplayApplication = false, + GameValue = state.BaseData.Parameters[flag], + AllowRevert = true, + }; +} diff --git a/Glamourer/Gui/Customization/CustomizeParameterDrawer.cs b/Glamourer/Gui/Customization/CustomizeParameterDrawer.cs new file mode 100644 index 0000000..4db6b14 --- /dev/null +++ b/Glamourer/Gui/Customization/CustomizeParameterDrawer.cs @@ -0,0 +1,315 @@ +using Dalamud.Interface; +using Glamourer.Designs; +using Glamourer.GameData; +using Glamourer.Interop.PalettePlus; +using Glamourer.State; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; + +namespace Glamourer.Gui.Customization; + +public class CustomizeParameterDrawer(Configuration config, PaletteImport import) : IService +{ + private readonly Dictionary _lastData = []; + private string _paletteName = string.Empty; + private CustomizeParameterData _data; + private CustomizeParameterFlag _flags; + private float _width; + private CustomizeParameterValue? _copy; + + public void Draw(DesignManager designManager, Design design) + { + using var generalSize = EnsureSize(); + DrawPaletteImport(designManager, design); + DrawConfig(true); + + using (_ = ImRaii.ItemWidth(_width - 2 * ImGui.GetFrameHeight() - 2 * ImGui.GetStyle().ItemInnerSpacing.X)) + { + foreach (var flag in CustomizeParameterExtensions.RgbFlags) + DrawColorInput3(CustomizeParameterDrawData.FromDesign(designManager, design, flag), true); + + foreach (var flag in CustomizeParameterExtensions.RgbaFlags) + DrawColorInput4(CustomizeParameterDrawData.FromDesign(designManager, design, flag)); + } + + foreach (var flag in CustomizeParameterExtensions.PercentageFlags) + DrawPercentageInput(CustomizeParameterDrawData.FromDesign(designManager, design, flag)); + + foreach (var flag in CustomizeParameterExtensions.ValueFlags) + DrawValueInput(CustomizeParameterDrawData.FromDesign(designManager, design, flag)); + } + + public void Draw(StateManager stateManager, ActorState state) + { + using var generalSize = EnsureSize(); + DrawConfig(false); + using (_ = ImRaii.ItemWidth(_width - 2 * ImGui.GetFrameHeight() - 2 * ImGui.GetStyle().ItemInnerSpacing.X)) + { + foreach (var flag in CustomizeParameterExtensions.RgbFlags) + DrawColorInput3(CustomizeParameterDrawData.FromState(stateManager, state, flag), state.ModelData.Customize.Highlights); + + foreach (var flag in CustomizeParameterExtensions.RgbaFlags) + DrawColorInput4(CustomizeParameterDrawData.FromState(stateManager, state, flag)); + } + + foreach (var flag in CustomizeParameterExtensions.PercentageFlags) + DrawPercentageInput(CustomizeParameterDrawData.FromState(stateManager, state, flag)); + + foreach (var flag in CustomizeParameterExtensions.ValueFlags) + DrawValueInput(CustomizeParameterDrawData.FromState(stateManager, state, flag)); + } + + private void DrawPaletteCombo() + { + using var id = ImRaii.PushId("Palettes"); + using var combo = ImRaii.Combo("##import", _paletteName.Length > 0 ? _paletteName : "Select Palette..."); + if (!combo) + return; + + foreach (var (name, (palette, flags)) in import.Data) + { + if (!ImGui.Selectable(name, _paletteName == name)) + continue; + + _paletteName = name; + _data = palette; + _flags = flags; + } + } + + private void DrawPaletteImport(DesignManager manager, Design design) + { + if (!config.ShowPalettePlusImport) + return; + + var spacing = ImGui.GetStyle().ItemInnerSpacing.X; + + DrawPaletteCombo(); + + ImGui.SameLine(0, spacing); + var value = true; + if (ImGui.Checkbox("Show Import", ref value)) + { + config.ShowPalettePlusImport = false; + config.Save(); + } + + ImGuiUtil.HoverTooltip("Hide the Palette+ Import bar from all designs. You can re-enable it in Glamourers interface settings."); + + var buttonWidth = new Vector2((_width - spacing) / 2, 0); + var tt = _paletteName.Length > 0 + ? $"Apply the imported data from the Palette+ palette [{_paletteName}] to this design." + : "Please select a palette first."; + if (ImGuiUtil.DrawDisabledButton("Apply Import", buttonWidth, tt, _paletteName.Length == 0 || design.WriteProtected())) + { + _lastData[design] = design.DesignData.Parameters; + foreach (var parameter in _flags.Iterate()) + manager.ChangeCustomizeParameter(design, parameter, _data[parameter]); + } + + ImGui.SameLine(0, spacing); + var enabled = _lastData.TryGetValue(design, out var oldData); + tt = enabled + ? $"Revert to the last set of advanced customization parameters of [{design.Name}] before importing." + : $"You have not imported any data that could be reverted for [{design.Name}]."; + if (ImGuiUtil.DrawDisabledButton("Revert Import", buttonWidth, tt, !enabled || design.WriteProtected())) + { + _lastData.Remove(design); + foreach (var parameter in CustomizeParameterExtensions.AllFlags) + manager.ChangeCustomizeParameter(design, parameter, oldData[parameter]); + } + } + + + private void DrawConfig(bool withApply) + { + if (!config.ShowColorConfig) + return; + + DrawColorDisplayOptions(); + DrawColorFormatOptions(withApply); + var value = config.ShowColorConfig; + ImGui.SameLine(); + if (ImGui.Checkbox("Show Config", ref value)) + { + config.ShowColorConfig = value; + config.Save(); + } + + ImGuiUtil.HoverTooltip( + "Hide the color configuration options from the Advanced Customization panel. You can re-enable it in Glamourers interface settings."); + } + + private void DrawColorDisplayOptions() + { + using var group = ImRaii.Group(); + if (ImGui.RadioButton("RGB", config.UseRgbForColors) && !config.UseRgbForColors) + { + config.UseRgbForColors = true; + config.Save(); + } + + ImGui.SameLine(); + if (ImGui.RadioButton("HSV", !config.UseRgbForColors) && config.UseRgbForColors) + { + config.UseRgbForColors = false; + config.Save(); + } + } + + private void DrawColorFormatOptions(bool withApply) + { + var width = _width + - (ImGui.CalcTextSize("Float").X + + ImGui.CalcTextSize("Integer").X + + 2 * (ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.X) + + ImGui.GetStyle().ItemInnerSpacing.X + + ImGui.GetItemRectSize().X); + if (!withApply) + width -= ImGui.GetFrameHeight() + ImGui.GetStyle().ItemInnerSpacing.X; + + ImGui.SameLine(0, width); + if (ImGui.RadioButton("Float", config.UseFloatForColors) && !config.UseFloatForColors) + { + config.UseFloatForColors = true; + config.Save(); + } + + ImGui.SameLine(); + if (ImGui.RadioButton("Integer", !config.UseFloatForColors) && config.UseFloatForColors) + { + config.UseFloatForColors = false; + config.Save(); + } + } + + private void DrawColorInput3(in CustomizeParameterDrawData data, bool allowHighlights) + { + using var id = ImRaii.PushId((int)data.Flag); + var value = data.CurrentValue.InternalTriple; + var noHighlights = !allowHighlights && data.Flag is CustomizeParameterFlag.HairHighlight; + DrawCopyPasteButtons(data, data.Locked || noHighlights); + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + using (_ = ImRaii.Disabled(data.Locked || noHighlights)) + { + if (ImGui.ColorEdit3("##value", ref value, GetFlags())) + data.ChangeParameter(new CustomizeParameterValue(value)); + } + + if (noHighlights) + ImGuiUtil.HoverTooltip("Highlights are disabled in your regular customizations.", ImGuiHoveredFlags.AllowWhenDisabled); + + DrawRevert(data); + + DrawApplyAndLabel(data); + } + + private void DrawColorInput4(in CustomizeParameterDrawData data) + { + using var id = ImRaii.PushId((int)data.Flag); + var value = data.CurrentValue.InternalQuadruple; + DrawCopyPasteButtons(data, data.Locked); + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + using (_ = ImRaii.Disabled(data.Locked)) + { + if (ImGui.ColorEdit4("##value", ref value, GetFlags() | ImGuiColorEditFlags.AlphaPreviewHalf)) + data.ChangeParameter(new CustomizeParameterValue(value)); + } + + DrawRevert(data); + + DrawApplyAndLabel(data); + } + + private void DrawValueInput(in CustomizeParameterDrawData data) + { + using var id = ImRaii.PushId((int)data.Flag); + var value = data.CurrentValue[0]; + + using (_ = ImRaii.Disabled(data.Locked)) + { + if (ImGui.InputFloat("##value", ref value, 0.1f, 0.5f)) + data.ChangeParameter(new CustomizeParameterValue(value)); + } + + DrawRevert(data); + + DrawApplyAndLabel(data); + } + + private void DrawPercentageInput(in CustomizeParameterDrawData data) + { + using var id = ImRaii.PushId((int)data.Flag); + var value = data.CurrentValue[0] * 100f; + + using (_ = ImRaii.Disabled(data.Locked)) + { + if (ImGui.SliderFloat("##value", ref value, -100f, 300, "%.2f")) + data.ChangeParameter(new CustomizeParameterValue(value / 100f)); + ImGuiUtil.HoverTooltip("You can control-click this to enter arbitrary values by hand instead of dragging."); + } + + DrawRevert(data); + + DrawApplyAndLabel(data); + } + + private static void DrawRevert(in CustomizeParameterDrawData data) + { + if (data.Locked || !data.AllowRevert) + return; + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) + data.ChangeParameter(data.GameValue); + + ImGuiUtil.HoverTooltip("Hold Control and Right-click to revert to game values."); + } + + private static void DrawApply(in CustomizeParameterDrawData data) + { + if (UiHelpers.DrawCheckbox("##apply", "Apply this custom parameter when applying the Design.", data.CurrentApply, out var enabled, + data.Locked)) + data.ChangeApplyParameter(enabled); + } + + private void DrawApplyAndLabel(in CustomizeParameterDrawData data) + { + if (data.DisplayApplication && !config.HideApplyCheckmarks) + { + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + DrawApply(data); + } + + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.TextUnformatted(data.Flag.ToName()); + } + + private ImGuiColorEditFlags GetFlags() + => Format | Display | ImGuiColorEditFlags.Hdr | ImGuiColorEditFlags.NoOptions; + + private ImGuiColorEditFlags Format + => config.UseFloatForColors ? ImGuiColorEditFlags.Float : ImGuiColorEditFlags.Uint8; + + private ImGuiColorEditFlags Display + => config.UseRgbForColors ? ImGuiColorEditFlags.DisplayRgb : ImGuiColorEditFlags.DisplayHsv; + + private ImRaii.IEndObject EnsureSize() + { + var iconSize = ImGui.GetTextLineHeight() * 2 + ImGui.GetStyle().ItemSpacing.Y + 4 * ImGui.GetStyle().FramePadding.Y; + _width = 7 * iconSize + 4 * ImGui.GetStyle().ItemInnerSpacing.X; + return ImRaii.ItemWidth(_width); + } + + private void DrawCopyPasteButtons(in CustomizeParameterDrawData data, bool locked) + { + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Copy.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Copy this color for later use.", false, true)) + _copy = data.CurrentValue; + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + _copy.HasValue ? "Paste the currently copied value." : "No value copied yet.", locked || !_copy.HasValue, true)) + data.ChangeParameter(_copy!.Value); + } +} diff --git a/Glamourer/Gui/DesignCombo.cs b/Glamourer/Gui/DesignCombo.cs new file mode 100644 index 0000000..2d8880e --- /dev/null +++ b/Glamourer/Gui/DesignCombo.cs @@ -0,0 +1,375 @@ +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Glamourer.Automation; +using Glamourer.Designs; +using Glamourer.Designs.History; +using Glamourer.Designs.Special; +using Glamourer.Events; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Extensions; +using OtterGui.Log; +using OtterGui.Widgets; + +namespace Glamourer.Gui; + +public abstract class DesignComboBase : FilterComboCache>, IDisposable +{ + protected readonly EphemeralConfig Config; + protected readonly DesignChanged DesignChanged; + protected readonly DesignColors DesignColors; + protected readonly TabSelected TabSelected; + protected float InnerWidth; + private IDesignStandIn? _currentDesign; + private bool _isCurrentSelectionDirty; + + protected DesignComboBase(Func>> generator, Logger log, DesignChanged designChanged, + TabSelected tabSelected, EphemeralConfig config, DesignColors designColors) + : base(generator, MouseWheelType.Control, log) + { + DesignChanged = designChanged; + TabSelected = tabSelected; + Config = config; + DesignColors = designColors; + DesignChanged.Subscribe(OnDesignChanged, DesignChanged.Priority.DesignCombo); + } + + public bool Incognito + => Config.IncognitoMode; + + void IDisposable.Dispose() + { + DesignChanged.Unsubscribe(OnDesignChanged); + GC.SuppressFinalize(this); + } + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var (design, path) = Items[globalIdx]; + bool ret; + switch (design) + { + case Design realDesign: + { + using var color = ImRaii.PushColor(ImGuiCol.Text, DesignColors.GetColor(realDesign)); + ret = base.DrawSelectable(globalIdx, selected); + DrawPath(path, realDesign); + return ret; + } + case QuickSelectedDesign quickDesign: + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.NormalDesign.Value()); + ret = base.DrawSelectable(globalIdx, selected); + DrawResolvedDesign(quickDesign); + return ret; + } + default: return base.DrawSelectable(globalIdx, selected); + } + } + + private static void DrawPath(string path, Design realDesign) + { + if (path.Length <= 0 || realDesign.Name == path) + return; + + DrawRightAligned(realDesign.Name, path, ImGui.GetColorU32(ImGuiCol.TextDisabled)); + } + + private void DrawResolvedDesign(QuickSelectedDesign quickDesign) + { + var linkedDesign = quickDesign.CurrentDesign; + if (linkedDesign != null) + DrawRightAligned(quickDesign.ResolveName(false), linkedDesign.Name.Text, DesignColors.GetColor(linkedDesign)); + else + DrawRightAligned(quickDesign.ResolveName(false), "[Nothing]", DesignColors.MissingColor); + } + + protected bool Draw(IDesignStandIn? currentDesign, string? label, float width) + { + _currentDesign = currentDesign; + UpdateCurrentSelection(); + InnerWidth = 400 * ImGuiHelpers.GlobalScale; + var name = label ?? "Select Design Here..."; + bool ret; + using (_ = currentDesign != null ? ImRaii.PushColor(ImGuiCol.Text, DesignColors.GetColor(currentDesign as Design)) : null) + { + ret = Draw("##design", name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) + && CurrentSelection != null; + } + + if (currentDesign is Design design) + { + if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) + TabSelected.Invoke(MainWindow.TabType.Designs, design); + ImGuiUtil.HoverTooltip("Control + Right-Click to move to design."); + } + + QuickSelectedDesignTooltip(currentDesign); + + _currentDesign = null; + return ret; + } + + protected override string ToString(Tuple obj) + => obj.Item1.ResolveName(Incognito); + + protected override float GetFilterWidth() + => InnerWidth - 2 * ImGui.GetStyle().FramePadding.X; + + protected override bool IsVisible(int globalIndex, LowerString filter) + { + var (design, path) = Items[globalIndex]; + return filter.IsContained(path) || filter.IsContained(design.ResolveName(false)); + } + + protected override void OnMouseWheel(string preview, ref int _2, int steps) + { + if (!ReferenceEquals(_currentDesign, CurrentSelection?.Item1)) + CurrentSelectionIdx = -1; + + base.OnMouseWheel(preview, ref _2, steps); + } + + private void UpdateCurrentSelection() + { + if (!_isCurrentSelectionDirty) + return; + + var priorState = IsInitialized; + if (priorState) + Cleanup(); + CurrentSelectionIdx = Items.IndexOf(s => ReferenceEquals(s.Item1, CurrentSelection?.Item1)); + if (CurrentSelectionIdx >= 0) + { + UpdateSelection(Items[CurrentSelectionIdx]); + } + else if (Items.Count > 0) + { + CurrentSelectionIdx = 0; + UpdateSelection(Items[0]); + } + else + { + UpdateSelection(null); + } + + if (!priorState) + Cleanup(); + _isCurrentSelectionDirty = false; + } + + protected override int UpdateCurrentSelected(int currentSelected) + { + CurrentSelectionIdx = Items.IndexOf(p => _currentDesign == p.Item1); + UpdateSelection(CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : null); + return CurrentSelectionIdx; + } + + private void OnDesignChanged(DesignChanged.Type type, Design? _1, ITransaction? _2 = null) + { + _isCurrentSelectionDirty = type switch + { + DesignChanged.Type.Created => true, + DesignChanged.Type.Renamed => true, + DesignChanged.Type.ChangedColor => true, + DesignChanged.Type.Deleted => true, + DesignChanged.Type.QuickDesignBar => true, + _ => _isCurrentSelectionDirty, + }; + } + + private void QuickSelectedDesignTooltip(IDesignStandIn? design) + { + if (!ImGui.IsItemHovered()) + return; + + if (design is not QuickSelectedDesign q) + return; + + using var tt = ImRaii.Tooltip(); + var linkedDesign = q.CurrentDesign; + if (linkedDesign != null) + { + ImGui.TextUnformatted("Currently resolving to "); + using var color = ImRaii.PushColor(ImGuiCol.Text, DesignColors.GetColor(linkedDesign)); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted(linkedDesign.Name.Text); + } + else + { + ImGui.TextUnformatted("No design selected in the Quick Design Bar."); + } + } + + private static void DrawRightAligned(string leftText, string text, uint color) + { + var start = ImGui.GetItemRectMin(); + var pos = start.X + ImGui.CalcTextSize(leftText).X; + var maxSize = ImGui.GetWindowPos().X + ImGui.GetWindowContentRegionMax().X; + var remainingSpace = maxSize - pos; + var requiredSize = ImGui.CalcTextSize(text).X + ImGui.GetStyle().ItemInnerSpacing.X; + var offset = remainingSpace - requiredSize; + if (ImGui.GetScrollMaxY() == 0) + offset -= ImGui.GetStyle().ItemInnerSpacing.X; + + if (offset < ImGui.GetStyle().ItemSpacing.X) + ImGuiUtil.HoverTooltip(text); + else + ImGui.GetWindowDrawList().AddText(start with { X = pos + offset }, + color, text); + } +} + +public abstract class DesignCombo : DesignComboBase +{ + protected DesignCombo(Logger log, DesignChanged designChanged, TabSelected tabSelected, + EphemeralConfig config, DesignColors designColors, Func>> generator) + : base(generator, log, designChanged, tabSelected, config, designColors) + { + if (Items.Count == 0) + return; + + CurrentSelection = Items[0]; + CurrentSelectionIdx = 0; + base.Cleanup(); + } + + public IDesignStandIn? Design + => CurrentSelection?.Item1; + + public void Draw(float width) + => Draw(Design, Design?.ResolveName(Incognito) ?? string.Empty, width); +} + +public sealed class QuickDesignCombo : DesignCombo +{ + public QuickDesignCombo(DesignFileSystem fileSystem, + Logger log, + DesignChanged designChanged, + TabSelected tabSelected, + EphemeralConfig config, + DesignColors designColors) + : base(log, designChanged, tabSelected, config, designColors, () => + [ + .. fileSystem + .Where(kvp => kvp.Key.QuickDesign) + .Select(kvp => new Tuple(kvp.Key, kvp.Value.FullName())) + .OrderBy(d => d.Item2), + ]) + { + if (config.SelectedQuickDesign != Guid.Empty) + { + CurrentSelectionIdx = Items.IndexOf(t => t.Item1 is Design d && d.Identifier == config.SelectedQuickDesign); + if (CurrentSelectionIdx >= 0) + CurrentSelection = Items[CurrentSelectionIdx]; + else if (Items.Count > 0) + CurrentSelectionIdx = 0; + } + + AllowMouseWheel = MouseWheelType.Unmodified; + SelectionChanged += OnSelectionChange; + } + + private void OnSelectionChange(Tuple? old, Tuple? @new) + { + if (old == null) + { + if (@new?.Item1 is not Design d) + return; + + Config.SelectedQuickDesign = d.Identifier; + Config.Save(); + } + else if (@new?.Item1 is not Design d) + { + Config.SelectedQuickDesign = Guid.Empty; + Config.Save(); + } + else if (!old.Item1.Equals(@new.Item1)) + { + Config.SelectedQuickDesign = d.Identifier; + Config.Save(); + } + } +} + +public sealed class LinkDesignCombo( + DesignFileSystem fileSystem, + Logger log, + DesignChanged designChanged, + TabSelected tabSelected, + EphemeralConfig config, + DesignColors designColors) + : DesignCombo(log, designChanged, tabSelected, config, designColors, () => + [ + .. fileSystem + .Select(kvp => new Tuple(kvp.Key, kvp.Value.FullName())) + .OrderBy(d => d.Item2), + ]); + +public sealed class RandomDesignCombo( + DesignManager designs, + DesignFileSystem fileSystem, + Logger log, + DesignChanged designChanged, + TabSelected tabSelected, + EphemeralConfig config, + DesignColors designColors) + : DesignCombo(log, designChanged, tabSelected, config, designColors, () => + [ + .. fileSystem + .Select(kvp => new Tuple(kvp.Key, kvp.Value.FullName())) + .OrderBy(d => d.Item2), + ]) +{ + private Design? GetDesign(RandomPredicate.Exact exact) + { + return exact.Which switch + { + RandomPredicate.Exact.Type.Name => designs.Designs.FirstOrDefault(d => d.Name == exact.Value), + RandomPredicate.Exact.Type.Path => fileSystem.Find(exact.Value.Text, out var c) && c is DesignFileSystem.Leaf l ? l.Value : null, + RandomPredicate.Exact.Type.Identifier => designs.Designs.ByIdentifier(Guid.TryParse(exact.Value.Text, out var g) ? g : Guid.Empty), + _ => null, + }; + } + + public bool Draw(RandomPredicate.Exact exact, float width) + { + var design = GetDesign(exact); + return Draw(design, design?.ResolveName(Incognito) ?? $"Not Found [{exact.Value.Text}]", width); + } + + public bool Draw(IDesignStandIn? design, float width) + => Draw(design, design?.ResolveName(Incognito) ?? string.Empty, width); +} + +public sealed class SpecialDesignCombo( + DesignFileSystem fileSystem, + TabSelected tabSelected, + DesignColors designColors, + Logger log, + DesignChanged designChanged, + AutoDesignManager autoDesignManager, + EphemeralConfig config, + RandomDesignGenerator rng, + QuickSelectedDesign quickSelectedDesign) + : DesignComboBase(() => fileSystem + .Select(kvp => new Tuple(kvp.Key, kvp.Value.FullName())) + .OrderBy(d => d.Item2) + .Prepend(new Tuple(new RandomDesign(rng), string.Empty)) + .Prepend(new Tuple(quickSelectedDesign, string.Empty)) + .Prepend(new Tuple(new RevertDesign(), string.Empty)) + .ToList(), log, designChanged, tabSelected, config, designColors) +{ + public void Draw(AutoDesignSet set, AutoDesign? design, int autoDesignIndex) + { + if (!Draw(design?.Design, design?.Design.ResolveName(Incognito), ImGui.GetContentRegionAvail().X)) + return; + + if (autoDesignIndex >= 0) + autoDesignManager.ChangeDesign(set, autoDesignIndex, CurrentSelection!.Item1); + else + autoDesignManager.AddDesign(set, CurrentSelection!.Item1); + } +} diff --git a/Glamourer/Gui/DesignQuickBar.cs b/Glamourer/Gui/DesignQuickBar.cs new file mode 100644 index 0000000..e8c0ce3 --- /dev/null +++ b/Glamourer/Gui/DesignQuickBar.cs @@ -0,0 +1,567 @@ +using Dalamud.Game.ClientState.Keys; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin.Services; +using Glamourer.Automation; +using Glamourer.Designs; +using Glamourer.Interop.Penumbra; +using Glamourer.State; +using Dalamud.Bindings.ImGui; +using OtterGui.Classes; +using OtterGui.Text; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; + +namespace Glamourer.Gui; + +[Flags] +public enum QdbButtons +{ + ApplyDesign = 0x01, + RevertAll = 0x02, + RevertAutomation = 0x04, + RevertAdvancedDyes = 0x08, + RevertEquip = 0x10, + RevertCustomize = 0x20, + ReapplyAutomation = 0x40, + ResetSettings = 0x80, + RevertAdvancedCustomization = 0x100, +} + +public sealed class DesignQuickBar : Window, IDisposable +{ + private ImGuiWindowFlags GetFlags + => _config.Ephemeral.LockDesignQuickBar + ? ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoMove + : ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing; + + private readonly Configuration _config; + private readonly QuickDesignCombo _designCombo; + private readonly StateManager _stateManager; + private readonly AutoDesignApplier _autoDesignApplier; + private readonly ActorObjectManager _objects; + private readonly PenumbraService _penumbra; + private readonly IKeyState _keyState; + private readonly ImRaii.Style _windowPadding = new(); + private readonly ImRaii.Color _windowColor = new(); + private DateTime _keyboardToggle = DateTime.UnixEpoch; + private int _numButtons; + private readonly StringBuilder _tooltipBuilder = new(512); + + public DesignQuickBar(Configuration config, QuickDesignCombo designCombo, StateManager stateManager, IKeyState keyState, + ActorObjectManager objects, AutoDesignApplier autoDesignApplier, PenumbraService penumbra) + : base("Glamourer Quick Bar", ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking) + { + _config = config; + _designCombo = designCombo; + _stateManager = stateManager; + _keyState = keyState; + _objects = objects; + _autoDesignApplier = autoDesignApplier; + _penumbra = penumbra; + IsOpen = _config.Ephemeral.ShowDesignQuickBar; + DisableWindowSounds = true; + Size = Vector2.Zero; + RespectCloseHotkey = false; + } + + public void Dispose() + => _windowPadding.Dispose(); + + public override void PreOpenCheck() + { + CheckHotkeys(); + IsOpen = _config.Ephemeral.ShowDesignQuickBar && _config.QdbButtons != 0; + } + + public override bool DrawConditions() + => _objects.Player.Valid; + + public override void PreDraw() + { + Flags = GetFlags; + UpdateWidth(); + + _windowPadding.Push(ImGuiStyleVar.WindowPadding, new Vector2(ImGuiHelpers.GlobalScale * 4)) + .Push(ImGuiStyleVar.WindowBorderSize, 0); + _windowColor.Push(ImGuiCol.WindowBg, ColorId.QuickDesignBg.Value()) + .Push(ImGuiCol.Button, ColorId.QuickDesignButton.Value()) + .Push(ImGuiCol.FrameBg, ColorId.QuickDesignFrame.Value()); + } + + public override void PostDraw() + { + _windowPadding.Dispose(); + _windowColor.Dispose(); + } + + public void DrawAtEnd(float yPos) + { + var width = UpdateWidth(); + ImGui.SetCursorPos(new Vector2(ImGui.GetWindowContentRegionMax().X - width, yPos - ImGuiHelpers.GlobalScale)); + Draw(); + } + + public override void Draw() + => Draw(ImGui.GetContentRegionAvail().X); + + private void Draw(float width) + { + using var group = ImUtf8.Group(); + var spacing = ImGui.GetStyle().ItemInnerSpacing; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); + var buttonSize = new Vector2(ImGui.GetFrameHeight()); + PrepareButtons(); + if (_config.QdbButtons.HasFlag(QdbButtons.ApplyDesign)) + { + var comboSize = width - _numButtons * (buttonSize.X + spacing.X); + _designCombo.Draw(comboSize); + ImGui.SameLine(); + DrawApplyButton(buttonSize); + } + + DrawRevertButton(buttonSize); + DrawRevertEquipButton(buttonSize); + DrawRevertCustomizeButton(buttonSize); + DrawRevertAdvancedCustomization(buttonSize); + DrawRevertAdvancedDyes(buttonSize); + DrawRevertAutomationButton(buttonSize); + DrawReapplyAutomationButton(buttonSize); + DrawResetSettingsButton(buttonSize); + } + + private ActorIdentifier _playerIdentifier; + private ActorData _playerData; + private ActorState? _playerState; + + private ActorData _targetData; + private ActorIdentifier _targetIdentifier; + private ActorState? _targetState; + + private void PrepareButtons() + { + (_playerIdentifier, _playerData) = _objects.PlayerData; + (_targetIdentifier, _targetData) = _objects.TargetData; + _playerState = _stateManager.GetValueOrDefault(_playerIdentifier); + _targetState = _stateManager.GetValueOrDefault(_targetIdentifier); + } + + private void DrawApplyButton(Vector2 size) + { + var design = _designCombo.Design as Design; + var available = 0; + _tooltipBuilder.Clear(); + + if (design == null) + { + _tooltipBuilder.Append("No design selected."); + } + else + { + if (_playerIdentifier.IsValid && _playerData.Valid) + { + available |= 1; + _tooltipBuilder.Append("Left-Click: Apply ") + .Append(design.ResolveName(_config.Ephemeral.IncognitoMode)) + .Append(" to yourself."); + } + + if (_targetIdentifier.IsValid && _targetData.Valid) + { + if (available != 0) + _tooltipBuilder.Append('\n'); + available |= 2; + _tooltipBuilder.Append("Right-Click: Apply ") + .Append(design.ResolveName(_config.Ephemeral.IncognitoMode)) + .Append(" to ").Append(_config.Ephemeral.IncognitoMode ? _targetIdentifier.Incognito(null) : _targetIdentifier.ToName()); + } + + if (available == 0) + _tooltipBuilder.Append("Neither player character nor target available."); + } + + + var (clicked, id, data, state) = ResolveTarget(FontAwesomeIcon.PlayCircle, size, available); + ImGui.SameLine(); + if (!clicked) + return; + + if (state == null && !_stateManager.GetOrCreate(id, data.Objects[0], out state)) + { + Glamourer.Messager.NotificationMessage( + $"Could not apply {design!.ResolveName(true)} to {id.Incognito(null)}: Failed to create state."); + return; + } + + using var _ = design!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); + _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { IsFinal = true }); + } + + private void DrawRevertButton(Vector2 buttonSize) + { + if (!_config.QdbButtons.HasFlag(QdbButtons.RevertAll)) + return; + + var available = 0; + _tooltipBuilder.Clear(); + + if (_playerIdentifier.IsValid && _playerState is { IsLocked: false }) + { + available |= 1; + _tooltipBuilder.Append("Left-Click: Revert the player character to their game state."); + } + + if (_targetIdentifier.IsValid && _targetState is { IsLocked: false }) + { + if (available != 0) + _tooltipBuilder.Append('\n'); + available |= 2; + _tooltipBuilder.Append("Right-Click: Revert ") + .Append(_targetIdentifier) + .Append(" to their game state."); + } + + if (available == 0) + _tooltipBuilder.Append( + "Neither player character nor target are available, have state modified by Glamourer, or their state is locked."); + + var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.UndoAlt, buttonSize, available); + ImGui.SameLine(); + if (clicked) + _stateManager.ResetState(state!, StateSource.Manual, isFinal: true); + } + + private void DrawRevertAutomationButton(Vector2 buttonSize) + { + if (!_config.EnableAutoDesigns) + return; + + if (!_config.QdbButtons.HasFlag(QdbButtons.RevertAutomation)) + return; + + var available = 0; + _tooltipBuilder.Clear(); + + if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) + { + available |= 1; + _tooltipBuilder.Append("Left-Click: Revert the player character to their automation state."); + } + + if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) + { + if (available != 0) + _tooltipBuilder.Append('\n'); + available |= 2; + _tooltipBuilder.Append("Right-Click: Revert ") + .Append(_targetIdentifier) + .Append(" to their automation state."); + } + + if (available == 0) + _tooltipBuilder.Append( + "Neither player character nor target are available, have state modified by Glamourer, or their state is locked."); + + var (clicked, id, data, state) = ResolveTarget(FontAwesomeIcon.SyncAlt, buttonSize, available); + ImGui.SameLine(); + if (!clicked) + return; + + foreach (var actor in data.Objects) + { + _autoDesignApplier.ReapplyAutomation(actor, id, state!, true, false, out var forcedRedraw); + _stateManager.ReapplyAutomationState(actor, forcedRedraw, true, StateSource.Manual); + } + } + + private void DrawReapplyAutomationButton(Vector2 buttonSize) + { + if (!_config.EnableAutoDesigns) + return; + + if (!_config.QdbButtons.HasFlag(QdbButtons.ReapplyAutomation)) + return; + + var available = 0; + _tooltipBuilder.Clear(); + + if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) + { + available |= 1; + _tooltipBuilder.Append("Left-Click: Reapply the player character's current automation on top of their current state."); + } + + if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) + { + if (available != 0) + _tooltipBuilder.Append('\n'); + available |= 2; + _tooltipBuilder.Append("Right-Click: Reapply ") + .Append(_targetIdentifier) + .Append("'s current automation on top of their current state."); + } + + if (available == 0) + _tooltipBuilder.Append( + "Neither player character nor target are available, have state modified by Glamourer, or their state is locked."); + + var (clicked, id, data, state) = ResolveTarget(FontAwesomeIcon.Repeat, buttonSize, available); + ImGui.SameLine(); + if (!clicked) + return; + + foreach (var actor in data.Objects) + { + _autoDesignApplier.ReapplyAutomation(actor, id, state!, false, false, out var forcedRedraw); + _stateManager.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Manual); + } + } + + private void DrawRevertAdvancedCustomization(Vector2 buttonSize) + { + if (!_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedCustomization)) + return; + + var available = 0; + _tooltipBuilder.Clear(); + + if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) + { + available |= 1; + _tooltipBuilder.Append("Left-Click: Revert the advanced customizations of the player character to their game state."); + } + + if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) + { + if (available != 0) + _tooltipBuilder.Append('\n'); + available |= 2; + _tooltipBuilder.Append("Right-Click: Revert the advanced customizations of ") + .Append(_targetIdentifier) + .Append(" to their game state."); + } + + if (available == 0) + _tooltipBuilder.Append("Neither player character nor target are available or their state is locked."); + + var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.PaintBrush, buttonSize, available); + ImGui.SameLine(); + if (clicked) + _stateManager.ResetAdvancedCustomizations(state!, StateSource.Manual); + } + + private void DrawRevertAdvancedDyes(Vector2 buttonSize) + { + if (!_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedDyes)) + return; + + var available = 0; + _tooltipBuilder.Clear(); + + if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) + { + available |= 1; + _tooltipBuilder.Append("Left-Click: Revert the advanced dyes of the player character to their game state."); + } + + if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) + { + if (available != 0) + _tooltipBuilder.Append('\n'); + available |= 2; + _tooltipBuilder.Append("Right-Click: Revert the advanced dyes of ") + .Append(_targetIdentifier) + .Append(" to their game state."); + } + + if (available == 0) + _tooltipBuilder.Append("Neither player character nor target are available or their state is locked."); + + var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.Palette, buttonSize, available); + ImGui.SameLine(); + if (clicked) + _stateManager.ResetAdvancedDyes(state!, StateSource.Manual); + } + + private void DrawRevertCustomizeButton(Vector2 buttonSize) + { + if (!_config.QdbButtons.HasFlag(QdbButtons.RevertCustomize)) + return; + + var available = 0; + _tooltipBuilder.Clear(); + + if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) + { + available |= 1; + _tooltipBuilder.Append("Left-Click: Revert the customizations of the player character to their game state."); + } + + if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) + { + if (available != 0) + _tooltipBuilder.Append('\n'); + available |= 2; + _tooltipBuilder.Append("Right-Click: Revert the customizations of ") + .Append(_targetIdentifier) + .Append(" to their game state."); + } + + if (available == 0) + _tooltipBuilder.Append("Neither player character nor target are available or their state is locked."); + + var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.User, buttonSize, available); + ImGui.SameLine(); + if (clicked) + _stateManager.ResetCustomize(state!, StateSource.Manual); + } + + private void DrawRevertEquipButton(Vector2 buttonSize) + { + if (!_config.QdbButtons.HasFlag(QdbButtons.RevertEquip)) + return; + + var available = 0; + _tooltipBuilder.Clear(); + + if (_playerIdentifier.IsValid && _playerState is { IsLocked: false } && _playerData.Valid) + { + available |= 1; + _tooltipBuilder.Append("Left-Click: Revert the equipment of the player character to its game state."); + } + + if (_targetIdentifier.IsValid && _targetState is { IsLocked: false } && _targetData.Valid) + { + if (available != 0) + _tooltipBuilder.Append('\n'); + available |= 2; + _tooltipBuilder.Append("Right-Click: Revert the equipment of ") + .Append(_targetIdentifier) + .Append(" to its game state."); + } + + if (available == 0) + _tooltipBuilder.Append("Neither player character nor target are available or their state is locked."); + + var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.Vest, buttonSize, available); + ImGui.SameLine(); + if (clicked) + _stateManager.ResetEquip(state!, StateSource.Manual); + } + + private void DrawResetSettingsButton(Vector2 buttonSize) + { + if (!_config.QdbButtons.HasFlag(QdbButtons.ResetSettings)) + return; + + var available = 0; + _tooltipBuilder.Clear(); + + if (_playerIdentifier.IsValid && _playerData.Valid) + { + available |= 1; + _tooltipBuilder + .Append( + "Left-Click: Reset all temporary settings applied by Glamourer (manually or through automation) to the collection affecting ") + .Append(_playerIdentifier) + .Append('.'); + } + + if (_targetIdentifier.IsValid && _targetData.Valid) + { + if (available != 0) + _tooltipBuilder.Append('\n'); + available |= 2; + _tooltipBuilder + .Append( + "Right-Click: Reset all temporary settings applied by Glamourer (manually or through automation) to the collection affecting ") + .Append(_targetIdentifier) + .Append('.'); + } + + if (available == 0) + _tooltipBuilder.Append("Neither player character nor target are available to identify their collections."); + + var (clicked, _, data, _) = ResolveTarget(FontAwesomeIcon.Cog, buttonSize, available); + ImGui.SameLine(); + if (clicked) + { + _penumbra.RemoveAllTemporarySettings(data.Objects[0].Index, StateSource.Manual); + _penumbra.RemoveAllTemporarySettings(data.Objects[0].Index, StateSource.Fixed); + } + } + + private (bool, ActorIdentifier, ActorData, ActorState?) ResolveTarget(FontAwesomeIcon icon, Vector2 buttonSize, int available) + { + var enumerator = _tooltipBuilder.GetChunks(); + var span = enumerator.MoveNext() ? enumerator.Current.Span : []; + ImUtf8.IconButton(icon, span, buttonSize, available == 0); + if ((available & 1) == 1 && ImGui.IsItemClicked(ImGuiMouseButton.Left)) + return (true, _playerIdentifier, _playerData, _playerState); + if ((available & 2) == 2 && ImGui.IsItemClicked(ImGuiMouseButton.Right)) + return (true, _targetIdentifier, _targetData, _targetState); + + return (false, ActorIdentifier.Invalid, ActorData.Invalid, null); + } + + private void CheckHotkeys() + { + if (_keyboardToggle > DateTime.UtcNow || !CheckKeyState(_config.ToggleQuickDesignBar, false)) + return; + + _keyboardToggle = DateTime.UtcNow.AddMilliseconds(500); + _config.Ephemeral.ShowDesignQuickBar = !_config.Ephemeral.ShowDesignQuickBar; + _config.Ephemeral.Save(); + } + + private bool CheckKeyState(ModifiableHotkey key, bool noKey) + { + if (key.Hotkey == VirtualKey.NO_KEY) + return noKey; + + return _keyState[key.Hotkey] && key.Modifier1.IsActive() && key.Modifier2.IsActive(); + } + + private float UpdateWidth() + { + _numButtons = 0; + if (_config.QdbButtons.HasFlag(QdbButtons.RevertAll)) + ++_numButtons; + if (_config.EnableAutoDesigns) + { + if (_config.QdbButtons.HasFlag(QdbButtons.RevertAutomation)) + ++_numButtons; + if (_config.QdbButtons.HasFlag(QdbButtons.ReapplyAutomation)) + ++_numButtons; + } + + if (_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedCustomization)) + ++_numButtons; + if (_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedDyes)) + ++_numButtons; + if (_config.QdbButtons.HasFlag(QdbButtons.RevertCustomize)) + ++_numButtons; + if (_config.QdbButtons.HasFlag(QdbButtons.RevertEquip)) + ++_numButtons; + if (_config.UseTemporarySettings && _config.QdbButtons.HasFlag(QdbButtons.ResetSettings)) + ++_numButtons; + if (_config.QdbButtons.HasFlag(QdbButtons.ApplyDesign)) + { + ++_numButtons; + Size = new Vector2((7 + _numButtons) * ImGui.GetFrameHeight() + _numButtons * ImGui.GetStyle().ItemInnerSpacing.X, + ImGui.GetFrameHeight()); + } + else + { + Size = new Vector2( + _numButtons * ImGui.GetFrameHeight() + + (_numButtons - 1) * ImGui.GetStyle().ItemInnerSpacing.X + + ImGui.GetStyle().WindowPadding.X * 2, + ImGui.GetFrameHeight()); + } + + return Size.Value.X; + } +} diff --git a/Glamourer/Gui/Equipment/BonusDrawData.cs b/Glamourer/Gui/Equipment/BonusDrawData.cs new file mode 100644 index 0000000..067c0c6 --- /dev/null +++ b/Glamourer/Gui/Equipment/BonusDrawData.cs @@ -0,0 +1,61 @@ +using Glamourer.Designs; +using Glamourer.Interop.Material; +using Glamourer.State; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Equipment; + +public struct BonusDrawData(BonusItemFlag slot, in DesignData designData) +{ + private IDesignEditor _editor = null!; + private object _object = null!; + public readonly BonusItemFlag Slot = slot; + public bool Locked; + public bool DisplayApplication; + public bool AllowRevert; + public bool HasAdvancedDyes; + + public readonly bool IsDesign + => _object is Design; + + public readonly bool IsState + => _object is ActorState; + + public readonly void SetItem(EquipItem item) + => _editor.ChangeBonusItem(_object, Slot, item, ApplySettings.Manual); + + public readonly void SetApplyItem(bool value) + { + var manager = (DesignManager)_editor; + var design = (Design)_object; + manager.ChangeApplyBonusItem(design, Slot, value); + } + + public EquipItem CurrentItem = designData.BonusItem(slot); + public EquipItem GameItem = default; + public bool CurrentApply; + + public static BonusDrawData FromDesign(DesignManager manager, Design design, BonusItemFlag slot) + => new(slot, design.DesignData) + { + _editor = manager, + _object = design, + CurrentApply = design.DoApplyBonusItem(slot), + Locked = design.WriteProtected(), + DisplayApplication = true, + HasAdvancedDyes = design.GetMaterialDataRef().CheckExistenceSlot(MaterialValueIndex.FromSlot(slot)), + }; + + public static BonusDrawData FromState(StateManager manager, ActorState state, BonusItemFlag slot) + => new(slot, state.ModelData) + { + _editor = manager, + _object = state, + Locked = state.IsLocked, + DisplayApplication = false, + GameItem = state.BaseData.BonusItem(slot), + AllowRevert = true, + HasAdvancedDyes = state.Materials.CheckExistenceSlot(MaterialValueIndex.FromSlot(slot)), + }; +} diff --git a/Glamourer/Gui/Equipment/BonusItemCombo.cs b/Glamourer/Gui/Equipment/BonusItemCombo.cs new file mode 100644 index 0000000..aa43da7 --- /dev/null +++ b/Glamourer/Gui/Equipment/BonusItemCombo.cs @@ -0,0 +1,121 @@ +using Dalamud.Plugin.Services; +using Glamourer.Services; +using Glamourer.Unlocks; +using Dalamud.Bindings.ImGui; +using Lumina.Excel.Sheets; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Extensions; +using OtterGui.Log; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Equipment; + +public sealed class BonusItemCombo : FilterComboCache +{ + private readonly FavoriteManager _favorites; + public readonly string Label; + private CustomItemId _currentItem; + private float _innerWidth; + + public PrimaryId CustomSetId { get; private set; } + public Variant CustomVariant { get; private set; } + + public BonusItemCombo(IDataManager gameData, ItemManager items, BonusItemFlag slot, Logger log, FavoriteManager favorites) + : base(() => GetItems(favorites, items, slot), MouseWheelType.Control, log) + { + _favorites = favorites; + Label = GetLabel(gameData, slot); + _currentItem = 0; + SearchByParts = true; + } + + protected override void DrawList(float width, float itemHeight) + { + base.DrawList(width, itemHeight); + if (NewSelection != null && Items.Count > NewSelection.Value) + CurrentSelection = Items[NewSelection.Value]; + } + + protected override int UpdateCurrentSelected(int currentSelected) + { + if (CurrentSelection.Id == _currentItem) + return currentSelected; + + CurrentSelectionIdx = Items.IndexOf(i => i.Id == _currentItem); + CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default; + return base.UpdateCurrentSelected(CurrentSelectionIdx); + } + + public bool Draw(string previewName, BonusItemId previewIdx, float width, float innerWidth) + { + _innerWidth = innerWidth; + _currentItem = previewIdx; + CustomVariant = 0; + return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); + } + + protected override float GetFilterWidth() + => _innerWidth - 2 * ImGui.GetStyle().FramePadding.X; + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var obj = Items[globalIdx]; + var name = ToString(obj); + if (UiHelpers.DrawFavoriteStar(_favorites, obj) && CurrentSelectionIdx == globalIdx) + { + CurrentSelectionIdx = -1; + _currentItem = obj.Id; + CurrentSelection = default; + } + + ImGui.SameLine(); + var ret = ImGui.Selectable(name, selected); + ImGui.SameLine(); + using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080); + ImGuiUtil.RightAlign($"({obj.PrimaryId.Id}-{obj.Variant.Id})"); + return ret; + } + + protected override bool IsVisible(int globalIndex, LowerString filter) + => base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].PrimaryId.Id.ToString()); + + protected override string ToString(EquipItem obj) + => obj.Name; + + private static string GetLabel(IDataManager gameData, BonusItemFlag slot) + { + var sheet = gameData.GetExcelSheet()!; + + return slot switch + { + BonusItemFlag.Glasses => sheet.TryGetRow(16050, out var text) ? text.Text.ToString() : "Facewear", + BonusItemFlag.UnkSlot => sheet.TryGetRow(16051, out var text) ? text.Text.ToString() : "Facewear", + + _ => string.Empty, + }; + } + + private static List GetItems(FavoriteManager favorites, ItemManager items, BonusItemFlag slot) + { + var nothing = EquipItem.BonusItemNothing(slot); + return items.ItemData.ByType[slot.ToEquipType()].OrderByDescending(favorites.Contains).ThenBy(i => i.Id.Id).Prepend(nothing).ToList(); + } + + protected override void OnClosePopup() + { + // If holding control while the popup closes, try to parse the input as a full pair of set id and variant, and set a custom item for that. + if (!ImGui.GetIO().KeyCtrl) + return; + + var split = Filter.Text.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length != 2 || !ushort.TryParse(split[0], out var setId) || !byte.TryParse(split[1], out var variant)) + return; + + CustomSetId = setId; + CustomVariant = variant; + } +} diff --git a/Glamourer/Gui/Equipment/EquipDrawData.cs b/Glamourer/Gui/Equipment/EquipDrawData.cs new file mode 100644 index 0000000..f32e22b --- /dev/null +++ b/Glamourer/Gui/Equipment/EquipDrawData.cs @@ -0,0 +1,80 @@ +using Glamourer.Designs; +using Glamourer.Interop.Material; +using Glamourer.State; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Equipment; + +public struct EquipDrawData(EquipSlot slot, in DesignData designData) +{ + private IDesignEditor _editor = null!; + private object _object = null!; + public readonly EquipSlot Slot = slot; + public bool Locked; + public bool DisplayApplication; + public bool AllowRevert; + + public readonly bool IsDesign + => _object is Design; + + public readonly bool IsState + => _object is ActorState; + + public readonly void SetItem(EquipItem item) + => _editor.ChangeItem(_object, Slot, item, ApplySettings.Manual); + + public readonly void SetStains(StainIds stains) + => _editor.ChangeStains(_object, Slot, stains, ApplySettings.Manual); + + public readonly void SetStain(int which, StainId stain) + => _editor.ChangeStains(_object, Slot, CurrentStains.With(which, stain), ApplySettings.Manual); + + public readonly void SetApplyItem(bool value) + { + var manager = (DesignManager)_editor; + manager.ChangeApplyItem((Design)_object, Slot, value); + } + + public readonly void SetApplyStain(bool value) + { + var manager = (DesignManager)_editor; + manager.ChangeApplyStains((Design)_object, Slot, value); + } + + public EquipItem CurrentItem = designData.Item(slot); + public StainIds CurrentStains = designData.Stain(slot); + public EquipItem GameItem = default; + public StainIds GameStains = default; + public bool CurrentApply; + public bool CurrentApplyStain; + public bool HasAdvancedDyes; + + public readonly Gender CurrentGender = designData.Customize.Gender; + public readonly Race CurrentRace = designData.Customize.Race; + + public static EquipDrawData FromDesign(DesignManager manager, Design design, EquipSlot slot) + => new(slot, design.DesignData) + { + _editor = manager, + _object = design, + CurrentApply = design.DoApplyEquip(slot), + CurrentApplyStain = design.DoApplyStain(slot), + Locked = design.WriteProtected(), + HasAdvancedDyes = design.GetMaterialDataRef().CheckExistenceSlot(MaterialValueIndex.FromSlot(slot)), + DisplayApplication = true, + }; + + public static EquipDrawData FromState(StateManager manager, ActorState state, EquipSlot slot) + => new(slot, state.ModelData) + { + _editor = manager, + _object = state, + Locked = state.IsLocked, + DisplayApplication = false, + GameItem = state.BaseData.Item(slot), + GameStains = state.BaseData.Stain(slot), + HasAdvancedDyes = state.Materials.CheckExistenceSlot(MaterialValueIndex.FromSlot(slot)), + AllowRevert = true, + }; +} diff --git a/Glamourer/Gui/Equipment/EquipItemSlotCache.cs b/Glamourer/Gui/Equipment/EquipItemSlotCache.cs new file mode 100644 index 0000000..20aaf11 --- /dev/null +++ b/Glamourer/Gui/Equipment/EquipItemSlotCache.cs @@ -0,0 +1,83 @@ +using Glamourer.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Equipment; + +[InlineArray(13)] +public struct EquipItemSlotCache +{ + private EquipItem _element; + + public EquipItem Dragged + { + get => this[^1]; + set => this[^1] = value; + } + + public void Clear() + => ((Span)this).Clear(); + + public EquipItem this[EquipSlot slot] + { + get => this[(int)slot.ToIndex()]; + set => this[(int)slot.ToIndex()] = value; + } + + public void Update(ItemManager items, in EquipItem item, EquipSlot startSlot) + { + if (item.Id == Dragged.Id && item.Type == Dragged.Type) + return; + + switch (startSlot) + { + case EquipSlot.MainHand: + { + Clear(); + this[EquipSlot.MainHand] = item; + if (item.Type is FullEquipType.Sword) + this[EquipSlot.OffHand] = items.FindClosestShield(item.ItemId, out var shield) ? shield : default; + else + this[EquipSlot.OffHand] = items.ItemData.Secondary.GetValueOrDefault(item.ItemId); + break; + } + case EquipSlot.OffHand: + { + Clear(); + if (item.Type is FullEquipType.Shield) + this[EquipSlot.MainHand] = items.FindClosestSword(item.ItemId, out var sword) ? sword : default; + else + this[EquipSlot.MainHand] = items.ItemData.Primary.GetValueOrDefault(item.ItemId); + this[EquipSlot.OffHand] = item; + break; + } + default: + { + this[EquipSlot.MainHand] = default; + this[EquipSlot.OffHand] = default; + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + if (startSlot == slot) + { + this[slot] = item; + continue; + } + + var slotItem = items.Identify(slot, item.PrimaryId, item.Variant); + if (!slotItem.Valid || slotItem.ItemId.Id is not 0 != item.ItemId.Id is not 0) + { + slotItem = items.Identify(EquipSlot.OffHand, item.PrimaryId, item.SecondaryId, 1, item.Type); + if (slotItem.ItemId.Id is not 0 != item.ItemId.Id is not 0) + slotItem = default; + } + + this[slot] = slotItem; + } + + break; + } + } + + Dragged = item; + } +} diff --git a/Glamourer/Gui/Equipment/EquipmentDrawer.cs b/Glamourer/Gui/Equipment/EquipmentDrawer.cs index 75ca0ab..01ec938 100644 --- a/Glamourer/Gui/Equipment/EquipmentDrawer.cs +++ b/Glamourer/Gui/Equipment/EquipmentDrawer.cs @@ -1,21 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Numerics; -using Dalamud.Interface.Components; +using Dalamud.Interface.Components; using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; -using Glamourer.Designs; using Glamourer.Events; +using Glamourer.Gui.Materials; using Glamourer.Services; -using Glamourer.Structs; using Glamourer.Unlocks; -using ImGuiNET; -using OtterGui; +using Dalamud.Bindings.ImGui; +using OtterGui.Extensions; using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Text.EndObjects; using OtterGui.Widgets; using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -26,735 +23,678 @@ public class EquipmentDrawer private const float DefaultWidth = 280; private readonly ItemManager _items; - private readonly FilterComboColors _stainCombo; - private readonly StainData _stainData; + private readonly GlamourerColorCombo _stainCombo; + private readonly DictStain _stainData; private readonly ItemCombo[] _itemCombo; + private readonly BonusItemCombo[] _bonusItemCombo; private readonly Dictionary _weaponCombo; - private readonly CodeService _codes; private readonly TextureService _textures; private readonly Configuration _config; private readonly GPoseService _gPose; + private readonly AdvancedDyePopup _advancedDyes; + private readonly ItemCopyService _itemCopy; private float _requiredComboWidthUnscaled; private float _requiredComboWidth; - public EquipmentDrawer(FavoriteManager favorites, IDataManager gameData, ItemManager items, CodeService codes, TextureService textures, - Configuration config, - GPoseService gPose) + private Stain? _draggedStain; + private EquipItemSlotCache _draggedItem; + private EquipSlot _dragTarget; + + public EquipmentDrawer(FavoriteManager favorites, IDataManager gameData, ItemManager items, TextureService textures, + Configuration config, GPoseService gPose, AdvancedDyePopup advancedDyes, ItemCopyService itemCopy) { - _items = items; - _codes = codes; - _textures = textures; - _config = config; - _gPose = gPose; - _stainData = items.Stains; - _stainCombo = new FilterComboColors(DefaultWidth - 20, - _stainData.Data.Prepend(new KeyValuePair(0, ("None", 0, false))), Glamourer.Log); - _itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e, Glamourer.Log, favorites)).ToArray(); - _weaponCombo = new Dictionary(FullEquipTypeExtensions.WeaponTypes.Count * 2); + _items = items; + _textures = textures; + _config = config; + _gPose = gPose; + _advancedDyes = advancedDyes; + _itemCopy = itemCopy; + _stainData = items.Stains; + _stainCombo = new GlamourerColorCombo(DefaultWidth - 20, _stainData, favorites); + _itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e, Glamourer.Log, favorites)).ToArray(); + _bonusItemCombo = BonusExtensions.AllFlags.Select(f => new BonusItemCombo(gameData, items, f, Glamourer.Log, favorites)).ToArray(); + _weaponCombo = new Dictionary(FullEquipTypeExtensions.WeaponTypes.Count * 2); foreach (var type in Enum.GetValues()) { if (type.ToSlot() is EquipSlot.MainHand) - _weaponCombo.TryAdd(type, new WeaponCombo(items, type, Glamourer.Log)); + _weaponCombo.TryAdd(type, new WeaponCombo(items, type, Glamourer.Log, favorites)); else if (type.ToSlot() is EquipSlot.OffHand) - _weaponCombo.TryAdd(type, new WeaponCombo(items, type, Glamourer.Log)); + _weaponCombo.TryAdd(type, new WeaponCombo(items, type, Glamourer.Log, favorites)); } - _weaponCombo.Add(FullEquipType.Unknown, new WeaponCombo(items, FullEquipType.Unknown, Glamourer.Log)); + _weaponCombo.Add(FullEquipType.Unknown, new WeaponCombo(items, FullEquipType.Unknown, Glamourer.Log, favorites)); } private Vector2 _iconSize; private float _comboLength; + private uint _advancedMaterialColor; public void Prepare() { _iconSize = new Vector2(2 * ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y); _comboLength = DefaultWidth * ImGuiHelpers.GlobalScale; if (_requiredComboWidthUnscaled == 0) - _requiredComboWidthUnscaled = _items.ItemService.AwaitedService.AllItems(true) - .Concat(_items.ItemService.AwaitedService.AllItems(false)) + _requiredComboWidthUnscaled = _items.ItemData.AllItems(true) + .Concat(_items.ItemData.AllItems(false)) .Max(i => ImGui.CalcTextSize($"{i.Item2.Name} ({i.Item2.ModelString})").X) / ImGuiHelpers.GlobalScale; - _requiredComboWidth = _requiredComboWidthUnscaled * ImGuiHelpers.GlobalScale; + _requiredComboWidth = _requiredComboWidthUnscaled * ImGuiHelpers.GlobalScale; + _advancedMaterialColor = ColorId.AdvancedDyeActive.Value(); + _dragTarget = EquipSlot.Unknown; } - private bool VerifyRestrictedGear(EquipSlot slot, EquipItem gear, Gender gender, Race race) + private bool VerifyRestrictedGear(EquipDrawData data) { - if (slot.IsAccessory()) + if (data.Slot.IsAccessory()) return false; - var (changed, _) = _items.ResolveRestrictedGear(gear.Armor(), slot, race, gender); + var (changed, _) = _items.ResolveRestrictedGear(data.CurrentItem.Armor(), data.Slot, data.CurrentRace, data.CurrentGender); return changed; } - - public DataChange DrawEquip(EquipSlot slot, in DesignData designData, out EquipItem rArmor, out StainId rStain, EquipFlag? cApply, - out bool rApply, out bool rApplyStain, bool locked) - => DrawEquip(slot, designData.Item(slot), out rArmor, designData.Stain(slot), out rStain, cApply, out rApply, out rApplyStain, locked, - designData.Customize.Gender, designData.Customize.Race); - - public DataChange DrawEquip(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, EquipFlag? cApply, - out bool rApply, out bool rApplyStain, bool locked, Gender gender = Gender.Unknown, Race race = Race.Unknown) + public void DrawEquip(EquipDrawData equipDrawData) { if (_config.HideApplyCheckmarks) - cApply = null; + equipDrawData.DisplayApplication = false; - using var id = ImRaii.PushId((int)slot); + using var id = ImUtf8.PushId((int)equipDrawData.Slot); var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); if (_config.SmallEquip) - return DrawEquipSmall(slot, cArmor, out rArmor, cStain, out rStain, cApply, out rApply, out rApplyStain, locked, gender, race); - - if (!locked && _codes.EnabledArtisan) - return DrawEquipArtisan(slot, cArmor, out rArmor, cStain, out rStain, cApply, out rApply, out rApplyStain); - - return DrawEquipNormal(slot, cArmor, out rArmor, cStain, out rStain, cApply, out rApply, out rApplyStain, locked, gender, race); + DrawEquipSmall(equipDrawData); + else + DrawEquipNormal(equipDrawData); } - public DataChange DrawWeapons(in DesignData designData, out EquipItem rMainhand, out EquipItem rOffhand, out StainId rMainhandStain, - out StainId rOffhandStain, EquipFlag? cApply, bool allWeapons, out bool rApplyMainhand, out bool rApplyMainhandStain, - out bool rApplyOffhand, out bool rApplyOffhandStain, bool locked) - => DrawWeapons(designData.Item(EquipSlot.MainHand), out rMainhand, designData.Item(EquipSlot.OffHand), out rOffhand, - designData.Stain(EquipSlot.MainHand), out rMainhandStain, designData.Stain(EquipSlot.OffHand), out rOffhandStain, cApply, - allWeapons, out rApplyMainhand, out rApplyMainhandStain, out rApplyOffhand, out rApplyOffhandStain, locked); - - private DataChange DrawWeapons(EquipItem cMainhand, out EquipItem rMainhand, EquipItem cOffhand, out EquipItem rOffhand, - StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, EquipFlag? cApply, - bool allWeapons, out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyOffhand, out bool rApplyOffhandStain, - bool locked) + public void DrawBonusItem(BonusDrawData bonusDrawData) { - if (cMainhand.ModelId.Id == 0) - { - rOffhand = cOffhand; - rMainhand = cMainhand; - rMainhandStain = cMainhandStain; - rOffhandStain = cOffhandStain; - rApplyMainhand = false; - rApplyMainhandStain = false; - rApplyOffhand = false; - rApplyOffhandStain = false; - return DataChange.None; - } - if (_config.HideApplyCheckmarks) - cApply = null; + bonusDrawData.DisplayApplication = false; - using var id = ImRaii.PushId("Weapons"); + using var id = ImUtf8.PushId(100 + (int)bonusDrawData.Slot); var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); if (_config.SmallEquip) - return DrawWeaponsSmall(cMainhand, out rMainhand, cOffhand, out rOffhand, cMainhandStain, out rMainhandStain, cOffhandStain, - out rOffhandStain, cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyOffhand, out rApplyOffhandStain, locked, - allWeapons); - - if (!locked && _codes.EnabledArtisan) - return DrawWeaponsArtisan(cMainhand, out rMainhand, cOffhand, out rOffhand, cMainhandStain, out rMainhandStain, cOffhandStain, - out rOffhandStain, cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyOffhand, out rApplyOffhandStain); - - return DrawWeaponsNormal(cMainhand, out rMainhand, cOffhand, out rOffhand, cMainhandStain, out rMainhandStain, cOffhandStain, - out rOffhandStain, cApply, out rApplyMainhand, out rApplyMainhandStain, out rApplyOffhand, out rApplyOffhandStain, locked, - allWeapons); + DrawBonusItemSmall(bonusDrawData); + else + DrawBonusItemNormal(bonusDrawData); } - public static bool DrawHatState(bool currentValue, out bool newValue, bool locked) - => UiHelpers.DrawCheckbox("Hat Visible", "Hide or show the characters head gear.", currentValue, out newValue, locked); - - public static DataChange DrawHatState(bool currentValue, bool currentApply, out bool newValue, out bool newApply, bool locked) - => UiHelpers.DrawMetaToggle("Hat Visible", currentValue, currentApply, out newValue, out newApply, locked); - - public static bool DrawVisorState(bool currentValue, out bool newValue, bool locked) - => UiHelpers.DrawCheckbox("Visor Toggled", "Toggle the visor state of the characters head gear.", currentValue, out newValue, locked); - - public static DataChange DrawVisorState(bool currentValue, bool currentApply, out bool newValue, out bool newApply, bool locked) - => UiHelpers.DrawMetaToggle("Visor Toggled", currentValue, currentApply, out newValue, out newApply, locked); - - public static bool DrawWeaponState(bool currentValue, out bool newValue, bool locked) - => UiHelpers.DrawCheckbox("Weapon Visible", "Hide or show the characters weapons when not drawn.", currentValue, out newValue, locked); - - public static DataChange DrawWeaponState(bool currentValue, bool currentApply, out bool newValue, out bool newApply, bool locked) - => UiHelpers.DrawMetaToggle("Weapon Visible", currentValue, currentApply, out newValue, out newApply, locked); - - private bool DrawMainhand(EquipItem current, bool drawAll, out EquipItem weapon, out string label, bool locked, bool small, bool open) + public void DrawWeapons(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons) { - weapon = current; - if (!_weaponCombo.TryGetValue(drawAll ? FullEquipType.Unknown : current.Type, out var combo)) + if (mainhand.CurrentItem.PrimaryId.Id == 0 && !allWeapons) + return; + + if (_config.HideApplyCheckmarks) { - label = string.Empty; - return false; + mainhand.DisplayApplication = false; + offhand.DisplayApplication = false; } - label = combo.Label; + using var id = ImUtf8.PushId("Weapons"u8); + var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); - var unknown = !_gPose.InGPose && current.Type is FullEquipType.Unknown; - var ret = false; - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); - using (var disabled = ImRaii.Disabled(locked | unknown)) - { - if (!locked && open) - UiHelpers.OpenCombo($"##{label}"); - if (combo.Draw(weapon.Name, weapon.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth)) - { - ret = true; - weapon = combo.CurrentSelection; - } - } - - if (unknown && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip("The weapon type could not be identified, thus changing it to other weapons of that type is not possible."); - - return ret; + if (_config.SmallEquip) + DrawWeaponsSmall(mainhand, offhand, allWeapons); + else + DrawWeaponsNormal(mainhand, offhand, allWeapons); } - private bool DrawOffhand(EquipItem mainhand, EquipItem current, out EquipItem weapon, out string label, bool locked, bool small, bool clear, - bool open) + public static void DrawMetaToggle(in ToggleDrawData data) { - weapon = current; - if (!_weaponCombo.TryGetValue(current.Type, out var combo)) + if (data.DisplayApplication) { - label = string.Empty; - return false; + var (valueChanged, applyChanged) = UiHelpers.DrawMetaToggle(data.Label, data.CurrentValue, data.CurrentApply, out var newValue, + out var newApply, data.Locked); + if (valueChanged) + data.SetValue(newValue); + if (applyChanged) + data.SetApply(newApply); } - - label = combo.Label; - locked |= !_gPose.InGPose && (current.Type is FullEquipType.Unknown || mainhand.Type is FullEquipType.Unknown); - using var disabled = ImRaii.Disabled(locked); - if (!locked && open) - UiHelpers.OpenCombo($"##{combo.Label}"); - var change = combo.Draw(weapon.Name, weapon.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth); - if (change) - weapon = combo.CurrentSelection; - - if (!locked) + else { - var defaultOffhand = _items.GetDefaultOffhand(mainhand); - if (defaultOffhand.Id != weapon.Id) - { - ImGuiUtil.HoverTooltip("Right-click to set to Default."); - if (clear || ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - change = true; - weapon = defaultOffhand; - } - } + if (UiHelpers.DrawCheckbox(data.Label, data.Tooltip, data.CurrentValue, out var newValue, data.Locked)) + data.SetValue(newValue); } - - return change; } - private bool DrawApply(EquipSlot slot, EquipFlag flags, out bool enabled, bool locked) - => UiHelpers.DrawCheckbox($"##apply{slot}", "Apply this item when applying the Design.", flags.HasFlag(slot.ToFlag()), out enabled, - locked); - - private bool DrawApplyStain(EquipSlot slot, EquipFlag flags, out bool enabled, bool locked) - => UiHelpers.DrawCheckbox($"##applyStain{slot}", "Apply this dye when applying the Design.", flags.HasFlag(slot.ToStainFlag()), - out enabled, locked); - - private bool DrawItem(EquipSlot slot, EquipItem current, out EquipItem armor, out string label, bool locked, bool small, bool clear, - bool open) - { - Debug.Assert(slot.IsEquipment() || slot.IsAccessory(), $"Called {nameof(DrawItem)} on {slot}."); - var combo = _itemCombo[slot.ToIndex()]; - label = combo.Label; - armor = current; - if (!locked && open) - UiHelpers.OpenCombo($"##{combo.Label}"); - - using var disabled = ImRaii.Disabled(locked); - var change = combo.Draw(armor.Name, armor.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, _requiredComboWidth); - if (change) - armor = combo.CurrentSelection; - - if (!locked && armor.ModelId.Id != 0) - { - if (clear || ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - change = true; - armor = ItemManager.NothingItem(slot); - } - - ImGuiUtil.HoverTooltip("Right-click to clear."); - } - - return change; - } - - public bool DrawAllStain(out StainId ret, bool locked) + public bool DrawAllStain(out StainIds ret, bool locked) { using var disabled = ImRaii.Disabled(locked); - var change = _stainCombo.Draw("Dye All Slots", Stain.None.RgbaColor, string.Empty, false, false); - ret = Stain.None.RowIndex; + var change = _stainCombo.Draw("Dye All Slots", Stain.None.RgbaColor, string.Empty, false, false, MouseWheelType.None); + ret = StainIds.None; if (change) if (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out var stain)) - ret = stain.RowIndex; + ret = StainIds.All(stain.RowIndex); else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex) - ret = Stain.None.RowIndex; + ret = StainIds.None; if (!locked) { if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && _config.DeleteDesignModifier.IsActive()) { - ret = Stain.None.RowIndex; + ret = StainIds.None; change = true; } - ImGuiUtil.HoverTooltip($"{_config.DeleteDesignModifier.ToString()} and Right-click to clear."); + ImUtf8.HoverTooltip($"{_config.DeleteDesignModifier.ToString()} and Right-click to clear."); } return change; } - private bool DrawStain(EquipSlot slot, StainId current, out StainId ret, bool locked, bool small) + + #region Small + + private void DrawEquipSmall(in EquipDrawData equipDrawData) { - var found = _stainData.TryGetValue(current, out var stain); - using var disabled = ImRaii.Disabled(locked); - var change = small - ? _stainCombo.Draw($"##stain{slot}", stain.RgbaColor, stain.Name, found, stain.Gloss) - : _stainCombo.Draw($"##stain{slot}", stain.RgbaColor, stain.Name, found, stain.Gloss, _comboLength); - ret = current; - if (change) - if (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain)) - ret = stain.RowIndex; - else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex) - ret = Stain.None.RowIndex; - - if (!locked && ret != Stain.None.RowIndex) - { - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - ret = Stain.None.RowIndex; - change = true; - } - - ImGuiUtil.HoverTooltip("Right-click to clear."); - } - - return change; - } - - /// Draw an input for armor that can set arbitrary values instead of choosing items. - private bool DrawArmorArtisan(EquipSlot slot, EquipItem current, out EquipItem armor) - { - int setId = current.ModelId.Id; - int variant = current.Variant.Id; - var ret = false; - armor = current; - ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##setId", ref setId, 0, 0)) - { - var newSetId = (SetId)Math.Clamp(setId, 0, ushort.MaxValue); - if (newSetId.Id != current.ModelId.Id) - { - armor = _items.Identify(slot, newSetId, current.Variant); - ret = true; - } - } - + DrawStain(equipDrawData, true); ImGui.SameLine(); - ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##variant", ref variant, 0, 0)) - { - var newVariant = (byte)Math.Clamp(variant, 0, byte.MaxValue); - if (newVariant != current.Variant) - { - armor = _items.Identify(slot, current.ModelId, newVariant); - ret = true; - } - } - - return ret; - } - - /// Draw an input for stain that can set arbitrary values instead of choosing valid stains. - private bool DrawStainArtisan(EquipSlot slot, StainId current, out StainId stain) - { - int stainId = current.Id; - ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##stain", ref stainId, 0, 0)) - { - var newStainId = (StainId)Math.Clamp(stainId, 0, byte.MaxValue); - if (newStainId != current) - { - stain = newStainId; - return true; - } - } - - stain = current; - return false; - } - - private DataChange DrawEquipArtisan(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, - EquipFlag? cApply, out bool rApply, out bool rApplyStain) - { - var changes = DataChange.None; - if (DrawStainArtisan(slot, cStain, out rStain)) - changes |= DataChange.Stain; - ImGui.SameLine(); - if (DrawArmorArtisan(slot, cArmor, out rArmor)) - changes |= DataChange.Item; - if (cApply.HasValue) + DrawItem(equipDrawData, out var label, true, false, false); + if (equipDrawData.DisplayApplication) { ImGui.SameLine(); - if (DrawApply(slot, cApply.Value, out rApply, false)) - changes |= DataChange.ApplyItem; + DrawApply(equipDrawData); ImGui.SameLine(); - if (DrawApplyStain(slot, cApply.Value, out rApplyStain, false)) - changes |= DataChange.ApplyStain; + DrawApplyStain(equipDrawData); } - else + else if (equipDrawData.IsState) { - rApply = false; - rApplyStain = false; + _advancedDyes.DrawButton(equipDrawData.Slot, equipDrawData.HasAdvancedDyes ? _advancedMaterialColor : 0u); } - return changes; - } - - private DataChange DrawEquipSmall(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, - EquipFlag? cApply, out bool rApply, out bool rApplyStain, bool locked, Gender gender, Race race) - { - var changes = DataChange.None; - if (DrawStain(slot, cStain, out rStain, locked, true)) - changes |= DataChange.Stain; - ImGui.SameLine(); - if (DrawItem(slot, cArmor, out rArmor, out var label, locked, true, false, false)) - changes |= DataChange.Item; - if (cApply.HasValue) - { - ImGui.SameLine(); - if (DrawApply(slot, cApply.Value, out rApply, false)) - changes |= DataChange.ApplyItem; - ImGui.SameLine(); - if (DrawApplyStain(slot, cApply.Value, out rApplyStain, false)) - changes |= DataChange.ApplyStain; - } - else - { - rApply = false; - rApplyStain = false; - } - - if (VerifyRestrictedGear(slot, rArmor, gender, race)) + if (VerifyRestrictedGear(equipDrawData)) label += " (Restricted)"; - ImGui.SameLine(); - ImGui.TextUnformatted(label); - - return changes; + DrawEquipLabel(equipDrawData is { IsDesign: true, HasAdvancedDyes: true }, label); } - private DataChange DrawEquipNormal(EquipSlot slot, EquipItem cArmor, out EquipItem rArmor, StainId cStain, out StainId rStain, - EquipFlag? cApply, out bool rApply, out bool rApplyStain, bool locked, Gender gender, Race race) + private void DrawBonusItemSmall(in BonusDrawData bonusDrawData) { - var changes = DataChange.None; - cArmor.DrawIcon(_textures, _iconSize, slot); + ImGui.Dummy(new Vector2(StainId.NumStains * ImUtf8.FrameHeight + (StainId.NumStains - 1) * ImUtf8.ItemSpacing.X, ImUtf8.FrameHeight)); + ImGui.SameLine(); + DrawBonusItem(bonusDrawData, out var label, true, false, false); + if (bonusDrawData.DisplayApplication) + { + ImGui.SameLine(); + DrawApply(bonusDrawData); + } + else if (bonusDrawData.IsState) + { + _advancedDyes.DrawButton(bonusDrawData.Slot, bonusDrawData.HasAdvancedDyes ? _advancedMaterialColor : 0u); + } + + DrawEquipLabel(bonusDrawData is { IsDesign: true, HasAdvancedDyes: true }, label); + } + + private void DrawWeaponsSmall(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons) + { + DrawStain(mainhand, true); + ImGui.SameLine(); + DrawMainhand(ref mainhand, ref offhand, out var mainhandLabel, allWeapons, true, false); + if (mainhand.DisplayApplication) + { + ImGui.SameLine(); + DrawApply(mainhand); + ImGui.SameLine(); + DrawApplyStain(mainhand); + } + else if (mainhand.IsState) + { + _advancedDyes.DrawButton(EquipSlot.MainHand, mainhand.HasAdvancedDyes ? _advancedMaterialColor : 0u); + } + + if (allWeapons) + mainhandLabel += $" ({mainhand.CurrentItem.Type.ToName()})"; + WeaponHelpMarker(mainhand is { IsDesign: true, HasAdvancedDyes: true }, mainhandLabel); + + if (offhand.CurrentItem.Type is FullEquipType.Unknown) + return; + + DrawStain(offhand, true); + ImGui.SameLine(); + DrawOffhand(mainhand, offhand, out var offhandLabel, true, false, false); + if (offhand.DisplayApplication) + { + ImGui.SameLine(); + DrawApply(offhand); + ImGui.SameLine(); + DrawApplyStain(offhand); + } + else if (offhand.IsState) + { + _advancedDyes.DrawButton(EquipSlot.OffHand, offhand.HasAdvancedDyes ? _advancedMaterialColor : 0u); + } + + WeaponHelpMarker(offhand is { IsDesign: true, HasAdvancedDyes: true }, offhandLabel); + } + + #endregion + + #region Normal + + private void DrawEquipNormal(in EquipDrawData equipDrawData) + { + equipDrawData.CurrentItem.DrawIcon(_textures, _iconSize, equipDrawData.Slot); var right = ImGui.IsItemClicked(ImGuiMouseButton.Right); var left = ImGui.IsItemClicked(ImGuiMouseButton.Left); ImGui.SameLine(); using var group = ImRaii.Group(); - if (DrawItem(slot, cArmor, out rArmor, out var label, locked, false, right, left)) - changes |= DataChange.Item; - if (cApply.HasValue) + DrawItem(equipDrawData, out var label, false, right, left); + if (equipDrawData.DisplayApplication) { ImGui.SameLine(); - if (DrawApply(slot, cApply.Value, out rApply, locked)) - changes |= DataChange.ApplyItem; - } - else - { - rApply = true; + DrawApply(equipDrawData); } - ImGui.SameLine(); - ImGui.TextUnformatted(label); - if (DrawStain(slot, cStain, out rStain, locked, false)) - changes |= DataChange.Stain; - if (cApply.HasValue) + DrawEquipLabel(equipDrawData is { IsDesign: true, HasAdvancedDyes: true }, label); + + DrawStain(equipDrawData, false); + if (equipDrawData.DisplayApplication) { ImGui.SameLine(); - if (DrawApplyStain(slot, cApply.Value, out rApplyStain, locked)) - changes |= DataChange.ApplyStain; + DrawApplyStain(equipDrawData); } - else + else if (equipDrawData.IsState) { - rApplyStain = true; + _advancedDyes.DrawButton(equipDrawData.Slot, equipDrawData.HasAdvancedDyes ? _advancedMaterialColor : 0u); } - if (VerifyRestrictedGear(slot, rArmor, gender, race)) + if (VerifyRestrictedGear(equipDrawData)) { ImGui.SameLine(); - ImGui.TextUnformatted("(Restricted)"); + ImUtf8.Text("(Restricted)"u8); } - - return changes; } - private static void WeaponHelpMarker(string label, string? type = null) + private void DrawBonusItemNormal(in BonusDrawData bonusDrawData) + { + bonusDrawData.CurrentItem.DrawIcon(_textures, _iconSize, bonusDrawData.Slot); + var right = ImGui.IsItemClicked(ImGuiMouseButton.Right); + var left = ImGui.IsItemClicked(ImGuiMouseButton.Left); + ImGui.SameLine(); + DrawBonusItem(bonusDrawData, out var label, false, right, left); + if (bonusDrawData.DisplayApplication) + { + ImGui.SameLine(); + DrawApply(bonusDrawData); + } + else if (bonusDrawData.IsState) + { + _advancedDyes.DrawButton(bonusDrawData.Slot, bonusDrawData.HasAdvancedDyes ? _advancedMaterialColor : 0u); + } + + DrawEquipLabel(bonusDrawData is { IsDesign: true, HasAdvancedDyes: true }, label); + } + + private void DrawWeaponsNormal(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }); + + mainhand.CurrentItem.DrawIcon(_textures, _iconSize, EquipSlot.MainHand); + var left = ImGui.IsItemClicked(ImGuiMouseButton.Left); + ImGui.SameLine(); + using (ImUtf8.Group()) + { + DrawMainhand(ref mainhand, ref offhand, out var mainhandLabel, allWeapons, false, left); + if (mainhand.DisplayApplication) + { + ImGui.SameLine(); + DrawApply(mainhand); + } + + WeaponHelpMarker(mainhand is { IsDesign: true, HasAdvancedDyes: true }, mainhandLabel, + allWeapons ? mainhand.CurrentItem.Type.ToName() : null); + + DrawStain(mainhand, false); + if (mainhand.DisplayApplication) + { + ImGui.SameLine(); + DrawApplyStain(mainhand); + } + else if (mainhand.IsState) + { + _advancedDyes.DrawButton(EquipSlot.MainHand, mainhand.HasAdvancedDyes ? _advancedMaterialColor : 0u); + } + } + + if (offhand.CurrentItem.Type is FullEquipType.Unknown) + return; + + offhand.CurrentItem.DrawIcon(_textures, _iconSize, EquipSlot.OffHand); + var right = ImGui.IsItemClicked(ImGuiMouseButton.Right); + left = ImGui.IsItemClicked(ImGuiMouseButton.Left); + ImGui.SameLine(); + using (ImUtf8.Group()) + { + DrawOffhand(mainhand, offhand, out var offhandLabel, false, right, left); + if (offhand.DisplayApplication) + { + ImGui.SameLine(); + DrawApply(offhand); + } + + WeaponHelpMarker(offhand is { IsDesign: true, HasAdvancedDyes: true }, offhandLabel); + + DrawStain(offhand, false); + if (offhand.DisplayApplication) + { + ImGui.SameLine(); + DrawApplyStain(offhand); + } + else if (offhand.IsState) + { + _advancedDyes.DrawButton(EquipSlot.OffHand, offhand.HasAdvancedDyes ? _advancedMaterialColor : 0u); + } + } + } + + private void DrawStain(in EquipDrawData data, bool small) + { + using var disabled = ImRaii.Disabled(data.Locked); + var width = (_comboLength - ImUtf8.ItemInnerSpacing.X * (data.CurrentStains.Count - 1)) / data.CurrentStains.Count; + foreach (var (stainId, index) in data.CurrentStains.WithIndex()) + { + using var id = ImUtf8.PushId(index); + var found = _stainData.TryGetValue(stainId, out var stain); + var change = small + ? _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss) + : _stainCombo.Draw($"##stain{data.Slot}", stain.RgbaColor, stain.Name, found, stain.Gloss, width); + + _itemCopy.HandleCopyPaste(data, index); + if (!change) + DrawStainDragDrop(data, index, stain, found); + + if (index < data.CurrentStains.Count - 1) + ImUtf8.SameLineInner(); + + if (change) + if (_stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain)) + data.SetStains(data.CurrentStains.With(index, stain.RowIndex)); + else if (_stainCombo.CurrentSelection.Key == Stain.None.RowIndex) + data.SetStains(data.CurrentStains.With(index, Stain.None.RowIndex)); + if (ResetOrClear(data.Locked, false, data.AllowRevert, true, stainId, data.GameStains[index], Stain.None.RowIndex, + out var newStain)) + data.SetStains(data.CurrentStains.With(index, newStain)); + } + } + + private void DrawStainDragDrop(in EquipDrawData data, int index, Stain stain, bool found) + { + if (found) + { + using var dragSource = ImUtf8.DragDropSource(); + if (dragSource.Success) + { + DragDropSource.SetPayload("stainDragDrop"u8); + _draggedStain = stain; + ImUtf8.Text($"Dragging {stain.Name}..."); + } + } + + using var dragTarget = ImUtf8.DragDropTarget(); + if (dragTarget.IsDropping("stainDragDrop"u8) && _draggedStain.HasValue) + { + data.SetStains(data.CurrentStains.With(index, _draggedStain.Value.RowIndex)); + _draggedStain = null; + } + } + + private void DrawItem(in EquipDrawData data, out string label, bool small, bool clear, bool open) + { + Debug.Assert(data.Slot.IsEquipment() || data.Slot.IsAccessory(), $"Called {nameof(DrawItem)} on {data.Slot}."); + + var combo = _itemCombo[data.Slot.ToIndex()]; + label = combo.Label; + if (!data.Locked && open) + UiHelpers.OpenCombo($"##{combo.Label}"); + + using var disabled = ImRaii.Disabled(data.Locked); + var change = combo.Draw(data.CurrentItem.Name, data.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, + _requiredComboWidth); + DrawGearDragDrop(data); + if (change) + data.SetItem(combo.CurrentSelection); + else if (combo.CustomVariant.Id > 0) + data.SetItem(_items.Identify(data.Slot, combo.CustomSetId, combo.CustomVariant)); + _itemCopy.HandleCopyPaste(data); + + if (ResetOrClear(data.Locked, clear, data.AllowRevert, true, data.CurrentItem, data.GameItem, ItemManager.NothingItem(data.Slot), + out var item)) + data.SetItem(item); + } + + private void DrawBonusItem(in BonusDrawData data, out string label, bool small, bool clear, bool open) + { + var combo = _bonusItemCombo[data.Slot.ToIndex()]; + label = combo.Label; + if (!data.Locked && open) + UiHelpers.OpenCombo($"##{combo.Label}"); + + using var disabled = ImRaii.Disabled(data.Locked); + var change = combo.Draw(data.CurrentItem.Name, data.CurrentItem.Id.BonusItem, + small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, + _requiredComboWidth); + if (ImGui.IsItemHovered() && ImGui.GetIO().KeyCtrl) + { + if (ImGui.IsKeyPressed(ImGuiKey.C)) + _itemCopy.Copy(combo.CurrentSelection); + else if (ImGui.IsKeyPressed(ImGuiKey.V)) + _itemCopy.Paste(data.Slot.ToEquipType(), data.SetItem); + } + + if (change) + data.SetItem(combo.CurrentSelection); + else if (combo.CustomVariant.Id > 0) + data.SetItem(_items.Identify(data.Slot, combo.CustomSetId, combo.CustomVariant)); + + if (ResetOrClear(data.Locked, clear, data.AllowRevert, true, data.CurrentItem, data.GameItem, EquipItem.BonusItemNothing(data.Slot), + out var item)) + data.SetItem(item); + } + + private void DrawGearDragDrop(in EquipDrawData data) + { + if (data.CurrentItem.Valid) + { + using var dragSource = ImUtf8.DragDropSource(); + if (dragSource.Success) + { + DragDropSource.SetPayload("equipDragDrop"u8); + _draggedItem.Update(_items, data.CurrentItem, data.Slot); + } + } + + using var dragTarget = ImUtf8.DragDropTarget(); + if (!dragTarget) + return; + + var item = _draggedItem[data.Slot]; + if (!item.Valid) + return; + + _dragTarget = data.Slot; + if (!dragTarget.IsDropping("equipDragDrop"u8)) + return; + + data.SetItem(item); + _draggedItem.Clear(); + } + + public unsafe void DrawDragDropTooltip() + { + var payload = ImGui.GetDragDropPayload().Handle; + if (payload is null) + return; + + if (!MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)Unsafe.AsPointer(ref payload->DataType_0)).SequenceEqual("equipDragDrop"u8)) + return; + + using var tt = ImUtf8.Tooltip(); + if (_dragTarget is EquipSlot.Unknown) + ImUtf8.Text($"Dragging {_draggedItem.Dragged.Name}..."); + else + ImUtf8.Text($"Converting to {_draggedItem[_dragTarget].Name}..."); + } + + private static bool ResetOrClear(bool locked, bool clicked, bool allowRevert, bool allowClear, + in T currentItem, in T revertItem, in T clearItem, out T? item) where T : IEquatable + { + if (locked) + { + item = default; + return false; + } + + clicked = clicked || ImGui.IsItemClicked(ImGuiMouseButton.Right); + + (var tt, item, var valid) = (allowRevert && !revertItem.Equals(currentItem), allowClear && !clearItem.Equals(currentItem), + ImGui.GetIO().KeyCtrl) switch + { + (true, true, true) => ("Right-click to clear. Control and Right-Click to revert to game.\nControl and mouse wheel to scroll.", + revertItem, true), + (true, true, false) => ("Right-click to clear. Control and Right-Click to revert to game.\nControl and mouse wheel to scroll.", + clearItem, true), + (true, false, true) => ("Control and Right-Click to revert to game.\nControl and mouse wheel to scroll.", revertItem, true), + (true, false, false) => ("Control and Right-Click to revert to game.\nControl and mouse wheel to scroll.", default, false), + (false, true, _) => ("Right-click to clear.\nControl and mouse wheel to scroll.", clearItem, true), + (false, false, _) => ("Control and mouse wheel to scroll.", default, false), + }; + ImUtf8.HoverTooltip(tt); + + return clicked && valid; + } + + private void DrawMainhand(ref EquipDrawData mainhand, ref EquipDrawData offhand, out string label, bool drawAll, bool small, + bool open) + { + if (!_weaponCombo.TryGetValue(drawAll ? FullEquipType.Unknown : mainhand.CurrentItem.Type, out var combo)) + { + label = string.Empty; + return; + } + + label = combo.Label; + var unknown = !_gPose.InGPose && mainhand.CurrentItem.Type is FullEquipType.Unknown; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); + EquipItem? changedItem = null; + using (var _ = ImRaii.Disabled(mainhand.Locked | unknown)) + { + if (!mainhand.Locked && open) + UiHelpers.OpenCombo($"##{label}"); + if (combo.Draw(mainhand.CurrentItem.Name, mainhand.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, + _requiredComboWidth)) + changedItem = combo.CurrentSelection; + else if (combo.CustomVariant.Id > 0 && (drawAll || ItemData.ConvertWeaponId(combo.CustomSetId) == mainhand.CurrentItem.Type)) + changedItem = _items.Identify(mainhand.Slot, combo.CustomSetId, combo.CustomWeaponId, combo.CustomVariant); + _itemCopy.HandleCopyPaste(mainhand); + DrawGearDragDrop(mainhand); + + if (ResetOrClear(mainhand.Locked || unknown, open, mainhand.AllowRevert, false, mainhand.CurrentItem, mainhand.GameItem, + default, out var c)) + changedItem = c; + + if (changedItem != null) + { + mainhand.SetItem(changedItem.Value); + if (changedItem.Value.Type.ValidOffhand() != mainhand.CurrentItem.Type.ValidOffhand()) + { + offhand.CurrentItem = _items.GetDefaultOffhand(changedItem.Value); + offhand.SetItem(offhand.CurrentItem); + } + + mainhand.CurrentItem = changedItem.Value; + } + } + + if (unknown) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + "The weapon type could not be identified, thus changing it to other weapons of that type is not possible."u8); + } + + private void DrawOffhand(in EquipDrawData mainhand, in EquipDrawData offhand, out string label, bool small, bool clear, bool open) + { + if (!_weaponCombo.TryGetValue(offhand.CurrentItem.Type, out var combo)) + { + label = string.Empty; + return; + } + + label = combo.Label; + var locked = offhand.Locked + || !_gPose.InGPose && (offhand.CurrentItem.Type.IsUnknown() || mainhand.CurrentItem.Type.IsUnknown()); + using var disabled = ImRaii.Disabled(locked); + if (!locked && open) + UiHelpers.OpenCombo($"##{combo.Label}"); + if (combo.Draw(offhand.CurrentItem.Name, offhand.CurrentItem.ItemId, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength, + _requiredComboWidth)) + offhand.SetItem(combo.CurrentSelection); + else if (combo.CustomVariant.Id > 0 && ItemData.ConvertWeaponId(combo.CustomSetId) == offhand.CurrentItem.Type) + offhand.SetItem(_items.Identify(mainhand.Slot, combo.CustomSetId, combo.CustomWeaponId, combo.CustomVariant)); + _itemCopy.HandleCopyPaste(offhand); + DrawGearDragDrop(offhand); + + var defaultOffhand = _items.GetDefaultOffhand(mainhand.CurrentItem); + if (ResetOrClear(locked, clear, offhand.AllowRevert, true, offhand.CurrentItem, offhand.GameItem, defaultOffhand, out var item)) + offhand.SetItem(item); + } + + private static void DrawApply(in EquipDrawData data) + { + if (UiHelpers.DrawCheckbox($"##apply{data.Slot}", "Apply this item when applying the Design.", data.CurrentApply, out var enabled, + data.Locked)) + data.SetApplyItem(enabled); + } + + private static void DrawApply(in BonusDrawData data) + { + if (UiHelpers.DrawCheckbox($"##apply{data.Slot}", "Apply this bonus item when applying the Design.", data.CurrentApply, out var enabled, + data.Locked)) + data.SetApplyItem(enabled); + } + + private static void DrawApplyStain(in EquipDrawData data) + { + if (UiHelpers.DrawCheckbox($"##applyStain{data.Slot}", "Apply this dye to the item when applying the Design.", data.CurrentApplyStain, + out var enabled, + data.Locked)) + data.SetApplyStain(enabled); + } + + #endregion + + private void WeaponHelpMarker(bool hasAdvancedDyes, string label, string? type = null) { ImGui.SameLine(); ImGuiComponents.HelpMarker( "Changing weapons to weapons of different types can cause crashes, freezes, soft- and hard locks and cheating, " + "thus it is only allowed to change weapons to other weapons of the same type."); - ImGui.SameLine(); - ImGui.TextUnformatted(label); - if (type != null) - { - var pos = ImGui.GetItemRectMin(); - pos.Y += ImGui.GetFrameHeightWithSpacing(); - ImGui.GetWindowDrawList().AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), $"({type})"); - } + DrawEquipLabel(hasAdvancedDyes, label); + + if (type == null) + return; + + var pos = ImGui.GetItemRectMin(); + pos.Y += ImGui.GetFrameHeightWithSpacing(); + ImGui.GetWindowDrawList().AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), $"({type})"); } - private DataChange DrawWeaponsSmall(EquipItem cMainhand, out EquipItem rMainhand, EquipItem cOffhand, out EquipItem rOffhand, - StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, EquipFlag? cApply, - out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyOffhand, out bool rApplyOffhandStain, bool locked, - bool allWeapons) + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + private void DrawEquipLabel(bool hasAdvancedDyes, string label) { - var changes = DataChange.None; - if (DrawStain(EquipSlot.MainHand, cMainhandStain, out rMainhandStain, locked, true)) - changes |= DataChange.Stain; ImGui.SameLine(); - - rOffhand = cOffhand; - if (DrawMainhand(cMainhand, allWeapons, out rMainhand, out var mainhandLabel, locked, true, false)) + using (ImRaii.PushColor(ImGuiCol.Text, _advancedMaterialColor, hasAdvancedDyes)) { - changes |= DataChange.Item; - if (rMainhand.Type.ValidOffhand() != cMainhand.Type.ValidOffhand()) - { - rOffhand = _items.GetDefaultOffhand(rMainhand); - changes |= DataChange.Item2; - } + ImUtf8.Text(label); } - if (cApply.HasValue) - { - ImGui.SameLine(); - if (DrawApply(EquipSlot.MainHand, cApply.Value, out rApplyMainhand, locked)) - changes |= DataChange.ApplyItem; - ImGui.SameLine(); - if (DrawApplyStain(EquipSlot.MainHand, cApply.Value, out rApplyMainhandStain, locked)) - changes |= DataChange.ApplyStain; - } - else - { - rApplyMainhand = true; - rApplyMainhandStain = true; - } - - if (allWeapons) - mainhandLabel += $" ({cMainhand.Type.ToName()})"; - WeaponHelpMarker(mainhandLabel); - - if (rOffhand.Type is FullEquipType.Unknown) - { - rOffhandStain = cOffhandStain; - rApplyOffhand = false; - rApplyOffhandStain = false; - return changes; - } - - if (DrawStain(EquipSlot.OffHand, cOffhandStain, out rOffhandStain, locked, true)) - changes |= DataChange.Stain2; - - ImGui.SameLine(); - if (DrawOffhand(rMainhand, rOffhand, out rOffhand, out var offhandLabel, locked, true, false, false)) - changes |= DataChange.Item2; - if (cApply.HasValue) - { - ImGui.SameLine(); - if (DrawApply(EquipSlot.OffHand, cApply.Value, out rApplyOffhand, locked)) - changes |= DataChange.ApplyItem2; - ImGui.SameLine(); - if (DrawApplyStain(EquipSlot.OffHand, cApply.Value, out rApplyOffhandStain, locked)) - changes |= DataChange.ApplyStain2; - } - else - { - rApplyOffhand = true; - rApplyOffhandStain = true; - } - - WeaponHelpMarker(offhandLabel); - - return changes; - } - - private DataChange DrawWeaponsNormal(EquipItem cMainhand, out EquipItem rMainhand, EquipItem cOffhand, out EquipItem rOffhand, - StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, EquipFlag? cApply, - out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyOffhand, out bool rApplyOffhandStain, bool locked, - bool allWeapons) - { - var changes = DataChange.None; - - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }); - - cMainhand.DrawIcon(_textures, _iconSize, EquipSlot.MainHand); - var left = ImGui.IsItemClicked(ImGuiMouseButton.Left); - ImGui.SameLine(); - using (var group = ImRaii.Group()) - { - rOffhand = cOffhand; - if (DrawMainhand(cMainhand, allWeapons, out rMainhand, out var mainhandLabel, locked, false, left)) - { - changes |= DataChange.Item; - if (rMainhand.Type.ValidOffhand() != cMainhand.Type.ValidOffhand()) - { - rOffhand = _items.GetDefaultOffhand(rMainhand); - changes |= DataChange.Item2; - } - } - - if (cApply.HasValue) - { - ImGui.SameLine(); - if (DrawApply(EquipSlot.MainHand, cApply.Value, out rApplyMainhand, locked)) - changes |= DataChange.ApplyItem; - } - else - { - rApplyMainhand = true; - } - - WeaponHelpMarker(mainhandLabel, allWeapons ? cMainhand.Type.ToName() : null); - - if (DrawStain(EquipSlot.MainHand, cMainhandStain, out rMainhandStain, locked, false)) - changes |= DataChange.Stain; - if (cApply.HasValue) - { - ImGui.SameLine(); - if (DrawApplyStain(EquipSlot.MainHand, cApply.Value, out rApplyMainhandStain, locked)) - changes |= DataChange.ApplyStain; - } - else - { - rApplyMainhandStain = true; - } - } - - if (rOffhand.Type is FullEquipType.Unknown) - { - rOffhandStain = cOffhandStain; - rApplyOffhand = false; - rApplyOffhandStain = false; - return changes; - } - - rOffhand.DrawIcon(_textures, _iconSize, EquipSlot.OffHand); - var right = ImGui.IsItemClicked(ImGuiMouseButton.Right); - left = ImGui.IsItemClicked(ImGuiMouseButton.Left); - ImGui.SameLine(); - using (var group = ImRaii.Group()) - { - if (DrawOffhand(rMainhand, rOffhand, out rOffhand, out var offhandLabel, locked, false, right, left)) - changes |= DataChange.Item2; - if (cApply.HasValue) - { - ImGui.SameLine(); - if (DrawApply(EquipSlot.OffHand, cApply.Value, out rApplyOffhand, locked)) - changes |= DataChange.ApplyItem2; - } - else - { - rApplyOffhand = true; - } - - WeaponHelpMarker(offhandLabel); - - if (DrawStain(EquipSlot.OffHand, cOffhandStain, out rOffhandStain, locked, false)) - changes |= DataChange.Stain2; - if (cApply.HasValue) - { - ImGui.SameLine(); - if (DrawApplyStain(EquipSlot.OffHand, cApply.Value, out rApplyOffhandStain, locked)) - changes |= DataChange.ApplyStain2; - } - else - { - rApplyOffhandStain = true; - } - } - - return changes; - } - - private DataChange DrawWeaponsArtisan(EquipItem cMainhand, out EquipItem rMainhand, EquipItem cOffhand, out EquipItem rOffhand, - StainId cMainhandStain, out StainId rMainhandStain, StainId cOffhandStain, out StainId rOffhandStain, EquipFlag? cApply, - out bool rApplyMainhand, out bool rApplyMainhandStain, out bool rApplyOffhand, out bool rApplyOffhandStain) - { - rApplyMainhand = (cApply ?? 0).HasFlag(EquipFlag.Mainhand); - rApplyMainhandStain = (cApply ?? 0).HasFlag(EquipFlag.MainhandStain); - rApplyOffhand = (cApply ?? 0).HasFlag(EquipFlag.Offhand); - rApplyOffhandStain = (cApply ?? 0).HasFlag(EquipFlag.MainhandStain); - - bool DrawWeapon(EquipItem current, out EquipItem ret) - { - int setId = current.ModelId.Id; - int type = current.WeaponType.Id; - int variant = current.Variant.Id; - ret = current; - var changed = false; - - ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##setId", ref setId, 0, 0)) - { - var newSetId = (SetId)Math.Clamp(setId, 0, ushort.MaxValue); - if (newSetId.Id != current.ModelId.Id) - { - ret = _items.Identify(EquipSlot.MainHand, newSetId, current.WeaponType, current.Variant); - changed = true; - } - } - - ImGui.SameLine(); - ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##type", ref type, 0, 0)) - { - var newType = (WeaponType)Math.Clamp(type, 0, ushort.MaxValue); - if (newType.Id != current.WeaponType.Id) - { - ret = _items.Identify(EquipSlot.MainHand, current.ModelId, newType, current.Variant); - changed = true; - } - } - - ImGui.SameLine(); - ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale); - if (ImGui.InputInt("##variant", ref variant, 0, 0)) - { - var newVariant = (Variant)Math.Clamp(variant, 0, byte.MaxValue); - if (newVariant.Id != current.Variant.Id) - { - ret = _items.Identify(EquipSlot.MainHand, current.ModelId, current.WeaponType, newVariant); - changed = true; - } - } - - return changed; - } - - var ret = DataChange.None; - using (var id = ImRaii.PushId(0)) - { - if (DrawStainArtisan(EquipSlot.MainHand, cMainhandStain, out rMainhandStain)) - ret |= DataChange.Stain; - ImGui.SameLine(); - if (DrawWeapon(cMainhand, out rMainhand)) - ret |= DataChange.Item; - } - - using (var id = ImRaii.PushId(1)) - { - if (DrawStainArtisan(EquipSlot.OffHand, cOffhandStain, out rOffhandStain)) - ret |= DataChange.Stain; - ImGui.SameLine(); - if (DrawWeapon(cOffhand, out rOffhand)) - ret |= DataChange.Item; - } - - return ret; + if (hasAdvancedDyes) + ImUtf8.HoverTooltip("This design has advanced dyes setup for this slot."u8); } } diff --git a/Glamourer/Gui/Equipment/GlamourerColorCombo.cs b/Glamourer/Gui/Equipment/GlamourerColorCombo.cs new file mode 100644 index 0000000..3149e67 --- /dev/null +++ b/Glamourer/Gui/Equipment/GlamourerColorCombo.cs @@ -0,0 +1,44 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Glamourer.Unlocks; +using Dalamud.Bindings.ImGui; +using OtterGui.Widgets; +using Penumbra.GameData.DataContainers; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Equipment; + +public sealed class GlamourerColorCombo(float _comboWidth, DictStain _stains, FavoriteManager _favorites) + : FilterComboColors(_comboWidth, MouseWheelType.Control, CreateFunc(_stains, _favorites), Glamourer.Log) +{ + protected override bool DrawSelectable(int globalIdx, bool selected) + { + using (var _ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGuiHelpers.ScaledVector2(4, 0))) + { + if (globalIdx == 0) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGui.Dummy(ImGui.CalcTextSize(FontAwesomeIcon.Star.ToIconString())); + } + else + { + UiHelpers.DrawFavoriteStar(_favorites, Items[globalIdx].Key); + } + + ImGui.SameLine(); + } + + var buttonWidth = ImGui.GetContentRegionAvail().X; + var totalWidth = ImGui.GetContentRegionMax().X; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(buttonWidth / 2 / totalWidth, 0.5f)); + + return base.DrawSelectable(globalIdx, selected); + } + + private static Func>> CreateFunc(DictStain stains, + FavoriteManager favorites) + => () => stains.Select(kvp => (kvp, favorites.Contains(kvp.Key))).OrderBy(p => !p.Item2).Select(p => p.kvp) + .Prepend(new KeyValuePair(Stain.None.RowIndex, Stain.None)).Select(kvp + => new KeyValuePair(kvp.Key.Id, (kvp.Value.Name, kvp.Value.RgbaColor, kvp.Value.Gloss))).ToList(); +} diff --git a/Glamourer/Gui/Equipment/ItemCombo.cs b/Glamourer/Gui/Equipment/ItemCombo.cs index 5062949..7c0c3bc 100644 --- a/Glamourer/Gui/Equipment/ItemCombo.cs +++ b/Glamourer/Gui/Equipment/ItemCombo.cs @@ -1,14 +1,13 @@ -using System.Collections.Generic; -using System.Linq; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.Services; using Glamourer.Services; using Glamourer.Unlocks; -using ImGuiNET; -using Lumina.Excel.GeneratedSheets; -using OtterGui; +using Dalamud.Bindings.ImGui; +using Lumina.Excel.Sheets; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Log; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -22,8 +21,11 @@ public sealed class ItemCombo : FilterComboCache private ItemId _currentItem; private float _innerWidth; + public PrimaryId CustomSetId { get; private set; } + public Variant CustomVariant { get; private set; } + public ItemCombo(IDataManager gameData, ItemManager items, EquipSlot slot, Logger log, FavoriteManager favorites) - : base(() => GetItems(favorites, items, slot), log) + : base(() => GetItems(favorites, items, slot), MouseWheelType.Control, log) { _favorites = favorites; Label = GetLabel(gameData, slot); @@ -50,8 +52,9 @@ public sealed class ItemCombo : FilterComboCache public bool Draw(string previewName, ItemId previewIdx, float width, float innerWidth) { - _innerWidth = innerWidth; - _currentItem = previewIdx; + _innerWidth = innerWidth; + _currentItem = previewIdx; + CustomVariant = 0; return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); } @@ -73,48 +76,59 @@ public sealed class ItemCombo : FilterComboCache var ret = ImGui.Selectable(name, selected); ImGui.SameLine(); using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080); - ImGuiUtil.RightAlign($"({obj.ModelString})"); + ImUtf8.TextRightAligned($"({obj.PrimaryId.Id}-{obj.Variant.Id})"); return ret; } protected override bool IsVisible(int globalIndex, LowerString filter) - => base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].ModelId.Id.ToString()); + => base.IsVisible(globalIndex, filter) || Items[globalIndex].ModelString.StartsWith(filter.Lower); protected override string ToString(EquipItem obj) => obj.Name; private static string GetLabel(IDataManager gameData, EquipSlot slot) { - var sheet = gameData.GetExcelSheet()!; + var sheet = gameData.GetExcelSheet(); return slot switch { - EquipSlot.Head => sheet.GetRow(740)?.Text.ToString() ?? "Head", - EquipSlot.Body => sheet.GetRow(741)?.Text.ToString() ?? "Body", - EquipSlot.Hands => sheet.GetRow(742)?.Text.ToString() ?? "Hands", - EquipSlot.Legs => sheet.GetRow(744)?.Text.ToString() ?? "Legs", - EquipSlot.Feet => sheet.GetRow(745)?.Text.ToString() ?? "Feet", - EquipSlot.Ears => sheet.GetRow(746)?.Text.ToString() ?? "Ears", - EquipSlot.Neck => sheet.GetRow(747)?.Text.ToString() ?? "Neck", - EquipSlot.Wrists => sheet.GetRow(748)?.Text.ToString() ?? "Wrists", - EquipSlot.RFinger => sheet.GetRow(749)?.Text.ToString() ?? "Right Ring", - EquipSlot.LFinger => sheet.GetRow(750)?.Text.ToString() ?? "Left Ring", + EquipSlot.Head => sheet.TryGetRow(740, out var text) ? text.Text.ToString() : "Head", + EquipSlot.Body => sheet.TryGetRow(741, out var text) ? text.Text.ToString() : "Body", + EquipSlot.Hands => sheet.TryGetRow(742, out var text) ? text.Text.ToString() : "Hands", + EquipSlot.Legs => sheet.TryGetRow(744, out var text) ? text.Text.ToString() : "Legs", + EquipSlot.Feet => sheet.TryGetRow(745, out var text) ? text.Text.ToString() : "Feet", + EquipSlot.Ears => sheet.TryGetRow(746, out var text) ? text.Text.ToString() : "Ears", + EquipSlot.Neck => sheet.TryGetRow(747, out var text) ? text.Text.ToString() : "Neck", + EquipSlot.Wrists => sheet.TryGetRow(748, out var text) ? text.Text.ToString() : "Wrists", + EquipSlot.RFinger => sheet.TryGetRow(749, out var text) ? text.Text.ToString() : "Right Ring", + EquipSlot.LFinger => sheet.TryGetRow(750, out var text) ? text.Text.ToString() : "Left Ring", _ => string.Empty, }; } - private static IReadOnlyList GetItems(FavoriteManager favorites, ItemManager items, EquipSlot slot) + private static List GetItems(FavoriteManager favorites, ItemManager items, EquipSlot slot) { var nothing = ItemManager.NothingItem(slot); - if (!items.ItemService.AwaitedService.TryGetValue(slot.ToEquipType(), out var list)) - return new[] - { - nothing, - }; + if (!items.ItemData.ByType.TryGetValue(slot.ToEquipType(), out var list)) + return [nothing]; var enumerable = list.AsEnumerable(); if (slot.IsEquipment()) enumerable = enumerable.Append(ItemManager.SmallClothesItem(slot)); return enumerable.OrderByDescending(favorites.Contains).ThenBy(i => i.Name).Prepend(nothing).ToList(); } + + protected override void OnClosePopup() + { + // If holding control while the popup closes, try to parse the input as a full pair of set id and variant, and set a custom item for that. + if (!ImGui.GetIO().KeyCtrl) + return; + + var split = Filter.Text.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length != 2 || !ushort.TryParse(split[0], out var setId) || !byte.TryParse(split[1], out var variant)) + return; + + CustomSetId = setId; + CustomVariant = variant; + } } diff --git a/Glamourer/Gui/Equipment/ItemCopyService.cs b/Glamourer/Gui/Equipment/ItemCopyService.cs new file mode 100644 index 0000000..6912f1f --- /dev/null +++ b/Glamourer/Gui/Equipment/ItemCopyService.cs @@ -0,0 +1,73 @@ +using Glamourer.Services; +using Dalamud.Bindings.ImGui; +using OtterGui.Services; +using Penumbra.GameData.DataContainers; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Equipment; + +public class ItemCopyService(ItemManager items, DictStain stainData) : IUiService +{ + public EquipItem? Item { get; private set; } + public Stain? Stain { get; private set; } + + public void Copy(in EquipItem item) + => Item = item; + + public void Copy(in Stain stain) + => Stain = stain; + + public void Paste(int which, Action setter) + { + if (Stain is { } stain) + setter(which, stain.RowIndex); + } + + public void Paste(FullEquipType type, Action setter) + { + if (Item is not { } item) + return; + + if (type != item.Type) + { + if (type.IsBonus()) + item = items.Identify(type.ToBonus(), item.PrimaryId, item.Variant); + else if (type.IsEquipment() || type.IsAccessory()) + item = items.Identify(type.ToSlot(), item.PrimaryId, item.Variant); + else + item = items.Identify(type.ToSlot(), item.PrimaryId, item.SecondaryId, item.Variant); + } + + if (item.Valid && item.Type == type) + setter(item); + } + + public void HandleCopyPaste(in EquipDrawData data) + { + if (ImGui.GetIO().KeyCtrl) + { + if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + Paste(data.CurrentItem.Type, data.SetItem); + } + else if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) && ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + { + Copy(data.CurrentItem); + } + } + + public void HandleCopyPaste(in EquipDrawData data, int which) + { + if (ImGui.GetIO().KeyCtrl) + { + if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + Paste(which, data.SetStain); + } + else if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) + && ImGui.IsMouseClicked(ImGuiMouseButton.Middle) + && stainData.TryGetValue(data.CurrentStains[which].Id, out var stain)) + { + Copy(stain); + } + } +} diff --git a/Glamourer/Gui/Equipment/WeaponCombo.cs b/Glamourer/Gui/Equipment/WeaponCombo.cs index 5a1792e..3029db7 100644 --- a/Glamourer/Gui/Equipment/WeaponCombo.cs +++ b/Glamourer/Gui/Equipment/WeaponCombo.cs @@ -1,12 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Glamourer.Services; -using ImGuiNET; -using OtterGui; +using Glamourer.Services; +using Glamourer.Unlocks; +using Dalamud.Bindings.ImGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Log; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -15,13 +14,19 @@ namespace Glamourer.Gui.Equipment; public sealed class WeaponCombo : FilterComboCache { - public readonly string Label; - private ItemId _currentItemId; - private float _innerWidth; + private readonly FavoriteManager _favorites; + public readonly string Label; + private ItemId _currentItem; + private float _innerWidth; - public WeaponCombo(ItemManager items, FullEquipType type, Logger log) - : base(() => GetWeapons(items, type), log) + public PrimaryId CustomSetId { get; private set; } + public SecondaryId CustomWeaponId { get; private set; } + public Variant CustomVariant { get; private set; } + + public WeaponCombo(ItemManager items, FullEquipType type, Logger log, FavoriteManager favorites) + : base(() => GetWeapons(favorites, items, type), MouseWheelType.Control, log) { + _favorites = favorites; Label = GetLabel(type); SearchByParts = true; } @@ -35,64 +40,92 @@ public sealed class WeaponCombo : FilterComboCache protected override int UpdateCurrentSelected(int currentSelected) { - if (CurrentSelection.ItemId == _currentItemId) + if (CurrentSelection.ItemId == _currentItem) return currentSelected; - CurrentSelectionIdx = Items.IndexOf(i => i.ItemId == _currentItemId); + CurrentSelectionIdx = Items.IndexOf(i => i.ItemId == _currentItem); CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default; return base.UpdateCurrentSelected(CurrentSelectionIdx); } + public bool Draw(string previewName, ItemId previewIdx, float width, float innerWidth) + { + _innerWidth = innerWidth; + _currentItem = previewIdx; + CustomVariant = 0; + return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); + } + protected override float GetFilterWidth() => _innerWidth - 2 * ImGui.GetStyle().FramePadding.X; - public bool Draw(string previewName, ItemId previewId, float width, float innerWidth) - { - _currentItemId = previewId; - _innerWidth = innerWidth; - return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); - } protected override bool DrawSelectable(int globalIdx, bool selected) { var obj = Items[globalIdx]; var name = ToString(obj); - var ret = ImGui.Selectable(name, selected); + if (UiHelpers.DrawFavoriteStar(_favorites, obj) && CurrentSelectionIdx == globalIdx) + { + CurrentSelectionIdx = -1; + _currentItem = obj.ItemId; + CurrentSelection = default; + } + + ImGui.SameLine(); + var ret = ImGui.Selectable(name, selected); ImGui.SameLine(); using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080); - ImGuiUtil.RightAlign($"({obj.ModelId.Id}-{obj.WeaponType.Id}-{obj.Variant})"); + ImUtf8.TextRightAligned($"({obj.PrimaryId.Id}-{obj.SecondaryId.Id}-{obj.Variant.Id})"); return ret; } + protected override void OnClosePopup() + { + // If holding control while the popup closes, try to parse the input as a full tuple of set id, weapon id and variant, and set a custom item for that. + if (!ImGui.GetIO().KeyCtrl) + return; + + var split = Filter.Text.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length != 3 + || !ushort.TryParse(split[0], out var setId) + || !ushort.TryParse(split[1], out var weaponId) + || !byte.TryParse(split[2], out var variant)) + return; + + CustomSetId = setId; + CustomWeaponId = weaponId; + CustomVariant = variant; + } + protected override bool IsVisible(int globalIndex, LowerString filter) - => base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].ModelId.Id.ToString()); + => base.IsVisible(globalIndex, filter) || Items[globalIndex].ModelString.StartsWith(filter.Lower); protected override string ToString(EquipItem obj) => obj.Name; private static string GetLabel(FullEquipType type) - => type is FullEquipType.Unknown ? "Mainhand" : type.ToName(); + => type.IsUnknown() ? "Mainhand" : type.ToName(); - private static IReadOnlyList GetWeapons(ItemManager items, FullEquipType type) + private static IReadOnlyList GetWeapons(FavoriteManager favorites, ItemManager items, FullEquipType type) { if (type is FullEquipType.Unknown) { var enumerable = Array.Empty().AsEnumerable(); foreach (var t in Enum.GetValues().Where(e => e.ToSlot() is EquipSlot.MainHand)) { - if (items.ItemService.AwaitedService.TryGetValue(t, out var l)) + if (items.ItemData.ByType.TryGetValue(t, out var l)) enumerable = enumerable.Concat(l); } - return enumerable.OrderBy(e => e.Name).ToList(); + return [.. enumerable.OrderByDescending(favorites.Contains).ThenBy(e => e.Name)]; } - if (!items.ItemService.AwaitedService.TryGetValue(type, out var list)) - return Array.Empty(); + if (!items.ItemData.ByType.TryGetValue(type, out var list)) + return []; if (type.AllowsNothing()) - return list.OrderBy(e => e.Name).Prepend(ItemManager.NothingItem(type)).ToList(); + return [ItemManager.NothingItem(type), .. list.OrderByDescending(favorites.Contains).ThenBy(e => e.Name)]; - return list.OrderBy(e => e.Name).ToList(); + return [.. list.OrderByDescending(favorites.Contains).ThenBy(e => e.Name)]; } } diff --git a/Glamourer/Gui/GenericPopupWindow.cs b/Glamourer/Gui/GenericPopupWindow.cs index 7a201e8..5061862 100644 --- a/Glamourer/Gui/GenericPopupWindow.cs +++ b/Glamourer/Gui/GenericPopupWindow.cs @@ -1,7 +1,9 @@ -using System.Numerics; +using Dalamud.Game.ClientState.Conditions; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; -using ImGuiNET; +using Dalamud.Plugin.Services; +using Glamourer.Gui.Materials; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; @@ -9,10 +11,13 @@ namespace Glamourer.Gui; public class GenericPopupWindow : Window { - private readonly Configuration _config; - public bool OpenFestivalPopup { get; internal set; } = false; + private readonly Configuration _config; + private readonly AdvancedDyePopup _advancedDye; + private readonly ICondition _condition; + private readonly IClientState _state; + public bool OpenFestivalPopup { get; internal set; } = false; - public GenericPopupWindow(Configuration config) + public GenericPopupWindow(Configuration config, IClientState state, ICondition condition, AdvancedDyePopup advancedDye) : base("Glamourer Popups", ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoDecoration @@ -23,26 +28,43 @@ public class GenericPopupWindow : Window | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoTitleBar, true) { - _config = config; - IsOpen = true; + _config = config; + _state = state; + _condition = condition; + _advancedDye = advancedDye; + DisableWindowSounds = true; + IsOpen = true; } public override void Draw() { - if (OpenFestivalPopup) + if (OpenFestivalPopup && CheckFestivalPopupConditions()) { ImGui.OpenPopup("FestivalPopup"); OpenFestivalPopup = false; } DrawFestivalPopup(); + //_advancedDye.Draw(); } + private bool CheckFestivalPopupConditions() + => !_state.IsPvPExcludingDen + && !_condition[ConditionFlag.InCombat] + && !_condition[ConditionFlag.BoundByDuty] + && !_condition[ConditionFlag.WatchingCutscene] + && !_condition[ConditionFlag.WatchingCutscene78] + && !_condition[ConditionFlag.BoundByDuty95] + && !_condition[ConditionFlag.BoundByDuty56] + && !_condition[ConditionFlag.InDeepDungeon] + && !_condition[ConditionFlag.PlayingLordOfVerminion] + && !_condition[ConditionFlag.ChocoboRacing]; + private void DrawFestivalPopup() { var viewportSize = ImGui.GetWindowViewport().Size; - ImGui.SetNextWindowSize(new Vector2(viewportSize.X / 5, viewportSize.Y / 7)); + ImGui.SetNextWindowSize(new Vector2(Math.Max(viewportSize.X / 5, 400), Math.Max(viewportSize.Y / 7, 150))); ImGui.SetNextWindowPos(viewportSize / 2, ImGuiCond.Always, new Vector2(0.5f)); using var popup = ImRaii.Popup("FestivalPopup", ImGuiWindowFlags.Modal); if (!popup) diff --git a/Glamourer/Gui/GlamourerChangelog.cs b/Glamourer/Gui/GlamourerChangelog.cs index a03b230..686d4a1 100644 --- a/Glamourer/Gui/GlamourerChangelog.cs +++ b/Glamourer/Gui/GlamourerChangelog.cs @@ -20,18 +20,489 @@ public class GlamourerChangelog Add1_0_0_6(Changelog); Add1_0_1_1(Changelog); Add1_0_2_0(Changelog); + Add1_0_3_0(Changelog); + Add1_0_4_0(Changelog); + Add1_0_5_0(Changelog); + Add1_0_6_0(Changelog); + Add1_0_7_0(Changelog); + Add1_1_0_0(Changelog); + Add1_1_0_2(Changelog); + Add1_1_0_4(Changelog); + AddDummy(Changelog); + AddDummy(Changelog); + Add1_2_0_0(Changelog); + Add1_2_1_0(Changelog); + AddDummy(Changelog); + Add1_2_3_0(Changelog); + Add1_3_1_0(Changelog); + Add1_3_2_0(Changelog); + Add1_3_3_0(Changelog); + Add1_3_4_0(Changelog); + Add1_3_5_0(Changelog); + Add1_3_6_0(Changelog); + Add1_3_7_0(Changelog); + Add1_3_8_0(Changelog); + Add1_4_0_0(Changelog); + Add1_5_0_0(Changelog); + Add1_5_1_0(Changelog); } private (int, ChangeLogDisplayType) ConfigData() - => (_config.LastSeenVersion, _config.ChangeLogDisplayType); + => (_config.Ephemeral.LastSeenVersion, _config.ChangeLogDisplayType); private void Save(int version, ChangeLogDisplayType type) { - _config.LastSeenVersion = version; - _config.ChangeLogDisplayType = type; - _config.Save(); + if (_config.Ephemeral.LastSeenVersion != version) + { + _config.Ephemeral.LastSeenVersion = version; + _config.Ephemeral.Save(); + } + + if (_config.ChangeLogDisplayType != type) + { + _config.ChangeLogDisplayType = type; + _config.Save(); + } } + private static void Add1_5_1_0(Changelog log) + => log.NextVersion("Version 1.5.1.0") + .RegisterHighlight("Added support for Penumbras PCP functionality to add the current state of the character as a design.") + .RegisterEntry("On import, a design for the PCP is created and, if possible, applied to the character.", 1) + .RegisterEntry("No automation is assigned.", 1) + .RegisterEntry("Finer control about this can be found in the settings.", 1) + .RegisterEntry("Fixed an issue with static visors not toggling through Glamourer (1.5.0.7).") + .RegisterEntry("The advanced dye slot combo now contains glasses (1.5.0.7).") + .RegisterEntry("Several fixes for patch-related issues (1.5.0.1 - 1.5.0.6"); + + private static void Add1_5_0_0(Changelog log) + => log.NextVersion("Version 1.5.0.0") + .RegisterImportant("Updated for game version 7.30 and Dalamud API13, which uses a new GUI backend. Some things may not work as expected. Please let me know any issues you encounter.") + .RegisterHighlight("Added the new Viera Ears state to designs. Old designs will not apply the state.") + .RegisterHighlight("Added the option to make newly created designs write-protected by default to the design defaults.") + .RegisterEntry("Fixed issues with reverting state and IPC.") + .RegisterEntry("Fixed an issue when using the mousewheel to scroll through designs (1.4.0.3).") + .RegisterEntry("Fixed an issue with invalid bonus items (1.4.0.3).") + .RegisterHighlight("Added drag & drop of equipment pieces which will try to match the corresponding model IDs in other slots if possible (1.4.0.2).") + .RegisterEntry("Heavily optimized some issues when having many designs and creating new ones or updating them (1.4.0.2)") + .RegisterEntry("Fixed an issue with staining templates (1.4.0.1).") + .RegisterEntry("Fixed an issue with the QDB buttons not counting correctly (1.4.0.1)."); + + private static void Add1_4_0_0(Changelog log) + => log.NextVersion("Version 1.4.0.0") + .RegisterHighlight("The design selector width is now draggable within certain restrictions that depend on the total window width.") + .RegisterEntry("The current behavior may not be final, let me know if you have any comments.", 1) + .RegisterEntry("Regular customization colors can now be dragged & dropped onto other customizations.") + .RegisterEntry( + "If no identical color is available in the target slot, the most similar color available (for certain values of similar) will be chosen instead.", + 1) + .RegisterEntry("Resetting advanced dyes and customizations has been split into two buttons for the quick design bar.") + .RegisterEntry("Weapons now also support custom ID input in the combo search box.") + .RegisterEntry("Added new IPC methods GetExtendedDesignData, AddDesign, DeleteDesign, GetDesignBase64, GetDesignJObject.") + .RegisterEntry("Added the option to prevent immediate repeats for random design selection (Thanks Diorik!).") + .RegisterEntry("Optimized some multi-design changes when selecting many designs and changing them at once.") + .RegisterEntry("Fixed item combos not starting from the currently selected item when scrolling them via mouse wheel.") + .RegisterEntry("Fixed some issue with Glamourer not searching mods by name for mod associations in some cases.") + .RegisterEntry("Fixed the IPC methods SetMetaState and SetMetaStateName not working (Thanks Caraxi!).") + .RegisterEntry("Added new IPC method GetDesignListExtended. (1.3.8.6)") + .RegisterEntry( + "Improved the naming of NPCs for identifiers by using Haselnussbombers new naming functionality (Thanks Hasel!). (1.3.8.6)") + .RegisterEntry( + "Added a modifier key separate from the delete modifier key that is used for less important key-checks, specifically toggling incognito mode. (1.3.8.5)") + .RegisterEntry("Used better Penumbra IPC for some things. (1.3.8.5)") + .RegisterEntry("Fixed an issue with advanced dyes for weapons. (1.3.8.5)") + .RegisterEntry("Fixed an issue with NPC automation due to missing job detection. (1.3.8.1)"); + + private static void Add1_3_8_0(Changelog log) + => log.NextVersion("Version 1.3.8.0") + .RegisterImportant("Updated Glamourer for update 7.20 and Dalamud API 12.") + .RegisterEntry( + "This is not thoroughly tested, but I decided to push to stable instead of testing because otherwise a lot of people would just go to testing just for early access again despite having no business doing so.", + 1) + .RegisterEntry( + "I also do not use most of the functionality of Glamourer myself, so I am unable to even encounter most issues myself.", 1) + .RegisterEntry("If you encounter any issues, please report them quickly on the discord.", 1) + .RegisterEntry("Added a chat command to clear temporary settings applied by Glamourer to Penumbra.") + .RegisterEntry("Fixed small issues with customizations not applicable to your race still applying."); + + private static void Add1_3_7_0(Changelog log) + => log.NextVersion("Version 1.3.7.0") + .RegisterImportant( + "The option to disable advanced customizations or advanced dyes has been removed. The functionality can no longer be disabled entirely, you can just decide not to use it, and to hide it.") + .RegisterHighlight( + "You can now configure which panels (like Customization, Equipment, Advanced Customization etc.) are displayed at all, and which are expanded by default. This does not disable any functionality.") + .RegisterHighlight( + "The Unlocks tab now shows whether items are modded in the currently selected collection in Penumbra in Overview mode and shows and can filter and sort for it in Detailed mode.") + .RegisterEntry("Added an optional button to the Quick Design Bar to reset all temporary settings applied by Glamourer.") + .RegisterHighlight( + "Any existing advanced dyes will now be highlighted on the corresponding Advanced Dye buttons in the actors panel and on the corresponding equip slot name in the design panel.") + .RegisterEntry("This also affects currently inactive advanced dyes, which can now be manually removed on the inactive materials.", + 1) + .RegisterHighlight( + "In the design list of an automation set, the design indices are now highlighted if a design contains advanced dyes, mod associations, or links to other designs.") + .RegisterHighlight("Some quality of life improvements:") + .RegisterEntry("Added some buttons for some application rule presets to the Application Rules panel.", 1) + .RegisterEntry("Added some buttons to enable, disable or delete all advanced dyes in a design.", 1) + .RegisterEntry("Some of those buttons are also available in multi-design selection to apply to all selected designs at once.", 1) + .RegisterEntry( + "A copied material color set from Penumbra should now be able to be imported into a advanced dye color set, as well as the other way around.") + .RegisterEntry( + "Automatically applied character updates when applying a design with mod associations and temporary settings are now skipped to prevent some issues with GPose. This should not affect anything else.") + .RegisterEntry("Glamourer now differentiates between temporary settings applied through manual or automatic application."); + + + private static void Add1_3_6_0(Changelog log) + => log.NextVersion("Version 1.3.6.0") + .RegisterHighlight("Added some new multi design selection functionality to change design settings of many designs at once.") + .RegisterEntry("Also added the number of selected designs and folders to the multi design selection display.", 1) + .RegisterEntry("Glamourer will now use temporary settings when saving mod associations, if they exist in Penumbra.") + .RegisterEntry( + "Actually added the checkbox to reset all temporary settings to Automation Sets (functionality was there, just not exposed to the UI...).") + .RegisterEntry( + "Adapted the behavior for identified copies of characters that have a different state than the character itself to deal with the associated Penumbra changes.") + .RegisterEntry( + "Added '/glamour resetdesign' as a command, that re-applies automation but resets randomly chosen designs (Thanks Diorik).") + .RegisterEntry("All existing facepaints should now be accepted in designs, including NPC facepaints.") + .RegisterEntry( + "Overwriting a design with your characters current state will now discard any prior advanced dyes and only add those from the current state.") + .RegisterEntry("Fixed an issue with racial mount and accessory scaling when changing zones on a changed race.") + .RegisterEntry("Fixed issues with the detection of gear set changes in certain circumstances (Thanks Cordelia).") + .RegisterEntry("Fixed an issue with the Force to Inherit checkbox in mod associations.") + .RegisterEntry( + "Added a new IPC event that fires only when Glamourer finalizes its current changes to a character (for/from Cordelia).") + .RegisterEntry("Added new IPC to set a meta flag on actors. (for/from Cordelia)."); + + private static void Add1_3_5_0(Changelog log) + => log.NextVersion("Version 1.3.5.0") + .RegisterHighlight( + "Added the usage of the new Temporary Mod Setting functionality from Penumbra to apply mod associations. This is on by default but can be turned back to permanent changes in the settings.") + .RegisterEntry("Designs now have a setting to always reset all prior temporary settings made by Glamourer on application.", 1) + .RegisterEntry("Automation Sets also have a setting to do this, independently of the designs contained in them.", 1) + .RegisterHighlight("More NPC customization options should now be accepted as valid for designs, regardless of clan/gender.") + .RegisterHighlight("The 'Apply' chat command had the currently selected design and the current quick bar design added as choices.") + .RegisterEntry( + "The application buttons for designs, NPCs or actors should now stick at the top of their respective panels even when scrolling down.") + .RegisterHighlight("Randomly chosen designs should now stay across loading screens or redrawing. (1.3.4.3)") + .RegisterEntry( + "In automation, Random designs now have an option to always choose another design, including during loading screens or redrawing.", + 1) + .RegisterEntry("Fixed an issue where disabling auto designs did not work as expected.") + .RegisterEntry("Fixed the inversion of application flags in IPC calls.") + .RegisterEntry("Fixed an issue with the scaling of the Advanced Dye popup with increased font sizes.") + .RegisterEntry("Fixed a bug when editing gear set conditions in the automation tab.") + .RegisterEntry("Fixed some ImGui issues."); + + private static void Add1_3_4_0(Changelog log) + => log.NextVersion("Version 1.3.4.0") + .RegisterEntry("Glamourer has been updated for Dalamud API 11 and patch 7.1.") + .RegisterEntry("Maybe fixed issues with shared weapon types and reset designs.") + .RegisterEntry("Fixed issues with resetting advanced dyes and certain weapon types."); + + private static void Add1_3_3_0(Changelog log) + => log.NextVersion("Version 1.3.3.0") + .RegisterHighlight("Added the option to create automations for owned human NPCs (like trust avatars).") + .RegisterEntry("Added some special filters to the Actors tab selector, hover over it to see the options.") + .RegisterEntry("Added an option for designs to always reset all previously applied advanced dyes.") + .RegisterEntry("Added some new NPC-only customizations to the valid customizations.") + .RegisterEntry("Reworked quite a bit of things around face wear / bonus items. Please let me know if anything broke."); + + private static void Add1_3_2_0(Changelog log) + => log.NextVersion("Version 1.3.2.0") + .RegisterEntry("Fixed an issue with weapon hiding when leaving GPose or changing zones.") + .RegisterEntry("Added support for unnamed items to be previewed from Penumbra.") + .RegisterEntry( + "Item combos filters now check if the model string starts with the current filter, instead of checking if the primary ID contains the current filter.") + .RegisterEntry("Improved the handling of bonus items (glasses) in designs.") + .RegisterEntry("Imported .chara files now import bonus items.") + .RegisterEntry( + "Added a Debug Data rider in the Actors tab that is visible if Debug Mode is enabled and (currently) contains some IDs.") + .RegisterEntry("Fixed bonus items not reverting correctly in some cases.") + .RegisterEntry("Fixed an issue with the RNG in cheat codes and events skipping some possible entries.") + .RegisterEntry("Fixed the chat log context menu for glamourer Try-On.") + .RegisterEntry("Fixed some issues with cheat code sets.") + .RegisterEntry( + "Made the popped out Advanced Dye Window and Unlocks Window non-docking as that caused issues when docked to the main Glamourer window.") + .RegisterEntry("Refreshed NPC name associations.") + .RegisterEntry("Removed a now useless cheat code.") + .RegisterEntry("Added API for Bonus Items. (1.3.1.1)"); + + private static void Add1_3_1_0(Changelog log) + => log.NextVersion("Version 1.3.1.0") + .RegisterHighlight("Glamourer is now released for Dawntrail!") + .RegisterEntry("Added support for female Hrothgar.", 1) + .RegisterEntry("Added support for the Glasses slot.", 1) + .RegisterEntry("Added support for two dye slots.", 1) + .RegisterImportant( + "There were some issues with Advanced Dyes stored in Designs. When launching this update, Glamourer will try to migrate all your old designs into the new form.") + .RegisterEntry("Unfortunately, this is slightly based on guesswork and may cause false-positive migrations.", 1) + .RegisterEntry("In general, the values for Gloss and Specular Strength were swapped, so the migration swaps them back.", 1) + .RegisterEntry( + "In some cases this may not be correct, or the values stored were problematic to begin with and will now cause further issues.", + 1) + .RegisterImportant( + "If your designs lose their specular color, you need to verify that the Specular Strength is non-zero (usually in 0-100%).", 1) + .RegisterImportant( + "If your designs are extremely glossy and reflective, you need to verify that the Gloss value is greater than zero (usually a power of 2 >= 1, it should never be 0).", + 1) + .RegisterEntry( + "I am very sorry for the inconvenience but there is no way to salvage this sanely in all cases, especially with user-input values.", + 1) + .RegisterImportant( + "Any materials already using Dawntrails shaders will currently not be able to edit the Gloss or Specular Strength Values in Advanced Dyes.") + .RegisterImportant( + "Skin and Hair Shine from advanced customizations are not supported by the game any longer, so they are not displayed for the moment.") + .RegisterHighlight("All eyes now support Limbal rings (which use the Feature Color for their color).") + .RegisterHighlight("Dyes can now be dragged and dropped onto other dyes to replicate them.") + .RegisterEntry("The job filter in the Unlocks tab has been improved.") + .RegisterHighlight( + "Editing designs or actors now has a history and you can undo up to 16 of the last changes you made, separately per design or actor.") + .RegisterEntry( + "Some changes (like when a weapon applies its offhand) may count as multiple and have to be stepped back separately.", 1) + .RegisterEntry("You can now change the priority or enabled state of associated mods directly.") + .RegisterEntry("Glamourer now has a Support Info button akin to Penumbra's.") + .RegisterEntry("Glamourer now respects write protection on designs better.") + .RegisterEntry("The advanced dye window popup should now get focused when it is opening even in detached state.") + .RegisterEntry("Added API and IPC for bonus items, i.e. the Glasses slot.") + .RegisterHighlight("You can now display your characters height in Corgis or Olympic Swimming Pools.") + .RegisterEntry("Fixed some issues with advanced customizations and dyes applied via IPC. (1.2.3.2)") + .RegisterEntry( + "Glamourer now uses the last matching game object for advanced dyes instead of the first (mainly relevant for GPose). (1.2.3.1)"); + + private static void Add1_2_3_0(Changelog log) + => log.NextVersion("Version 1.2.3.0") + .RegisterHighlight( + "Added a field to rename designs directly from the mod selector context menu, instead of moving them in the filesystem.") + .RegisterEntry("You can choose which rename field (none, either one or both) to display in the settings.", 1) + .RegisterEntry("Automatically applied offhand weapons due to mainhand settings now also apply the mainhands dye.") + .RegisterHighlight("Added a height display in real-world units next to the height-selector.") + .RegisterEntry("This can be configured to use your favourite unit of measurement, even wrong ones, or not display at all.", 1) + .RegisterHighlight( + "Added a chat command '/glamour applycustomization' that can apply single customization values to actors. Use without arguments for help.") + .RegisterHighlight( + "Added an option for designs to always force a redraw when applied to a character, regardless of whether it is necessary or not.") + .RegisterHighlight("Added a button to overwrite the selected design with the current player state.") + .RegisterEntry("Added some copy/paste functionality for mod associations.") + .RegisterEntry("Reworked the API and IPC structure heavily.") + .RegisterEntry("Added warnings if Glamourer can not attach successfully to Penumbra or if Penumbras IPC version is not correct.") + .RegisterEntry("Added hints for all of the available cheat codes and improved the cheat code display somewhat.") + .RegisterEntry("Fixed weapon selectors not having a favourite star available.") + .RegisterEntry("Fixed issues with items with custom names.") + .RegisterEntry("Fixed the labels for eye colors.") + .RegisterEntry("Fixed the tooltip for Apply Dye checkboxes.") + .RegisterEntry("Fixed an issue when hovering over assigned mod settings.") + .RegisterEntry("Made conformant to Dalamud guidelines by adding a button to open the main UI.") + .RegisterEntry("Fixed an issue with visor states. (1.2.1.3)") + .RegisterEntry("Fixed an issue with identical weapon types and multiple restricted designs. (1.2.1.3)"); + + private static void Add1_2_1_0(Changelog log) + => log.NextVersion("Version 1.2.1.0") + .RegisterEntry("Updated for .net 8 and FFXIV 6.58, using some new framework options to improve performance and stability.") + .RegisterEntry("Previewing changed items in Penumbra now works with all weapons in GPose. (1.2.0.8)") + .RegisterEntry( + "Added a design type selectable for automation that applies the design currently selected in the quick design bar. (1.2.0.4)") + .RegisterEntry("Added an option to respect manual changes when changing automation settings. (1.2.0.3)") + .RegisterEntry( + "You can now apply designs to the player character with a double click on them (can be turned off in settings). (1.2.0.1)") + .RegisterEntry("The last selected design and tab are now stored and applied on startup. (1.2.0.1)") + .RegisterEntry("Fixed behavior of revert to automation to actually revert and not just reapply. (1.2.0.8)") + .RegisterEntry("Added Reapply Automation buttons and chat commands with prior behaviour.", 1) + .RegisterEntry("Fixed random design never applying the last design in the set. (1.2.0.7)") + .RegisterEntry("Fixed colors of special designs. (1.2.0.7)") + .RegisterEntry("Fixed issues with weapon tracking. (1.2.0.5, 1.2.0.6)") + .RegisterEntry("Fixed issues with moved items and gearset changes not being listened to. (1.2.0.4)") + .RegisterEntry("Fixed issues with applying advanced dyes in fixed states. (1.2.0.2)") + .RegisterEntry("Fixed issues turning non-humans human. (1.2.0.1)") + .RegisterEntry("Fixed issues with body type application. (1.2.0.1, 1.2.0.2)") + .RegisterEntry("Fixed issue with design link application rule checkboxes. (1.2.0.1)"); + + private static void Add1_2_0_0(Changelog log) + => log.NextVersion("Version 1.2.0.0") + .RegisterHighlight("Added the option to link to other designs in a design, causing all of them to be applied at once.") + .RegisterEntry("This required reworking the handling for applying multiple designs at once (i.e.merging them).", 1) + .RegisterEntry( + "This was a considerable backend change on both automation sets and design application. I may have messed up and introduced bugs. " + + "The new version was on Testing for multiple weeks, but not many people use it. " + + "Please let me know if something does not work right anymore.", + 1) + .RegisterHighlight("Added advanced dye options for equipment. You can now live-edit the color sets of your gear.") + .RegisterEntry( + "The logic for this is very complicated and may interfere with other options or not update correctly, it will need a lot of testing.", + 1) + .RegisterEntry("Like Advanced Customization options, this can be turned off in the behaviour settings.", 1) + .RegisterEntry( + "To access the options, click the palette buttons in the Equipment Panel - the popup can also be detached from the main window in the settings.", + 1) + .RegisterEntry("In designs, only actually changed rows will be stored. You can manually add rows, too.", 1) + .RegisterHighlight( + "Added an option so that manual application of a mainhand weapon will also automatically apply its associated offhand (and gloves, for certain fist weapons). This is off by default.") + .RegisterHighlight( + "Added an option that always tries to apply associated mod settings for designs to the Penumbra collection associated with the character the design is applied to.") + .RegisterEntry( + "This is off by default and I strongly recommend AGAINST using it, since Glamourer has no way to revert such changes. You are responsible for keeping your collection in order.", + 1) + .RegisterHighlight( + "Added mouse wheel scrolling to many selectors, e.g. for equipment, dyes or customizations. You need to hold Control while scrolling in most places.") + .RegisterEntry("Improved handling for highlights with advanced customization colors and normal customization settings.") + .RegisterHighlight( + "Changed Item Customizations in Penumbra can now be right-clicked to preview them on your character, if you have the correct Gender/Race combo on them.") + .RegisterHighlight( + "Add the option to override associated collections for characters, so that automatically applied mod associations affect the overriden collection.") + .RegisterHighlight( + "Added the option to apply random designs (with optional restrictions) to characters via slash commands and automation.") + .RegisterEntry("Added copy/paste buttons for advanced customization colors.") + .RegisterEntry("Added alpha preview to advanced customization colors.") + .RegisterEntry("Added a button to update the settings for an associated mod from their current settings.") + .RegisterHighlight("Added 'Revert Equipment' and 'Revert Customizations' buttons to the Quick Design Bar.") + .RegisterEntry("You can now toggle every functionality of the Quick Design Bar on or off separately.") + .RegisterEntry("Updated a few fun module things. Now there are Pink elephants on parade!") + .RegisterEntry("Split up the IPC source state so IPC consumers can apply designs without them sticking around.") + .RegisterEntry("Fixed an issue with gearset changes not registering in Glamourer for Automation.") + .RegisterEntry("Fixed an issue with weapon loading being dependant on the order of loading Penumbra and Glamourer.") + .RegisterEntry( + "Fixed an issue with buttons sharing state and switching from design duplication to creating new ones caused errors.") + .RegisterEntry("Fixed an issue where actors leaving during cutscenes or GPose caused Glamourer to throw a fit.") + .RegisterEntry("Fixed an issue with NPC designs applying advanced customizations to targets and coloring them entirely black."); + + private static void AddDummy(Changelog log) + => log.NextVersion(string.Empty); + + private static void Add1_1_0_4(Changelog log) + => log.NextVersion("Version 1.1.0.4") + .RegisterEntry("Added a check and warning for a lingering Palette+ installation.") + .RegisterHighlight( + "Added a button to only revert advanced customizations to game state to the quick design bar. This can be toggled off in the interface settings.") + .RegisterEntry("Added visible configuration options for color display for the advanced customizations.") + .RegisterEntry("Updated Battle NPC data from Gubal for 6.55.") + .RegisterEntry("Fixed issues with advanced customizations not resetting correctly with Use Game State as Base.") + .RegisterEntry("Fixed an issues with non-standard body type customizations not transmitting through Mare.") + .RegisterEntry("Fixed issues with application rule checkboxes not working for advanced parameters.") + .RegisterEntry("Fixed an issue with fist weapons, again again.") + .RegisterEntry("Fixed multiple issues with advanced parameters not applying after certain other changes.") + .RegisterEntry("Fixed another wrong restricted item."); + + private static void Add1_1_0_2(Changelog log) + => log.NextVersion("Version 1.1.0.2") + .RegisterEntry("Added design colors in the preview of combos (in the quick bar and the automation panel).") + .RegisterHighlight("Improved Palette+ import options: Instead of entering a name, you can now select from available palettes.") + .RegisterHighlight("In the settings tab, there is also a button to import ALL palettes from Palette+ as separate designs.", 1) + .RegisterEntry( + "Added a tooltip that you can enter numeric values to drag sliders by control-clicking for the muscle slider, also used slightly more useful caps.") + .RegisterEntry("Fixed issues with monk weapons, again.") + .RegisterEntry("Fixed an issue with the favourites file not loading.") + .RegisterEntry("Fixed the name of the advanced parameters in the application panel.") + .RegisterEntry("Fixed design clones not respecting advanced parameter application rules."); + + + private static void Add1_1_0_0(Changelog log) + => log.NextVersion("Version 1.1.0.0") + .RegisterHighlight("Added a new tab to browse, apply or copy (human) NPC appearances.") + .RegisterHighlight("A characters body type can now be changed when copying state or saving designs from certain NPCs.") + .RegisterHighlight("Added support for picking advanced colors for your characters customizations.") + .RegisterEntry("The display and application of those can be toggled off in Glamourers behaviour settings.", 1) + .RegisterEntry( + "This provides the same functionality as Palette+, and Palette+ will probably be discontinued soonish (in accordance with Chirp).", + 1) + .RegisterEntry( + "An option to import existing palettes from Palette+ by name is provided for designs, and can be toggled off in the settings.", + 1) + .RegisterHighlight( + "Advanced colors, equipment and dyes can now be reset to their game state separately by Control-Rightclicking them.") + .RegisterHighlight("Hairstyles and face paints can now be made favourites.") + .RegisterEntry("Added a new command '/glamour delete' to delete saved designs by name or identifier.") + .RegisterEntry( + "Added an optional parameter to the '/glamour apply' command that makes it apply the associated mod settings for a design to the collection associated with the identified character.") + .RegisterEntry("Fixed changing weapons in Designs not working correctly.") + .RegisterEntry("Fixed restricted gear protection breaking outfits for Mare pairs.") + .RegisterEntry("Improved the handling of some cheat codes and added new ones.") + .RegisterEntry("Added IPC to set single items or stains on characters.") + .RegisterEntry("Added IPC to apply designs by GUID, and obtain a list of designs."); + + private static void Add1_0_7_0(Changelog log) + => log.NextVersion("Version 1.0.7.0") + .RegisterHighlight("Glamourer now can set the free company crests on body slots, head slots and shields.") + .RegisterEntry("Fixed an issue with tooltips in certain combo selectors.") + .RegisterEntry("Fixed some issues with Hide Hat Gear and monsters turned into humans.") + .RegisterEntry( + "Hopefully fixed issues with icons used by Glamourer that are modified through Penumbra preventing Glamourer to even start in some cases.") + .RegisterEntry("Those icons might still not appear if they fail to load, but Glamourer should at least still work.", 1) + .RegisterEntry("Pre-emptively fixed a potential issue for the holidays."); + + private static void Add1_0_6_0(Changelog log) + => log.NextVersion("Version 1.0.6.0") + .RegisterHighlight("Added the option to define custom color groups and associate designs with them.") + .RegisterEntry("You can create and name design colors in Settings -> Colors -> Custom Design Colors.", 1) + .RegisterEntry( + "By default, all designs have an automatic coloring corresponding to the current system, that chooses a color dynamically based on application rules.", + 1) + .RegisterEntry( + "Example: You create a custom color named 'Test' and make it bright blue. Now you assign 'Test' to some design in its Design Details, and it will always display bright blue in the design list.", + 1) + .RegisterEntry("Design colors are stored by name. If a color can not be found, the design will display the Missing Color instead.", + 1) + .RegisterEntry("You can filter for designs using specific colors via c:", 1) + .RegisterHighlight( + "You can now filter for the special case 'None' for filters where that makes sense (like Tags or Mod Associations).") + .RegisterHighlight( + "When selecting multiple designs, you can now add or remove tags from them at once, and set their colors at once.") + .RegisterEntry("Improved tri-state checkboxes. The colors of the new symbols can be changed in Color Settings.") + .RegisterEntry("Removed half-baked localization of customization names and fixed some names in application rules.") + .RegisterEntry("Improved Brio compatibility") + .RegisterEntry("Fixed some display issues with text color on locked designs.") + .RegisterEntry("Fixed issues with automatic design color display for customization-only designs.") + .RegisterEntry("Removed borders from the quick design window regardless of custom styling.") + .RegisterEntry("Improved handling of (un)available customization options.") + .RegisterEntry( + "Some configuration like the currently selected tab states are now stored in a separate file that is not backed up and saved less often.") + .RegisterEntry("Added option to open the Glamourer main window at game start independently of Debug Mode."); + + private static void Add1_0_5_0(Changelog log) + => log.NextVersion("Version 1.0.5.0") + .RegisterHighlight("Dyes are can now be favorited the same way equipment pieces can.") + .RegisterHighlight( + "The quick design bar combo can now be scrolled through via mousewheel when hovering over the combo without opening it.") + .RegisterEntry( + "Control-Rightclicking the quick design bar now not only jumps to the corresponding design, but also opens the main window if it is not currently open.") + .RegisterHighlight("You can now filter for designs containing specific items by using \"i:partial item name\".") + .RegisterEntry( + "When overwriting a saved designs data entirely from clipboard, you can now undo this change and restore the prior design data once via a button top-left.") + .RegisterEntry("Removed the \"Enabled\" checkbox in the settings since it was barely doing anything but breaking Glamourer.") + .RegisterEntry( + "If you want to disable Glamourers state-tracking and hooking, you will need to disable the entire Plugin via Dalamud now.", 1) + .RegisterEntry("Added a reference to \"/glamour\" in the \"/glamourer help\" section.") + .RegisterEntry("Updated BNPC Data with new crowd-sourced data from the gubal library.") + .RegisterEntry("Fixed an issue with the quick design bar when no designs are saved.") + .RegisterEntry("Fixed a problem with characters not redrawing after leaving GPose even if necessary."); + + private static void Add1_0_4_0(Changelog log) + => log.NextVersion("Version 1.0.4.0") + .RegisterEntry("The GPose target is now used for target-dependent functionality in GPose.") + .RegisterEntry("Fixed a few issues with transformations, especially their weapons and head gear.") + .RegisterEntry( + "Previewing Offhand Models for both-handed weapons via right click is now possible (may need to wait for a not-yet released Penumbra update).") + .RegisterEntry("Updated the known list of Battle NPCs.") + .RegisterEntry("Removed another technically unrestricted item from restricted item list.") + .RegisterEntry("Use local time for discerning the current day on start-up instead of UTC-time.") + .RegisterEntry("Improved the Unlocks Table with additional info. (1.0.3.1)") + .RegisterEntry("Added position locking option and more color options. (1.0.3.1)") + .RegisterEntry("Removed the default key combination for toggling the quick bar. (1.0.3.1)"); + + private static void Add1_0_3_0(Changelog log) + => log.NextVersion("Version 1.0.3.0") + .RegisterEntry("Hopefully improved Palette+ compatibility.") + .RegisterHighlight( + "Added a Quick Design Bar, which is a small bar in which you can select your designs and apply them to yourself or your target, or revert them.") + .RegisterEntry("You can toggle visibility of this bar via keybinds, which you can set up in the settings tab.", 1) + .RegisterEntry("You can also lock the bar, and enable or disable an additional, identical bar in the main window.", 1) + .RegisterEntry("Disabled a sound that played on startup when a certain Dalamud setting was enabled.") + .RegisterEntry("Fixed an issue with reading state for Who Am I!?!. (1.0.2.2)") + .RegisterEntry("Fixed an issue where applying gear sets would not always update your dyes. (1.0.2.2)") + .RegisterEntry("Fixed an issue where some errors due to missing null-checks wound up in the log. (1.0.2.2)") + .RegisterEntry("Fixed an issue with hat visibility. (1.0.2.1 and 1.0.2.2)") + .RegisterEntry("Improved some logging. (1.0.2.1)") + .RegisterEntry("Improved notifications when encountering errors while loading automation sets. (1.0.2.1)") + .RegisterEntry("Fixed another issue with monk fist weapons. (1.0.2.1)") + .RegisterEntry("Added missing dot to changelog entry."); + private static void Add1_0_2_0(Changelog log) => log.NextVersion("Version 1.0.2.0") .RegisterHighlight("Added option to favorite items so they appear first in the item selection combos.") diff --git a/Glamourer/Gui/GlamourerWindowSystem.cs b/Glamourer/Gui/GlamourerWindowSystem.cs index 8858c9e..f86f42b 100644 --- a/Glamourer/Gui/GlamourerWindowSystem.cs +++ b/Glamourer/Gui/GlamourerWindowSystem.cs @@ -1,5 +1,4 @@ -using System; -using Dalamud.Interface; +using Dalamud.Interface; using Dalamud.Interface.Windowing; using Glamourer.Gui.Tabs.UnlocksTab; @@ -8,12 +7,12 @@ namespace Glamourer.Gui; public class GlamourerWindowSystem : IDisposable { private readonly WindowSystem _windowSystem = new("Glamourer"); - private readonly UiBuilder _uiBuilder; + private readonly IUiBuilder _uiBuilder; private readonly MainWindow _ui; private readonly PenumbraChangedItemTooltip _penumbraTooltip; - public GlamourerWindowSystem(UiBuilder uiBuilder, MainWindow ui, GenericPopupWindow popups, PenumbraChangedItemTooltip penumbraTooltip, - Configuration config, UnlocksTab unlocksTab, GlamourerChangelog changelog) + public GlamourerWindowSystem(IUiBuilder uiBuilder, MainWindow ui, GenericPopupWindow popups, PenumbraChangedItemTooltip penumbraTooltip, + Configuration config, UnlocksTab unlocksTab, GlamourerChangelog changelog, DesignQuickBar quick) { _uiBuilder = uiBuilder; _ui = ui; @@ -22,14 +21,18 @@ public class GlamourerWindowSystem : IDisposable _windowSystem.AddWindow(popups); _windowSystem.AddWindow(unlocksTab); _windowSystem.AddWindow(changelog.Changelog); + _windowSystem.AddWindow(quick); + _uiBuilder.OpenMainUi += _ui.Toggle; _uiBuilder.Draw += _windowSystem.Draw; - _uiBuilder.OpenConfigUi += _ui.Toggle; + _uiBuilder.OpenConfigUi += _ui.OpenSettings; _uiBuilder.DisableCutsceneUiHide = !config.HideWindowInCutscene; + _uiBuilder.DisableUserUiHide = config.ShowWindowWhenUiHidden; } public void Dispose() { + _uiBuilder.OpenMainUi -= _ui.Toggle; _uiBuilder.Draw -= _windowSystem.Draw; - _uiBuilder.OpenConfigUi -= _ui.Toggle; + _uiBuilder.OpenConfigUi -= _ui.OpenSettings; } } diff --git a/Glamourer/Gui/MainWindow.cs b/Glamourer/Gui/MainWindow.cs index 1a68018..abde603 100644 --- a/Glamourer/Gui/MainWindow.cs +++ b/Glamourer/Gui/MainWindow.cs @@ -1,6 +1,4 @@ -using System; -using System.Numerics; -using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Glamourer.Designs; @@ -8,14 +6,30 @@ using Glamourer.Events; using Glamourer.Gui.Tabs; using Glamourer.Gui.Tabs.ActorTab; using Glamourer.Gui.Tabs.AutomationTab; +using Glamourer.Gui.Tabs.DebugTab; using Glamourer.Gui.Tabs.DesignTab; +using Glamourer.Gui.Tabs.NpcTab; +using Glamourer.Gui.Tabs.SettingsTab; using Glamourer.Gui.Tabs.UnlocksTab; -using ImGuiNET; +using Glamourer.Interop.Penumbra; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Classes; using OtterGui.Custom; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; namespace Glamourer.Gui; +public class MainWindowPosition : IService +{ + public bool IsOpen { get; set; } + public Vector2 Position { get; set; } + public Vector2 Size { get; set; } +} + public class MainWindow : Window, IDisposable { public enum TabType @@ -28,12 +42,16 @@ public class MainWindow : Window, IDisposable Automation = 4, Unlocks = 5, Messages = 6, + Npcs = 7, } - private readonly Configuration _config; - private readonly TabSelected _event; - private readonly ConvenienceRevertButtons _convenienceButtons; - private readonly ITab[] _tabs; + private readonly Configuration _config; + private readonly PenumbraService _penumbra; + private readonly DesignQuickBar _quickBar; + private readonly TabSelected _event; + private readonly MainWindowPosition _position; + private readonly ITab[] _tabs; + private bool _ignorePenumbra = false; public readonly SettingsTab Settings; public readonly ActorTab Actors; @@ -41,14 +59,15 @@ public class MainWindow : Window, IDisposable public readonly DesignTab Designs; public readonly AutomationTab Automation; public readonly UnlocksTab Unlocks; + public readonly NpcTab Npcs; public readonly MessagesTab Messages; - public TabType SelectTab = TabType.None; + public TabType SelectTab; - public MainWindow(DalamudPluginInterface pi, Configuration config, SettingsTab settings, ActorTab actors, DesignTab designs, - DebugTab debugTab, AutomationTab automation, UnlocksTab unlocks, TabSelected @event, ConvenienceRevertButtons convenienceButtons, - MessagesTab messages) - : base(GetLabel()) + public MainWindow(IDalamudPluginInterface pi, Configuration config, SettingsTab settings, ActorTab actors, DesignTab designs, + DebugTab debugTab, AutomationTab automation, UnlocksTab unlocks, TabSelected @event, MessagesTab messages, DesignQuickBar quickBar, + NpcTab npcs, MainWindowPosition position, PenumbraService penumbra) + : base("GlamourerMainWindow") { pi.UiBuilder.DisableGposeUiHide = true; SizeConstraints = new WindowSizeConstraints() @@ -56,44 +75,96 @@ public class MainWindow : Window, IDisposable MinimumSize = new Vector2(700, 675), MaximumSize = ImGui.GetIO().DisplaySize, }; - Settings = settings; - Actors = actors; - Designs = designs; - Automation = automation; - Debug = debugTab; - Unlocks = unlocks; - _event = @event; - _convenienceButtons = convenienceButtons; - Messages = messages; - _config = config; - _tabs = new ITab[] - { + Settings = settings; + Actors = actors; + Designs = designs; + Automation = automation; + Debug = debugTab; + Unlocks = unlocks; + _event = @event; + Messages = messages; + _quickBar = quickBar; + Npcs = npcs; + _position = position; + _config = config; + _penumbra = penumbra; + _tabs = + [ settings, actors, designs, automation, unlocks, + npcs, messages, debugTab, - }; + ]; + SelectTab = _config.Ephemeral.SelectedTab; _event.Subscribe(OnTabSelected, TabSelected.Priority.MainWindow); - IsOpen = _config.DebugMode; + IsOpen = _config.OpenWindowAtStart; + + _penumbra.DrawSettingsSection += Settings.DrawPenumbraIntegrationSettings; + } + + public void OpenSettings() + { + IsOpen = true; + SelectTab = TabType.Settings; + } + + public override void PreDraw() + { + Flags = _config.Ephemeral.LockMainWindow + ? Flags | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize + : Flags & ~(ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize); + _position.IsOpen = IsOpen; + WindowName = GetLabel(); } public void Dispose() - => _event.Unsubscribe(OnTabSelected); + { + _event.Unsubscribe(OnTabSelected); + _penumbra.DrawSettingsSection -= Settings.DrawPenumbraIntegrationSettings; + } public override void Draw() { var yPos = ImGui.GetCursorPosY(); - if (TabBar.Draw("##tabs", ImGuiTabBarFlags.None, ToLabel(SelectTab), out var currentTab, () => { }, _tabs)) - { - SelectTab = TabType.None; - _config.SelectedTab = FromLabel(currentTab); - _config.Save(); - } + _position.Size = ImGui.GetWindowSize(); + _position.Position = ImGui.GetWindowPos(); - _convenienceButtons.DrawButtons(yPos); + if (!_penumbra.Available && !_ignorePenumbra) + { + if (_penumbra.CurrentMajor == 0) + DrawProblemWindow( + "Could not attach to Penumbra. Please make sure Penumbra is installed and running.\n\nPenumbra is required for Glamourer to work properly."); + else if (_penumbra is + { + + CurrentMajor: PenumbraService.RequiredPenumbraBreakingVersion, + CurrentMinor: >= PenumbraService.RequiredPenumbraFeatureVersion, + }) + DrawProblemWindow( + $"You are currently not attached to Penumbra, seemingly by manually detaching from it.\n\nPenumbra's last API Version was {_penumbra.CurrentMajor}.{_penumbra.CurrentMinor}.\n\nPenumbra is required for Glamourer to work properly."); + else + DrawProblemWindow( + $"Attaching to Penumbra failed.\n\nPenumbra's API Version was {_penumbra.CurrentMajor}.{_penumbra.CurrentMinor}, but Glamourer requires a version of {PenumbraService.RequiredPenumbraBreakingVersion}.{PenumbraService.RequiredPenumbraFeatureVersion}, where the major version has to match exactly, and the minor version has to be greater or equal.\nYou may need to update Penumbra or enable Testing Builds for it for this version of Glamourer.\n\nPenumbra is required for Glamourer to work properly."); + } + else + { + if (TabBar.Draw("##tabs", ImGuiTabBarFlags.None, ToLabel(SelectTab), out var currentTab, () => { }, _tabs)) + SelectTab = TabType.None; + var tab = FromLabel(currentTab); + + if (tab != _config.Ephemeral.SelectedTab) + { + _config.Ephemeral.SelectedTab = FromLabel(currentTab); + _config.Ephemeral.Save(); + } + + if (_config.ShowQuickBarInTabs) + _quickBar.DrawAtEnd(yPos); + } } private ReadOnlySpan ToLabel(TabType type) @@ -106,6 +177,7 @@ public class MainWindow : Window, IDisposable TabType.Automation => Automation.Label, TabType.Unlocks => Unlocks.Label, TabType.Messages => Messages.Label, + TabType.Npcs => Npcs.Label, _ => ReadOnlySpan.Empty, }; @@ -117,37 +189,88 @@ public class MainWindow : Window, IDisposable if (label == Settings.Label) return TabType.Settings; if (label == Automation.Label) return TabType.Automation; if (label == Unlocks.Label) return TabType.Unlocks; + if (label == Npcs.Label) return TabType.Npcs; if (label == Messages.Label) return TabType.Messages; if (label == Debug.Label) return TabType.Debug; // @formatter:on return TabType.None; } - /// Draw the support button group on the right-hand side of the window. - public static void DrawSupportButtons(Changelog changelog) - { - var width = ImGui.CalcTextSize("Join Discord for Support").X + ImGui.GetStyle().FramePadding.X * 2; - var xPos = ImGui.GetWindowWidth() - width; - // Respect the scroll bar width. - if (ImGui.GetScrollMaxY() > 0) - xPos -= ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().FramePadding.X; + /// The longest support button text. + public static ReadOnlySpan SupportInfoButtonText + => "Copy Support Info to Clipboard"u8; + /// Draw the support button group on the right-hand side of the window. + public static void DrawSupportButtons(Glamourer glamourer, Changelog changelog) + { + var width = ImUtf8.CalcTextSize(SupportInfoButtonText).X + ImGui.GetStyle().FramePadding.X * 2; + var xPos = ImGui.GetWindowWidth() - width; ImGui.SetCursorPos(new Vector2(xPos, 0)); CustomGui.DrawDiscordButton(Glamourer.Messager, width); ImGui.SetCursorPos(new Vector2(xPos, ImGui.GetFrameHeightWithSpacing())); - CustomGui.DrawGuideButton(Glamourer.Messager, width); + DrawSupportButton(glamourer); ImGui.SetCursorPos(new Vector2(xPos, 2 * ImGui.GetFrameHeightWithSpacing())); + CustomGui.DrawGuideButton(Glamourer.Messager, width); + + ImGui.SetCursorPos(new Vector2(xPos, 3 * ImGui.GetFrameHeightWithSpacing())); if (ImGui.Button("Show Changelogs", new Vector2(width, 0))) changelog.ForceOpen = true; } - private void OnTabSelected(TabType type, Design? _) - => SelectTab = type; + /// + /// Draw a button that copies the support info to clipboards. + /// + private static void DrawSupportButton(Glamourer glamourer) + { + if (!ImUtf8.Button(SupportInfoButtonText)) + return; - private static string GetLabel() - => Glamourer.Version.Length == 0 - ? "Glamourer###GlamourerMainWindow" - : $"Glamourer v{Glamourer.Version}###GlamourerMainWindow"; + var text = glamourer.GatherSupportInformation(); + ImGui.SetClipboardText(text); + Glamourer.Messager.NotificationMessage("Copied Support Info to Clipboard.", NotificationType.Success, false); + } + + private void OnTabSelected(TabType type, Design? _) + { + SelectTab = type; + IsOpen = true; + } + + private string GetLabel() + => (Glamourer.Version.Length == 0, _config.Ephemeral.IncognitoMode) switch + { + (true, true) => "Glamourer (Incognito Mode)###GlamourerMainWindow", + (true, false) => "Glamourer###GlamourerMainWindow", + (false, false) => $"Glamourer v{Glamourer.Version}###GlamourerMainWindow", + (false, true) => $"Glamourer v{Glamourer.Version} (Incognito Mode)###GlamourerMainWindow", + }; + + private void DrawProblemWindow(string text) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.SelectedRed); + ImGui.NewLine(); + ImGui.NewLine(); + ImGuiUtil.TextWrapped(text); + color.Pop(); + + ImGui.NewLine(); + if (ImUtf8.Button("Try Attaching Again"u8)) + _penumbra.Reattach(); + + var ignoreAllowed = _config.DeleteDesignModifier.IsActive(); + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Ignore Penumbra This Time"u8, + $"Some functionality, like automation or retaining state, will not work correctly without Penumbra.\n\nIgnore this at your own risk!{(ignoreAllowed ? string.Empty : $"\n\nHold {_config.DeleteDesignModifier} while clicking to enable this button.")}", + default, !ignoreAllowed)) + _ignorePenumbra = true; + + ImGui.NewLine(); + ImGui.NewLine(); + CustomGui.DrawDiscordButton(Glamourer.Messager, 0); + ImGui.SameLine(); + ImGui.NewLine(); + ImGui.NewLine(); + } } diff --git a/Glamourer/Gui/Materials/AdvancedDyePopup.cs b/Glamourer/Gui/Materials/AdvancedDyePopup.cs new file mode 100644 index 0000000..4499107 --- /dev/null +++ b/Glamourer/Gui/Materials/AdvancedDyePopup.cs @@ -0,0 +1,514 @@ +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Utility; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.Interop; +using Glamourer.Designs; +using Glamourer.Interop.Material; +using Glamourer.State; +using Dalamud.Bindings.ImGui; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Widgets; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Interop; +using Penumbra.String; +using Notification = OtterGui.Classes.Notification; + +namespace Glamourer.Gui.Materials; + +public sealed unsafe class AdvancedDyePopup( + Configuration config, + StateManager stateManager, + LiveColorTablePreviewer preview, + DirectXService directX) : IService +{ + private MaterialValueIndex? _drawIndex; + private ActorState _state = null!; + private Actor _actor; + private ColorRow.Mode _mode; + private byte _selectedMaterial = byte.MaxValue; + private bool _anyChanged; + private bool _forceFocus; + + private const int RowsPerPage = 16; + private int _rowOffset; + + private bool ShouldBeDrawn() + { + if (_drawIndex is not { Valid: true }) + return false; + + if (!_actor.IsCharacter || !_state.ModelData.IsHuman || !_actor.Model.IsHuman) + return false; + + return true; + } + + public void DrawButton(EquipSlot slot, uint color) + => DrawButton(MaterialValueIndex.FromSlot(slot), color); + + public void DrawButton(BonusItemFlag slot, uint color) + => DrawButton(MaterialValueIndex.FromSlot(slot), color); + + private void DrawButton(MaterialValueIndex index, uint color) + { + if (config.HideDesignPanel.HasFlag(DesignPanelFlag.AdvancedDyes)) + return; + + ImGui.SameLine(); + using var id = ImUtf8.PushId(index.SlotIndex | ((int)index.DrawObject << 8)); + var isOpen = index == _drawIndex; + + var (textColor, buttonColor) = isOpen + ? (ColorId.HeaderButtons.Value(), ImGui.GetColorU32(ImGuiCol.ButtonActive)) + : (color, 0u); + + using (ImRaii.PushColor(ImGuiCol.Border, textColor, isOpen)) + { + using var frame = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, isOpen); + if (ImUtf8.IconButton(FontAwesomeIcon.Palette, ""u8, default, false, textColor, buttonColor)) + { + _forceFocus = true; + _selectedMaterial = byte.MaxValue; + _drawIndex = isOpen ? null : index; + } + } + + ImUtf8.HoverTooltip("Open advanced dyes for this slot."u8); + } + + private (string Path, string GamePath) ResourceName(MaterialValueIndex index) + { + var materialHandle = + (MaterialResourceHandle*)_actor.Model.AsCharacterBase->MaterialsSpan[ + index.MaterialIndex + index.SlotIndex * MaterialService.MaterialsPerModel].Value; + var model = _actor.Model.AsCharacterBase->ModelsSpan[index.SlotIndex].Value; + var modelHandle = model == null ? null : model->ModelResourceHandle; + var path = materialHandle == null + ? string.Empty + : ByteString.FromSpanUnsafe(materialHandle->FileName.AsSpan(), true).ToString(); + var gamePath = modelHandle == null + ? string.Empty + : modelHandle->GetMaterialFileNameBySlot(index.MaterialIndex).ToString(); + return (path, gamePath); + } + + private void DrawTabBar(ReadOnlySpan> textures, ReadOnlySpan> materials, ref bool firstAvailable) + { + using var bar = ImUtf8.TabBar("tabs"u8); + if (!bar) + return; + + var table = new ColorTable.Table(); + var highLightColor = ColorId.AdvancedDyeActive.Value(); + for (byte i = 0; i < MaterialService.MaterialsPerModel; ++i) + { + var index = _drawIndex!.Value with { MaterialIndex = i }; + var available = index.TryGetTexture(textures, materials, out var texture, out _mode) + && directX.TryGetColorTable(*texture, out table); + + + if (index == preview.LastValueIndex with { RowIndex = 0 }) + table = preview.LastOriginalColorTable; + + using var disable = ImRaii.Disabled(!available); + var select = available && firstAvailable && _selectedMaterial == byte.MaxValue + ? ImGuiTabItemFlags.SetSelected + : ImGuiTabItemFlags.None; + + if (available) + firstAvailable = false; + + var hasAdvancedDyes = _state.Materials.CheckExistenceMaterial(index); + using var c = ImRaii.PushColor(ImGuiCol.Text, highLightColor, hasAdvancedDyes); + using var tab = _label.TabItem(i, select); + c.Pop(); + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + { + using var enabled = ImRaii.Enabled(); + var (path, gamePath) = ResourceName(index); + using var tt = ImUtf8.Tooltip(); + + if (gamePath.Length == 0 || path.Length == 0) + ImUtf8.Text("This material does not exist."u8); + else if (!available) + ImUtf8.Text($"This material does not have an associated color set.\n\n{gamePath}\n{path}"); + else + ImUtf8.Text($"{gamePath}\n{path}"); + + if (hasAdvancedDyes && !available) + { + ImUtf8.Text("\nRight-Click to remove ineffective advanced dyes."u8); + if (ImGui.IsMouseClicked(ImGuiMouseButton.Right)) + for (byte row = 0; row < ColorTable.NumRows; ++row) + stateManager.ResetMaterialValue(_state, index with { RowIndex = row }, ApplySettings.Game); + } + } + + if ((tab.Success || select is ImGuiTabItemFlags.SetSelected) && available) + { + _selectedMaterial = i; + DrawToggle(); + DrawTable(index, table); + } + } + } + + private void DrawToggle() + { + var buttonWidth = new Vector2(ImGui.GetContentRegionAvail().X / 2, 0); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var hoverColor = ImRaii.PushColor(ImGuiCol.ButtonHovered, ImGui.GetColorU32(ImGuiCol.TabHovered)); + + using (ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(_rowOffset == 0 ? ImGuiCol.TabActive : ImGuiCol.Tab))) + { + if (ToggleButton.ButtonEx("Row Pairs 1-8 ", buttonWidth, ImGuiButtonFlags.MouseButtonLeft, ImDrawFlags.RoundCornersLeft)) + _rowOffset = 0; + } + + ImGui.SameLine(0, 0); + + using (ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(_rowOffset == RowsPerPage ? ImGuiCol.TabActive : ImGuiCol.Tab))) + { + if (ToggleButton.ButtonEx("Row Pairs 9-16", buttonWidth, ImGuiButtonFlags.MouseButtonLeft, ImDrawFlags.RoundCornersRight)) + _rowOffset = RowsPerPage; + } + } + + private void DrawContent(ReadOnlySpan> textures, ReadOnlySpan> materials) + { + var firstAvailable = true; + DrawTabBar(textures, materials, ref firstAvailable); + + if (firstAvailable) + ImUtf8.Text("No Editable Materials available."u8); + } + + private void DrawWindow(ReadOnlySpan> textures, ReadOnlySpan> materials) + { + var flags = ImGuiWindowFlags.NoFocusOnAppearing + | ImGuiWindowFlags.NoCollapse + | ImGuiWindowFlags.NoDecoration + | ImGuiWindowFlags.NoResize + | ImGuiWindowFlags.NoDocking; + + // Set position to the right of the main window when attached + // The downwards offset is implicit through child position. + if (config.KeepAdvancedDyesAttached) + { + var position = ImGui.GetWindowPos(); + position.X += ImGui.GetWindowSize().X + ImGui.GetStyle().WindowPadding.X; + ImGui.SetNextWindowPos(position); + flags |= ImGuiWindowFlags.NoMove; + } + + var width = 7 * ImGui.GetFrameHeight() // Buttons + + 3 * ImGui.GetStyle().ItemSpacing.X // around text + + 7 * ImGui.GetStyle().ItemInnerSpacing.X + + 200 * ImGuiHelpers.GlobalScale // Drags + + 7 * UiBuilder.MonoFont.GetCharAdvance(' ') * ImGuiHelpers.GlobalScale // Row + + 2 * ImGui.GetStyle().WindowPadding.X; + var height = 19 * ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().WindowPadding.Y + 3 * ImGui.GetStyle().ItemSpacing.Y; + ImGui.SetNextWindowSize(new Vector2(width, height)); + + var window = ImGui.Begin("###Glamourer Advanced Dyes", flags); + if (ImGui.IsWindowAppearing() || _forceFocus) + { + ImGui.SetWindowFocus(); + _forceFocus = false; + } + + try + { + if (window) + DrawContent(textures, materials); + } + finally + { + ImGui.End(); + } + } + + public void Draw(Actor actor, ActorState state) + { + _actor = actor; + _state = state; + if (!ShouldBeDrawn()) + return; + + if (_drawIndex!.Value.TryGetTextures(actor, out var textures, out var materials)) + DrawWindow(textures, materials); + } + + private void DrawTable(MaterialValueIndex materialIndex, ColorTable.Table table) + { + if (!materialIndex.Valid) + return; + + using var disabled = ImRaii.Disabled(_state.IsLocked); + _anyChanged = false; + for (byte i = 0; i < RowsPerPage; ++i) + { + var actualI = (byte)(i + _rowOffset); + var index = materialIndex with { RowIndex = actualI }; + ref var row = ref table[actualI]; + DrawRow(ref row, index, table); + } + + ImGui.Separator(); + DrawAllRow(materialIndex, table); + } + + private static void CopyToClipboard(in ColorTable.Table table) + { + try + { + fixed (ColorTable.Table* ptr = &table) + { + var data = new ReadOnlySpan(ptr, sizeof(ColorTable.Table)); + var base64 = Convert.ToBase64String(data); + ImGui.SetClipboardText(base64); + } + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not copy color table to clipboard:\n{ex}"); + } + } + + private static bool ImportFromClipboard(out ColorTable.Table table) + { + try + { + var base64 = ImGui.GetClipboardText(); + if (base64.Length > 0) + { + var data = Convert.FromBase64String(base64); + if (sizeof(ColorTable.Table) <= data.Length) + { + table = new ColorTable.Table(); + fixed (ColorTable.Table* tPtr = &table) + { + fixed (byte* ptr = data) + { + new ReadOnlySpan(ptr, sizeof(ColorTable.Table)).CopyTo(new Span(tPtr, sizeof(ColorTable.Table))); + return true; + } + } + } + } + + if (ColorRowClipboard.IsTableSet) + { + table = ColorRowClipboard.Table; + return true; + } + } + catch (Exception ex) + { + Glamourer.Messager.AddMessage(new Notification(ex, "Could not paste color table from clipboard.", + "Could not paste color table from clipboard.", NotificationType.Error)); + } + + table = default; + return false; + } + + private void DrawAllRow(MaterialValueIndex materialIndex, in ColorTable.Table table) + { + using var id = ImRaii.PushId(100); + var buttonSize = new Vector2(ImGui.GetFrameHeight()); + ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight all affected colors on the character."u8, buttonSize); + if (ImGui.IsItemHovered()) + preview.OnHover(materialIndex with { RowIndex = byte.MaxValue }, _actor.Index, table); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImUtf8.Text("All Color Row Pairs (1-16)"u8); + } + + var spacing = ImGui.GetStyle().ItemInnerSpacing.X; + ImGui.SameLine(ImGui.GetWindowSize().X - 3 * buttonSize.X - 2 * spacing - ImGui.GetStyle().WindowPadding.X); + if (ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Export this table to your clipboard."u8, buttonSize)) + { + ColorRowClipboard.Table = table; + CopyToClipboard(table); + } + + ImGui.SameLine(0, spacing); + if (ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported table from your clipboard onto this table."u8, buttonSize) + && ImportFromClipboard(out var newTable)) + for (var idx = 0; idx < ColorTable.NumRows; ++idx) + { + var row = newTable[idx]; + var internalRow = new ColorRow(row); + var slot = materialIndex.ToEquipSlot(); + var weapon = slot is EquipSlot.MainHand or EquipSlot.OffHand + ? _state.ModelData.Weapon(slot) + : _state.ModelData.Armor(slot).ToWeapon(0); + var value = new MaterialValueState(internalRow, internalRow, weapon, StateSource.Manual); + stateManager.ChangeMaterialValue(_state, materialIndex with { RowIndex = (byte)idx }, value, ApplySettings.Manual); + } + + ImGui.SameLine(0, spacing); + if (ImUtf8.IconButton(FontAwesomeIcon.UndoAlt, "Reset this table to game state."u8, buttonSize, !_anyChanged)) + for (byte i = 0; i < ColorTable.NumRows; ++i) + stateManager.ResetMaterialValue(_state, materialIndex with { RowIndex = i }, ApplySettings.Game); + } + + private void DrawRow(ref ColorTableRow row, MaterialValueIndex index, in ColorTable.Table table) + { + using var id = ImUtf8.PushId(index.RowIndex); + var changed = _state.Materials.TryGetValue(index, out var value); + if (!changed) + { + var internalRow = new ColorRow(row); + var slot = index.ToEquipSlot(); + var weapon = slot switch + { + EquipSlot.MainHand => _state.ModelData.Weapon(EquipSlot.MainHand), + EquipSlot.OffHand => _state.ModelData.Weapon(EquipSlot.OffHand), + EquipSlot.Unknown => + _state.ModelData.BonusItem((index.SlotIndex - 16u).ToBonusSlot()).Armor().ToWeapon(0), // TODO: Handle better + _ => _state.ModelData.Armor(slot).ToWeapon(0), + }; + value = new MaterialValueState(internalRow, internalRow, weapon, StateSource.Manual); + } + else + { + _anyChanged = true; + value = new MaterialValueState(value.Game, value.Model, value.DrawData, StateSource.Manual); + } + + var buttonSize = new Vector2(ImGui.GetFrameHeight()); + ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight the affected colors on the character."u8, buttonSize); + if (ImGui.IsItemHovered()) + preview.OnHover(index, _actor.Index, table); + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + var rowIndex = index.RowIndex / 2 + 1; + var rowSuffix = (index.RowIndex & 1) == 0 ? 'A' : 'B'; + ImUtf8.Text($"Row {rowIndex,2}{rowSuffix}"); + } + + ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X * 2); + var applied = ImUtf8.ColorPicker("##diffuse"u8, "Change the diffuse value for this row."u8, value.Model.Diffuse, + v => value.Model.Diffuse = v, "D"u8); + + var spacing = ImGui.GetStyle().ItemInnerSpacing; + ImGui.SameLine(0, spacing.X); + applied |= ImUtf8.ColorPicker("##specular"u8, "Change the specular value for this row."u8, value.Model.Specular, + v => value.Model.Specular = v, "S"u8); + + ImGui.SameLine(0, spacing.X); + applied |= ImUtf8.ColorPicker("##emissive"u8, "Change the emissive value for this row."u8, value.Model.Emissive, + v => value.Model.Emissive = v, "E"u8); + + ImGui.SameLine(0, spacing.X); + if (_mode is not ColorRow.Mode.Dawntrail) + { + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + applied |= DragGloss(ref value.Model.GlossStrength); + ImUtf8.HoverTooltip("Change the gloss strength for this row."u8); + } + else + { + ImGui.Dummy(new Vector2(100 * ImGuiHelpers.GlobalScale, 0)); + } + + ImGui.SameLine(0, spacing.X); + if (_mode is not ColorRow.Mode.Dawntrail) + { + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + applied |= DragSpecularStrength(ref value.Model.SpecularStrength); + ImUtf8.HoverTooltip("Change the specular strength for this row."u8); + } + else + { + ImGui.Dummy(new Vector2(100 * ImGuiHelpers.GlobalScale, 0)); + } + + ImGui.SameLine(0, spacing.X); + if (ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Export this row to your clipboard."u8, buttonSize)) + ColorRowClipboard.Row = value.Model; + ImGui.SameLine(0, spacing.X); + if (ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported row from your clipboard onto this row."u8, buttonSize, + !ColorRowClipboard.IsSet)) + { + value.Model = ColorRowClipboard.Row; + applied = true; + } + + ImGui.SameLine(0, spacing.X); + if (ImUtf8.IconButton(FontAwesomeIcon.UndoAlt, "Reset this row to game state."u8, buttonSize, !changed)) + stateManager.ResetMaterialValue(_state, index, ApplySettings.Game); + + if (applied) + stateManager.ChangeMaterialValue(_state, index, value, ApplySettings.Manual); + } + + public static bool DragGloss(ref float value) + { + var tmp = value; + var minValue = ImGui.GetIO().KeyCtrl ? 0f : (float)Half.Epsilon; + if (!ImUtf8.DragScalar("##Gloss"u8, ref tmp, "%.1f G"u8, 0.001f, minValue, Math.Max(0.01f, 0.005f * value), + ImGuiSliderFlags.AlwaysClamp)) + return false; + + var tmp2 = Math.Clamp(tmp, minValue, (float)Half.MaxValue); + if (tmp2 == value) + return false; + + value = tmp2; + return true; + } + + public static bool DragSpecularStrength(ref float value) + { + var tmp = value * 100f; + if (!ImUtf8.DragScalar("##SpecularStrength"u8, ref tmp, "%.0f%% SS"u8, 0f, (float)Half.MaxValue * 100f, 0.05f, + ImGuiSliderFlags.AlwaysClamp)) + return false; + + var tmp2 = Math.Clamp(tmp, 0f, (float)Half.MaxValue * 100f) / 100f; + if (tmp2 == value) + return false; + + value = tmp2; + return true; + } + + private LabelStruct _label = new(); + + private struct LabelStruct + { + private fixed byte _label[5]; + + public ImRaii.IEndObject TabItem(byte materialIndex, ImGuiTabItemFlags flags) + { + _label[4] = (byte)('A' + materialIndex); + fixed (byte* ptr = _label) + { + return ImRaii.TabItem(ptr, flags | ImGuiTabItemFlags.NoTooltip); + } + } + + public LabelStruct() + { + _label[0] = (byte)'M'; + _label[1] = (byte)'a'; + _label[2] = (byte)'t'; + _label[3] = (byte)' '; + _label[5] = 0; + } + } +} diff --git a/Glamourer/Gui/Materials/ColorRowClipboard.cs b/Glamourer/Gui/Materials/ColorRowClipboard.cs new file mode 100644 index 0000000..f7fac1d --- /dev/null +++ b/Glamourer/Gui/Materials/ColorRowClipboard.cs @@ -0,0 +1,34 @@ +using Glamourer.Interop.Material; +using Penumbra.GameData.Files.MaterialStructs; + +namespace Glamourer.Gui.Materials; + +public static class ColorRowClipboard +{ + private static ColorRow _row; + private static ColorTable.Table _table; + + public static bool IsSet { get; private set; } + + public static bool IsTableSet { get; private set; } + + public static ColorTable.Table Table + { + get => _table; + set + { + IsTableSet = true; + _table = value; + } + } + + public static ColorRow Row + { + get => _row; + set + { + IsSet = true; + _row = value; + } + } +} diff --git a/Glamourer/Gui/Materials/MaterialDrawer.cs b/Glamourer/Gui/Materials/MaterialDrawer.cs new file mode 100644 index 0000000..7c16372 --- /dev/null +++ b/Glamourer/Gui/Materials/MaterialDrawer.cs @@ -0,0 +1,275 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Glamourer.Designs; +using Glamourer.Interop.Material; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Gui; + +namespace Glamourer.Gui.Materials; + +public class MaterialDrawer(DesignManager _designManager, Configuration _config) : IService +{ + public const float GlossWidth = 100; + public const float SpecularStrengthWidth = 125; + + private int _newMaterialIdx; + private int _newRowIdx; + private MaterialValueIndex _newKey = MaterialValueIndex.FromSlot(EquipSlot.Head); + + private Vector2 _buttonSize; + private float _spacing; + + public void Draw(Design design) + { + var available = ImGui.GetContentRegionAvail().X; + _spacing = ImGui.GetStyle().ItemInnerSpacing.X; + _buttonSize = new Vector2(ImGui.GetFrameHeight()); + var colorWidth = 4 * _buttonSize.X + + (GlossWidth + SpecularStrengthWidth) * ImGuiHelpers.GlobalScale + + 6 * _spacing + + ImUtf8.CalcTextSize("Revert"u8).X; + DrawMultiButtons(design); + ImUtf8.Dummy(0); + ImGui.Separator(); + ImUtf8.Dummy(0); + if (available > 1.95 * colorWidth) + DrawSingleRow(design); + else + DrawTwoRow(design); + DrawNew(design); + } + + private void DrawMultiButtons(Design design) + { + var any = design.Materials.Count > 0; + var disabled = !_config.DeleteDesignModifier.IsActive(); + var size = new Vector2(200 * ImUtf8.GlobalScale, 0); + if (ImUtf8.ButtonEx("Enable All Advanced Dyes"u8, + any + ? "Enable the application of all contained advanced dyes without deleting them."u8 + : "This design does not contain any advanced dyes."u8, size, + !any || disabled)) + _designManager.ChangeApplyMulti(design, null, null, null, null, null, null, true, null); + ; + if (disabled && any) + ImUtf8.HoverTooltip($"Hold {_config.DeleteDesignModifier} while clicking to enable."); + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Disable All Advanced Dyes"u8, + any + ? "Disable the application of all contained advanced dyes without deleting them."u8 + : "This design does not contain any advanced dyes."u8, size, + !any || disabled)) + _designManager.ChangeApplyMulti(design, null, null, null, null, null, null, false, null); + if (disabled && any) + ImUtf8.HoverTooltip($"Hold {_config.DeleteDesignModifier} while clicking to disable."); + + if (ImUtf8.ButtonEx("Delete All Advanced Dyes"u8, any ? ""u8 : "This design does not contain any advanced dyes."u8, size, + !any || disabled)) + while (design.Materials.Count > 0) + _designManager.ChangeMaterialValue(design, MaterialValueIndex.FromKey(design.Materials[0].Item1), null); + + if (disabled && any) + ImUtf8.HoverTooltip($"Hold {_config.DeleteDesignModifier} while clicking to delete."); + } + + private void DrawName(MaterialValueIndex index) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0.05f, 0.5f)); + ImUtf8.TextFramed(index.ToString(), 0, new Vector2((GlossWidth + SpecularStrengthWidth) * ImGuiHelpers.GlobalScale + _spacing, 0), + borderColor: ImGui.GetColorU32(ImGuiCol.Text)); + } + + private void DrawSingleRow(Design design) + { + for (var i = 0; i < design.Materials.Count; ++i) + { + using var id = ImRaii.PushId(i); + var (idx, value) = design.Materials[i]; + var key = MaterialValueIndex.FromKey(idx); + + DrawName(key); + ImGui.SameLine(0, _spacing); + DeleteButton(design, key, ref i); + ImGui.SameLine(0, _spacing); + CopyButton(value.Value); + ImGui.SameLine(0, _spacing); + PasteButton(design, key); + ImGui.SameLine(0, _spacing); + using var disabled = ImRaii.Disabled(design.WriteProtected()); + EnabledToggle(design, key, value.Enabled); + ImGui.SameLine(0, _spacing); + DrawRow(design, key, value.Value, value.Revert); + ImGui.SameLine(0, _spacing); + RevertToggle(design, key, value.Revert); + } + } + + private void DrawTwoRow(Design design) + { + for (var i = 0; i < design.Materials.Count; ++i) + { + using var id = ImRaii.PushId(i); + var (idx, value) = design.Materials[i]; + var key = MaterialValueIndex.FromKey(idx); + + DrawName(key); + ImGui.SameLine(0, _spacing); + DeleteButton(design, key, ref i); + ImGui.SameLine(0, _spacing); + CopyButton(value.Value); + ImGui.SameLine(0, _spacing); + PasteButton(design, key); + ImGui.SameLine(0, _spacing); + EnabledToggle(design, key, value.Enabled); + + + DrawRow(design, key, value.Value, value.Revert); + ImGui.SameLine(0, _spacing); + RevertToggle(design, key, value.Revert); + ImGui.Separator(); + } + } + + private void DeleteButton(Design design, MaterialValueIndex index, ref int idx) + { + var deleteEnabled = _config.DeleteDesignModifier.IsActive(); + if (!ImUtf8.IconButton(FontAwesomeIcon.Trash, + $"Delete this color row.{(deleteEnabled ? string.Empty : $"\nHold {_config.DeleteDesignModifier} to delete.")}", disabled: + !deleteEnabled || design.WriteProtected())) + return; + + _designManager.ChangeMaterialValue(design, index, null); + --idx; + } + + private void CopyButton(in ColorRow row) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Export this row to your clipboard."u8)) + ColorRowClipboard.Row = row; + } + + private void PasteButton(Design design, MaterialValueIndex index) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported row from your clipboard onto this row."u8, + disabled: !ColorRowClipboard.IsSet || design.WriteProtected())) + _designManager.ChangeMaterialValue(design, index, ColorRowClipboard.Row); + } + + private void EnabledToggle(Design design, MaterialValueIndex index, bool enabled) + { + if (ImUtf8.Checkbox("Enabled"u8, ref enabled)) + _designManager.ChangeApplyMaterialValue(design, index, enabled); + } + + private void RevertToggle(Design design, MaterialValueIndex index, bool revert) + { + if (ImUtf8.Checkbox("Revert"u8, ref revert)) + _designManager.ChangeMaterialRevert(design, index, revert); + ImUtf8.HoverTooltip( + "If this is checked, Glamourer will try to revert the advanced dye row to its game state instead of applying a specific row."u8); + } + + public sealed class MaterialSlotCombo; + + private void DrawSlotCombo() + { + var width = ImUtf8.CalcTextSize(EquipSlot.OffHand.ToName()).X + ImGui.GetFrameHeightWithSpacing(); + ImGui.SetNextItemWidth(width); + using var combo = ImUtf8.Combo("##slot"u8, _newKey.SlotName()); + if (combo) + { + var currentSlot = _newKey.ToEquipSlot(); + foreach (var tmpSlot in EquipSlotExtensions.FullSlots) + { + if (ImUtf8.Selectable(tmpSlot.ToName(), tmpSlot == currentSlot) && currentSlot != tmpSlot) + _newKey = MaterialValueIndex.FromSlot(tmpSlot) with + { + MaterialIndex = (byte)_newMaterialIdx, + RowIndex = (byte)_newRowIdx, + }; + } + + var currentBonus = _newKey.ToBonusSlot(); + foreach (var bonusSlot in BonusExtensions.AllFlags) + { + if (ImUtf8.Selectable(bonusSlot.ToName(), bonusSlot == currentBonus) && bonusSlot != currentBonus) + _newKey = MaterialValueIndex.FromSlot(bonusSlot) with + { + MaterialIndex = (byte)_newMaterialIdx, + RowIndex = (byte)_newRowIdx, + }; + } + } + + ImUtf8.HoverTooltip("Choose a slot for an advanced dye row."u8); + } + + public void DrawNew(Design design) + { + DrawSlotCombo(); + ImUtf8.SameLineInner(); + DrawMaterialIdxDrag(); + ImUtf8.SameLineInner(); + DrawRowIdxDrag(); + ImUtf8.SameLineInner(); + var exists = design.GetMaterialDataRef().TryGetValue(_newKey, out _); + if (ImUtf8.ButtonEx("Add New Row"u8, + exists ? "The selected advanced dye row already exists."u8 : "Add the selected advanced dye row."u8, Vector2.Zero, + exists || design.WriteProtected())) + _designManager.ChangeMaterialValue(design, _newKey, ColorRow.Empty); + } + + private void DrawMaterialIdxDrag() + { + ImGui.SetNextItemWidth(ImUtf8.CalcTextSize("Material AA"u8).X); + var format = $"Material {(char)('A' + _newMaterialIdx)}"; + if (ImUtf8.DragScalar("##Material"u8, ref _newMaterialIdx, format, 0, MaterialService.MaterialsPerModel - 1, 0.01f, + ImGuiSliderFlags.NoInput)) + { + _newMaterialIdx = Math.Clamp(_newMaterialIdx, 0, MaterialService.MaterialsPerModel - 1); + _newKey = _newKey with { MaterialIndex = (byte)_newMaterialIdx }; + } + + ImUtf8.HoverTooltip("Drag this to the left or right to change its value."u8); + } + + private void DrawRowIdxDrag() + { + ImGui.SetNextItemWidth(ImUtf8.CalcTextSize("Row 0000"u8).X); + var format = $"Row {_newRowIdx / 2 + 1}{(char)(_newRowIdx % 2 + 'A')}"; + if (ImUtf8.DragScalar("##Row"u8, ref _newRowIdx, format, 0, ColorTable.NumRows - 1, 0.01f, ImGuiSliderFlags.NoInput)) + { + _newRowIdx = Math.Clamp(_newRowIdx, 0, ColorTable.NumRows - 1); + _newKey = _newKey with { RowIndex = (byte)_newRowIdx }; + } + + ImUtf8.HoverTooltip("Drag this to the left or right to change its value."u8); + } + + private void DrawRow(Design design, MaterialValueIndex index, in ColorRow row, bool disabled) + { + var tmp = row; + using var _ = ImRaii.Disabled(disabled); + var applied = ImGuiUtil.ColorPicker("##diffuse", "Change the diffuse value for this row.", row.Diffuse, v => tmp.Diffuse = v, "D"); + ImUtf8.SameLineInner(); + applied |= ImGuiUtil.ColorPicker("##specular", "Change the specular value for this row.", row.Specular, v => tmp.Specular = v, "S"); + ImUtf8.SameLineInner(); + applied |= ImGuiUtil.ColorPicker("##emissive", "Change the emissive value for this row.", row.Emissive, v => tmp.Emissive = v, "E"); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(GlossWidth * ImGuiHelpers.GlobalScale); + applied |= AdvancedDyePopup.DragGloss(ref tmp.GlossStrength); + ImUtf8.HoverTooltip("Change the gloss strength for this row."u8); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(SpecularStrengthWidth * ImGuiHelpers.GlobalScale); + applied |= AdvancedDyePopup.DragSpecularStrength(ref tmp.SpecularStrength); + ImUtf8.HoverTooltip("Change the specular strength for this row."u8); + if (applied) + _designManager.ChangeMaterialValue(design, index, tmp); + } +} diff --git a/Glamourer/Gui/PenumbraChangedItemTooltip.cs b/Glamourer/Gui/PenumbraChangedItemTooltip.cs index ea57515..dff9a6e 100644 --- a/Glamourer/Gui/PenumbraChangedItemTooltip.cs +++ b/Glamourer/Gui/PenumbraChangedItemTooltip.cs @@ -1,43 +1,48 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; +using Glamourer.Designs; using Glamourer.Events; -using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.Services; using Glamourer.State; -using Glamourer.Structs; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using Penumbra.Api.Enums; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.Gui; -public class PenumbraChangedItemTooltip : IDisposable +public sealed class PenumbraChangedItemTooltip : IDisposable { - private readonly PenumbraService _penumbra; - private readonly StateManager _stateManager; - private readonly ItemManager _items; - private readonly ObjectManager _objects; + private readonly PenumbraService _penumbra; + private readonly StateManager _stateManager; + private readonly ItemManager _items; + private readonly ActorObjectManager _objects; + private readonly CustomizeService _customize; + private readonly GPoseService _gpose; - private readonly EquipItem[] _lastItems = new EquipItem[EquipFlagExtensions.NumEquipFlags / 2]; + private readonly EquipItem[] _lastItems = new EquipItem[EquipFlagExtensions.NumEquipFlags / 2 + BonusExtensions.AllFlags.Count]; - public IEnumerable> LastItems - => EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand).Zip(_lastItems) - .Select(p => new KeyValuePair(p.First, p.Second)); + public IEnumerable> LastItems + => EquipSlotExtensions.EqdpSlots.Cast().Append(EquipSlot.MainHand).Append(EquipSlot.OffHand) + .Concat(BonusExtensions.AllFlags.Cast()).Zip(_lastItems) + .Select(p => new KeyValuePair(p.First, p.Second)); - public DateTime LastTooltip { get; private set; } = DateTime.MinValue; - public DateTime LastClick { get; private set; } = DateTime.MinValue; + public ChangedItemType LastType { get; private set; } = ChangedItemType.None; + public uint LastId { get; private set; } + public DateTime LastTooltip { get; private set; } = DateTime.MinValue; + public DateTime LastClick { get; private set; } = DateTime.MinValue; - public PenumbraChangedItemTooltip(PenumbraService penumbra, StateManager stateManager, ItemManager items, ObjectManager objects) + public PenumbraChangedItemTooltip(PenumbraService penumbra, StateManager stateManager, ItemManager items, ActorObjectManager objects, + CustomizeService customize, GPoseService gpose) { _penumbra = penumbra; _stateManager = stateManager; _items = items; _objects = objects; + _customize = customize; + _gpose = gpose; _penumbra.Tooltip += OnPenumbraTooltip; _penumbra.Click += OnPenumbraClick; } @@ -68,6 +73,21 @@ public class PenumbraChangedItemTooltip : IDisposable if (!Player()) return; + var bonusSlot = item.Type.ToBonus(); + if (bonusSlot is not BonusItemFlag.Unknown) + { + // + 2 due to weapons. + var glasses = _lastItems[bonusSlot.ToSlot() + 2]; + using (_ = !openTooltip ? null : ImRaii.Tooltip()) + { + ImGui.TextUnformatted($"{prefix}Right-Click to apply to current actor."); + if (glasses.Valid) + ImGui.TextUnformatted($"{prefix}Control + Right-Click to re-apply {glasses.Name} to current actor."); + } + + return; + } + var slot = item.Type.ToSlot(); var last = _lastItems[slot.ToIndex()]; switch (slot) @@ -76,7 +96,7 @@ public class PenumbraChangedItemTooltip : IDisposable case EquipSlot.OffHand when !CanApplyWeapon(EquipSlot.OffHand, item): break; case EquipSlot.RFinger: - using (var tt = !openTooltip ? null : ImRaii.Tooltip()) + using (_ = !openTooltip ? null : ImRaii.Tooltip()) { ImGui.TextUnformatted($"{prefix}Right-Click to apply to current actor (Right Finger)."); ImGui.TextUnformatted($"{prefix}Shift + Right-Click to apply to current actor (Left Finger)."); @@ -92,7 +112,7 @@ public class PenumbraChangedItemTooltip : IDisposable break; default: - using (var tt = !openTooltip ? null : ImRaii.Tooltip()) + using (_ = !openTooltip ? null : ImRaii.Tooltip()) { ImGui.TextUnformatted($"{prefix}Right-Click to apply to current actor."); if (last.Valid) @@ -105,6 +125,27 @@ public class PenumbraChangedItemTooltip : IDisposable public void ApplyItem(ActorState state, EquipItem item) { + var bonusSlot = item.Type.ToBonus(); + if (bonusSlot is not BonusItemFlag.Unknown) + { + // + 2 due to weapons. + var glasses = _lastItems[bonusSlot.ToSlot() + 2]; + if (ImGui.GetIO().KeyCtrl && glasses.Valid) + { + Glamourer.Log.Debug($"Re-Applying {glasses.Name} to {bonusSlot.ToName()}."); + SetLastItem(bonusSlot, default, state); + _stateManager.ChangeBonusItem(state, bonusSlot, glasses, ApplySettings.Manual); + } + else + { + Glamourer.Log.Debug($"Applying {item.Name} to {bonusSlot.ToName()}."); + SetLastItem(bonusSlot, item, state); + _stateManager.ChangeBonusItem(state, bonusSlot, item, ApplySettings.Manual); + } + + return; + } + var slot = item.Type.ToSlot(); var last = _lastItems[slot.ToIndex()]; switch (slot) @@ -116,24 +157,24 @@ public class PenumbraChangedItemTooltip : IDisposable switch (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) { case (false, false): - Glamourer.Log.Information($"Applying {item.Name} to Right Finger."); + Glamourer.Log.Debug($"Applying {item.Name} to Right Finger."); SetLastItem(EquipSlot.RFinger, item, state); - _stateManager.ChangeItem(state, EquipSlot.RFinger, item, StateChanged.Source.Manual); + _stateManager.ChangeItem(state, EquipSlot.RFinger, item, ApplySettings.Manual); break; case (false, true): - Glamourer.Log.Information($"Applying {item.Name} to Left Finger."); + Glamourer.Log.Debug($"Applying {item.Name} to Left Finger."); SetLastItem(EquipSlot.LFinger, item, state); - _stateManager.ChangeItem(state, EquipSlot.LFinger, item, StateChanged.Source.Manual); + _stateManager.ChangeItem(state, EquipSlot.LFinger, item, ApplySettings.Manual); break; case (true, false) when last.Valid: - Glamourer.Log.Information($"Re-Applying {last.Name} to Right Finger."); + Glamourer.Log.Debug($"Re-Applying {last.Name} to Right Finger."); SetLastItem(EquipSlot.RFinger, default, state); - _stateManager.ChangeItem(state, EquipSlot.RFinger, last, StateChanged.Source.Manual); + _stateManager.ChangeItem(state, EquipSlot.RFinger, last, ApplySettings.Manual); break; case (true, true) when _lastItems[EquipSlot.LFinger.ToIndex()].Valid: - Glamourer.Log.Information($"Re-Applying {last.Name} to Left Finger."); + Glamourer.Log.Debug($"Re-Applying {last.Name} to Left Finger."); SetLastItem(EquipSlot.LFinger, default, state); - _stateManager.ChangeItem(state, EquipSlot.LFinger, last, StateChanged.Source.Manual); + _stateManager.ChangeItem(state, EquipSlot.LFinger, last, ApplySettings.Manual); break; } @@ -141,15 +182,15 @@ public class PenumbraChangedItemTooltip : IDisposable default: if (ImGui.GetIO().KeyCtrl && last.Valid) { - Glamourer.Log.Information($"Re-Applying {last.Name} to {slot.ToName()}."); + Glamourer.Log.Debug($"Re-Applying {last.Name} to {slot.ToName()}."); SetLastItem(slot, default, state); - _stateManager.ChangeItem(state, slot, last, StateChanged.Source.Manual); + _stateManager.ChangeItem(state, slot, last, ApplySettings.Manual); } else { - Glamourer.Log.Information($"Applying {item.Name} to {slot.ToName()}."); + Glamourer.Log.Debug($"Applying {item.Name} to {slot.ToName()}."); SetLastItem(slot, item, state); - _stateManager.ChangeItem(state, slot, item, StateChanged.Source.Manual); + _stateManager.ChangeItem(state, slot, item, ApplySettings.Manual); } return; @@ -158,25 +199,53 @@ public class PenumbraChangedItemTooltip : IDisposable private void OnPenumbraTooltip(ChangedItemType type, uint id) { + LastType = type; + LastId = id; LastTooltip = DateTime.UtcNow; if (!Player()) return; switch (type) { + case ChangedItemType.ItemOffhand: case ChangedItemType.Item: - if (!_items.ItemService.AwaitedService.TryGetValue(id, EquipSlot.MainHand, out var item)) + { + if (!_items.ItemData.TryGetValue(id, type is ChangedItemType.Item ? EquipSlot.MainHand : EquipSlot.OffHand, out var item)) return; CreateTooltip(item, "[Glamourer] ", false); return; + } + case ChangedItemType.CustomArmor: + { + var (model, variant, slot) = IdentifiedItem.Split(id); + var item = _items.Identify(slot.ToSlot(), model, variant); + if (item.Valid) + CreateTooltip(item, "[Glamourer] ", false); + return; + } + case ChangedItemType.Customization: + { + var (race, gender, index, value) = IdentifiedCustomization.Split(id); + if (!_objects.Player.Model.IsHuman) + return; + + var customize = _objects.Player.Model.GetCustomize(); + if (CheckGenderRace(customize, race, gender) && VerifyValue(customize, index, value)) + ImGui.TextUnformatted("[Glamourer] Right-Click to apply to current actor."); + + return; + } } } private bool CanApplyWeapon(EquipSlot slot, EquipItem item) { + if (_gpose.InGPose && slot is EquipSlot.MainHand) + return true; + var main = _objects.Player.GetMainhand(); - var mainItem = _items.Identify(slot, main.Set, main.Type, main.Variant); + var mainItem = _items.Identify(EquipSlot.MainHand, main.Skeleton, main.Weapon, main.Variant); if (slot == EquipSlot.MainHand) return item.Type == mainItem.Type; @@ -186,20 +255,39 @@ public class PenumbraChangedItemTooltip : IDisposable private void OnPenumbraClick(MouseButton button, ChangedItemType type, uint id) { LastClick = DateTime.UtcNow; + if (button is not MouseButton.Right) + return; + + if (!Player(out var state)) + return; + switch (type) { case ChangedItemType.Item: - if (button is not MouseButton.Right) - return; - - if (!Player(out var state)) - return; - - if (!_items.ItemService.AwaitedService.TryGetValue(id, EquipSlot.MainHand, out var item)) + case ChangedItemType.ItemOffhand: + { + if (!_items.ItemData.TryGetValue(id, type is ChangedItemType.Item ? EquipSlot.MainHand : EquipSlot.OffHand, out var item)) return; ApplyItem(state, item); return; + } + case ChangedItemType.CustomArmor: + { + var (model, variant, slot) = IdentifiedItem.Split(id); + var item = _items.Identify(slot.ToSlot(), model, variant); + if (item.Valid) + ApplyItem(state, item); + return; + } + case ChangedItemType.Customization: + { + var (race, gender, index, value) = IdentifiedCustomization.Split(id); + var customize = state.ModelData.Customize; + if (CheckGenderRace(customize, race, gender) && VerifyValue(customize, index, value)) + _stateManager.ChangeCustomize(state, index, value, ApplySettings.Manual); + return; + } } } @@ -213,8 +301,40 @@ public class PenumbraChangedItemTooltip : IDisposable else { var oldItem = state.ModelData.Item(slot); - if (oldItem.ItemId != item.ItemId) - _lastItems[slot.ToIndex()] = oldItem; + if (oldItem.Id != item.Id) + last = oldItem; } } + + private void SetLastItem(BonusItemFlag slot, EquipItem item, ActorState state) + { + ref var last = ref _lastItems[slot.ToSlot() + 2]; + if (!item.Valid) + { + last = default; + } + else + { + var oldItem = state.ModelData.BonusItem(slot); + if (oldItem.Id != item.Id) + last = oldItem; + } + } + + private static bool CheckGenderRace(in CustomizeArray customize, ModelRace race, Gender gender) + { + if (race is ModelRace.Unknown && gender is Gender.Unknown) + return true; + if (gender != customize.Gender) + return false; + if (race.ToRace() != customize.Race) + return false; + if (race is ModelRace.Highlander && customize.Clan is not SubRace.Highlander) + return false; + + return true; + } + + private bool VerifyValue(in CustomizeArray customize, CustomizeIndex index, CustomizeValue value) + => _customize.IsCustomizationValid(customize.Clan, customize.Gender, customize.Face, index, value); } diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index b1fd1f8..224154b 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -1,43 +1,94 @@ -using System; -using System.Numerics; -using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Conditions; using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Glamourer.Automation; -using Glamourer.Customization; using Glamourer.Designs; -using Glamourer.Events; +using Glamourer.Designs.History; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; +using Glamourer.Gui.Materials; using Glamourer.Interop; -using Glamourer.Interop.Structs; -using Glamourer.Services; using Glamourer.State; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Text.HelperObjects; using Penumbra.GameData.Actors; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; namespace Glamourer.Gui.Tabs.ActorTab; public class ActorPanel { - private readonly ActorSelector _selector; - private readonly StateManager _stateManager; - private readonly CustomizationDrawer _customizationDrawer; - private readonly EquipmentDrawer _equipmentDrawer; - private readonly IdentifierService _identification; - private readonly AutoDesignApplier _autoDesignApplier; - private readonly Configuration _config; - private readonly DesignConverter _converter; - private readonly ObjectManager _objects; - private readonly DesignManager _designManager; - private readonly DatFileService _datFileService; - private readonly ICondition _conditions; + private readonly ActorSelector _selector; + private readonly StateManager _stateManager; + private readonly CustomizationDrawer _customizationDrawer; + private readonly EquipmentDrawer _equipmentDrawer; + private readonly AutoDesignApplier _autoDesignApplier; + private readonly Configuration _config; + private readonly DesignConverter _converter; + private readonly ActorObjectManager _objects; + private readonly DesignManager _designManager; + private readonly ImportService _importService; + private readonly ICondition _conditions; + private readonly DictModelChara _modelChara; + private readonly CustomizeParameterDrawer _parameterDrawer; + private readonly AdvancedDyePopup _advancedDyes; + private readonly EditorHistory _editorHistory; + private readonly HeaderDrawer.Button[] _leftButtons; + private readonly HeaderDrawer.Button[] _rightButtons; + + public ActorPanel(ActorSelector selector, + StateManager stateManager, + CustomizationDrawer customizationDrawer, + EquipmentDrawer equipmentDrawer, + AutoDesignApplier autoDesignApplier, + Configuration config, + DesignConverter converter, + ActorObjectManager objects, + DesignManager designManager, + ImportService importService, + ICondition conditions, + DictModelChara modelChara, + CustomizeParameterDrawer parameterDrawer, + AdvancedDyePopup advancedDyes, + EditorHistory editorHistory) + { + _selector = selector; + _stateManager = stateManager; + _customizationDrawer = customizationDrawer; + _equipmentDrawer = equipmentDrawer; + _autoDesignApplier = autoDesignApplier; + _config = config; + _converter = converter; + _objects = objects; + _designManager = designManager; + _importService = importService; + _conditions = conditions; + _modelChara = modelChara; + _parameterDrawer = parameterDrawer; + _advancedDyes = advancedDyes; + _editorHistory = editorHistory; + _leftButtons = + [ + new SetFromClipboardButton(this), + new ExportToClipboardButton(this), + new SaveAsDesignButton(this), + new UndoButton(this), + ]; + _rightButtons = + [ + new LockedButton(this), + new HeaderDrawer.IncognitoButton(_config), + ]; + } + private ActorIdentifier _identifier; private string _actorName = string.Empty; @@ -46,25 +97,6 @@ public class ActorPanel private ActorState? _state; private bool _lockedRedraw; - public ActorPanel(ActorSelector selector, StateManager stateManager, CustomizationDrawer customizationDrawer, - EquipmentDrawer equipmentDrawer, IdentifierService identification, AutoDesignApplier autoDesignApplier, - Configuration config, DesignConverter converter, ObjectManager objects, DesignManager designManager, DatFileService datFileService, - ICondition conditions) - { - _selector = selector; - _stateManager = stateManager; - _customizationDrawer = customizationDrawer; - _equipmentDrawer = equipmentDrawer; - _identification = identification; - _autoDesignApplier = autoDesignApplier; - _config = config; - _converter = converter; - _objects = objects; - _designManager = designManager; - _datFileService = datFileService; - _conditions = conditions; - } - private CustomizeFlag CustomizeApplicationFlags => _lockedRedraw ? CustomizeFlagExtensions.AllRelevant & ~CustomizeFlagExtensions.RedrawRequired : CustomizeFlagExtensions.AllRelevant; @@ -72,7 +104,7 @@ public class ActorPanel { using var group = ImRaii.Group(); (_identifier, _data) = _selector.Selection; - _lockedRedraw = _identifier.Type is IdentifierType.Special + _lockedRedraw = _identifier.Type is IdentifierType.Special || _objects.IsInLobby || _conditions[ConditionFlag.OccupiedInCutSceneEvent]; (_actorName, _actor) = GetHeaderName(); DrawHeader(); @@ -81,18 +113,28 @@ public class ActorPanel if (_state is not { IsLocked: false }) return; - if (_datFileService.CreateImGuiTarget(out var dat)) - _stateManager.ChangeCustomize(_state!, dat.Customize, CustomizeApplicationFlags, StateChanged.Source.Manual); - _datFileService.CreateSource(); + if (_importService.CreateDatTarget(out var dat)) + { + _stateManager.ChangeEntireCustomize(_state!, dat.Customize, CustomizeApplicationFlags, ApplySettings.Manual); + Glamourer.Messager.NotificationMessage($"Applied games .dat file {dat.Description} customizations to {_state.Identifier}.", + NotificationType.Success, false); + } + else if (_importService.CreateCharaTarget(out var designBase, out var name)) + { + _stateManager.ApplyDesign(_state!, designBase, ApplySettings.Manual); + Glamourer.Messager.NotificationMessage($"Applied Anamnesis .chara file {name} to {_state.Identifier}.", NotificationType.Success, + false); + } + + _importService.CreateDatSource(); + _importService.CreateCharaSource(); } private void DrawHeader() { var textColor = !_identifier.IsValid ? ImGui.GetColorU32(ImGuiCol.Text) : _data.Valid ? ColorId.ActorAvailable.Value() : ColorId.ActorUnavailable.Value(); - HeaderDrawer.Draw(_actorName, textColor, ImGui.GetColorU32(ImGuiCol.FrameBg), - 3, SetFromClipboardButton(), ExportToClipboardButton(), SaveAsDesignButton(), LockedButton(), - HeaderDrawer.Button.IncognitoButton(_selector.IncognitoMode, v => _selector.IncognitoMode = v)); + HeaderDrawer.Draw(_actorName, textColor, ImGui.GetColorU32(ImGuiCol.FrameBg), _leftButtons, _rightButtons); SaveDesignDrawPopup(); } @@ -110,47 +152,66 @@ public class ActorPanel private unsafe void DrawPanel() { - using var child = ImRaii.Child("##Panel", -Vector2.One, true); - if (!child || !_selector.HasSelection || !_stateManager.GetOrCreate(_identifier, _actor, out _state)) + using var table = ImUtf8.Table("##Panel", 1, ImGuiTableFlags.BordersOuter | ImGuiTableFlags.ScrollY, ImGui.GetContentRegionAvail()); + if (!table || !_selector.HasSelection || !_stateManager.GetOrCreate(_identifier, _actor, out _state)) return; + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableNextColumn(); + ImGui.Dummy(Vector2.Zero); + var transformationId = _actor.IsCharacter ? _actor.AsCharacter->CharacterData.TransformationId : 0; + if (transformationId != 0) + ImGuiUtil.DrawTextButton($"Currently transformed to Transformation {transformationId}.", + -Vector2.UnitX, Colors.SelectedRed); + DrawApplyToSelf(); ImGui.SameLine(); DrawApplyToTarget(); RevertButtons(); + ImGui.TableNextColumn(); + using var disabled = ImRaii.Disabled(transformationId != 0); if (_state.ModelData.IsHuman) DrawHumanPanel(); else DrawMonsterPanel(); + if (_data.Objects.Count > 0) + _advancedDyes.Draw(_data.Objects.Last(), _state); } private void DrawHumanPanel() { DrawCustomizationsHeader(); DrawEquipmentHeader(); + DrawParameterHeader(); + DrawDebugData(); } private void DrawCustomizationsHeader() { + if (_config.HideDesignPanel.HasFlag(DesignPanelFlag.Customization)) + return; + var header = _state!.ModelData.ModelId == 0 ? "Customization" : $"Customization (Model Id #{_state.ModelData.ModelId})###Customization"; - if (!ImGui.CollapsingHeader(header)) + var expand = _config.AutoExpandDesignPanel.HasFlag(DesignPanelFlag.Customization); + using var h = ImUtf8.CollapsingHeaderId(header, expand ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None); + if (!h) return; if (_customizationDrawer.Draw(_state!.ModelData.Customize, _state.IsLocked, _lockedRedraw)) - _stateManager.ChangeCustomize(_state, _customizationDrawer.Customize, _customizationDrawer.Changed, StateChanged.Source.Manual); + _stateManager.ChangeEntireCustomize(_state, _customizationDrawer.Customize, _customizationDrawer.Changed, ApplySettings.Manual); - if (_customizationDrawer.DrawWetnessState(_state!.ModelData.IsWet(), out var newWetness, _state.IsLocked)) - _stateManager.ChangeWetness(_state, newWetness, StateChanged.Source.Manual); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.Wetness, _stateManager, _state)); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); } private void DrawEquipmentHeader() { - if (!ImGui.CollapsingHeader("Equipment")) + using var h = DesignPanelFlag.Equipment.Header(_config); + if (!h) return; _equipmentDrawer.Prepare(); @@ -158,71 +219,106 @@ public class ActorPanel var usedAllStain = _equipmentDrawer.DrawAllStain(out var newAllStain, _state!.IsLocked); foreach (var slot in EquipSlotExtensions.EqdpSlots) { - var changes = _equipmentDrawer.DrawEquip(slot, _state!.ModelData, out var newArmor, out var newStain, null, out _, out _, - _state.IsLocked); + var data = EquipDrawData.FromState(_stateManager, _state!, slot); + _equipmentDrawer.DrawEquip(data); if (usedAllStain) - { - changes |= DataChange.Stain; - newStain = newAllStain; - } - - switch (changes) - { - case DataChange.Item: - _stateManager.ChangeItem(_state, slot, newArmor, StateChanged.Source.Manual); - break; - case DataChange.Stain: - _stateManager.ChangeStain(_state, slot, newStain, StateChanged.Source.Manual); - break; - case DataChange.Item | DataChange.Stain: - _stateManager.ChangeEquip(_state, slot, newArmor, newStain, StateChanged.Source.Manual); - break; - } + _stateManager.ChangeStains(_state, slot, newAllStain, ApplySettings.Manual); } - var weaponChanges = _equipmentDrawer.DrawWeapons(_state!.ModelData, out var newMainhand, out var newOffhand, out var newMainhandStain, - out var newOffhandStain, null, GameMain.IsInGPose(), out _, out _, out _, out _, _state.IsLocked); - if (usedAllStain) + var mainhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.MainHand); + var offhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.OffHand); + _equipmentDrawer.DrawWeapons(mainhand, offhand, GameMain.IsInGPose()); + + foreach (var slot in BonusExtensions.AllFlags) { - weaponChanges |= DataChange.Stain | DataChange.Stain2; - newMainhandStain = newAllStain; - newOffhandStain = newAllStain; + var data = BonusDrawData.FromState(_stateManager, _state!, slot); + _equipmentDrawer.DrawBonusItem(data); } - if (weaponChanges.HasFlag(DataChange.Item)) - if (weaponChanges.HasFlag(DataChange.Stain)) - _stateManager.ChangeEquip(_state, EquipSlot.MainHand, newMainhand, newMainhandStain, StateChanged.Source.Manual); - else - _stateManager.ChangeItem(_state, EquipSlot.MainHand, newMainhand, StateChanged.Source.Manual); - else if (weaponChanges.HasFlag(DataChange.Stain)) - _stateManager.ChangeStain(_state, EquipSlot.MainHand, newMainhandStain, StateChanged.Source.Manual); - - if (weaponChanges.HasFlag(DataChange.Item2)) - if (weaponChanges.HasFlag(DataChange.Stain2)) - _stateManager.ChangeEquip(_state, EquipSlot.OffHand, newOffhand, newOffhandStain, StateChanged.Source.Manual); - else - _stateManager.ChangeItem(_state, EquipSlot.OffHand, newOffhand, StateChanged.Source.Manual); - else if (weaponChanges.HasFlag(DataChange.Stain2)) - _stateManager.ChangeStain(_state, EquipSlot.OffHand, newOffhandStain, StateChanged.Source.Manual); - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (EquipmentDrawer.DrawHatState(_state!.ModelData.IsHatVisible(), out var newHatState, _state!.IsLocked)) - _stateManager.ChangeHatState(_state, newHatState, StateChanged.Source.Manual); - ImGui.SameLine(); - if (EquipmentDrawer.DrawVisorState(_state!.ModelData.IsVisorToggled(), out var newVisorState, _state!.IsLocked)) - _stateManager.ChangeVisorState(_state, newVisorState, StateChanged.Source.Manual); - ImGui.SameLine(); - if (EquipmentDrawer.DrawWeaponState(_state!.ModelData.IsWeaponVisible(), out var newWeaponState, _state!.IsLocked)) - _stateManager.ChangeWeaponState(_state, newWeaponState, StateChanged.Source.Manual); + DrawEquipmentMetaToggles(); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + _equipmentDrawer.DrawDragDropTooltip(); + } + + private void DrawParameterHeader() + { + using var h = DesignPanelFlag.AdvancedCustomizations.Header(_config); + if (!h) + return; + + _parameterDrawer.Draw(_stateManager, _state!); + } + + private unsafe void DrawDebugData() + { + if (!_config.DebugMode) + return; + + using var h = DesignPanelFlag.DebugData.Header(_config); + if (!h) + return; + + using var t = ImUtf8.Table("table"u8, 2, ImGuiTableFlags.SizingFixedFit); + if (!t) + return; + + ImUtf8.DrawTableColumn("Object Index"u8); + DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->ObjectIndex))}"); + ImUtf8.DrawTableColumn("Name ID"u8); + DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->GetNameId()))}"); + ImUtf8.DrawTableColumn("Base ID"u8); + DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->BaseId))}"); + ImUtf8.DrawTableColumn("Entity ID"u8); + DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->EntityId))}"); + ImUtf8.DrawTableColumn("Owner ID"u8); + DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->OwnerId))}"); + ImUtf8.DrawTableColumn("Game Object ID"u8); + DrawCopyColumn($"{string.Join(", ", _data.Objects.Select(d => d.AsObject->GetGameObjectId().ObjectId))}"); + + static void DrawCopyColumn(ref Utf8StringHandler text) + { + ImUtf8.DrawTableColumn(ref text); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImUtf8.SetClipboardText(TextStringHandlerBuffer.Span); + } + } + + private void DrawEquipmentMetaToggles() + { + using (_ = ImRaii.Group()) + { + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.HatState, _stateManager, _state!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.Head, _stateManager, _state!)); + } + + ImGui.SameLine(); + using (_ = ImRaii.Group()) + { + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.VisorState, _stateManager, _state!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.Body, _stateManager, _state!)); + } + + ImGui.SameLine(); + using (_ = ImRaii.Group()) + { + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.WeaponState, _stateManager, _state!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.OffHand, _stateManager, _state!)); + } + + ImGui.SameLine(); + using (_ = ImRaii.Group()) + { + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.EarState, _stateManager, _state!)); + } } private void DrawMonsterPanel() { - var names = _identification.AwaitedService.ModelCharaNames(_state!.ModelData.ModelId); + var names = _modelChara[_state!.ModelData.ModelId]; var turnHuman = ImGui.Button("Turn Human"); ImGui.Separator(); - using (var box = ImRaii.ListBox("##MonsterList", + using (_ = ImRaii.ListBox("##MonsterList", new Vector2(ImGui.GetContentRegionAvail().X, 10 * ImGui.GetTextLineHeightWithSpacing()))) { if (names.Count == 0) @@ -234,14 +330,14 @@ public class ActorPanel ImGui.Separator(); ImGui.TextUnformatted("Customization Data"); - using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + using (_ = ImRaii.PushFont(UiBuilder.MonoFont)) { - foreach (var b in _state.ModelData.Customize.Data) + foreach (var b in _state.ModelData.Customize) { - using (var g = ImRaii.Group()) + using (_ = ImRaii.Group()) { - ImGui.TextUnformatted($" {b:X2}"); - ImGui.TextUnformatted($"{b,3}"); + ImGui.TextUnformatted($" {b.Value:X2}"); + ImGui.TextUnformatted($"{b.Value,3}"); } ImGui.SameLine(); @@ -255,11 +351,11 @@ public class ActorPanel ImGui.Separator(); ImGui.TextUnformatted("Equipment Data"); - using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + using (_ = ImRaii.PushFont(UiBuilder.MonoFont)) { foreach (var b in _state.ModelData.GetEquipmentBytes()) { - using (var g = ImRaii.Group()) + using (_ = ImRaii.Group()) { ImGui.TextUnformatted($" {b:X2}"); ImGui.TextUnformatted($"{b,3}"); @@ -275,62 +371,11 @@ public class ActorPanel } if (turnHuman) - _stateManager.TurnHuman(_state, StateChanged.Source.Manual); + _stateManager.TurnHuman(_state, StateSource.Manual); } - private HeaderDrawer.Button SetFromClipboardButton() - => new() - { - Description = - "Try to apply a design from your clipboard.\nHold Control to only apply gear.\nHold Shift to only apply customizations.", - Icon = FontAwesomeIcon.Clipboard, - OnClick = SetFromClipboard, - Visible = _state != null, - Disabled = _state?.IsLocked ?? true, - }; - - private HeaderDrawer.Button ExportToClipboardButton() - => new() - { - Description = - "Copy the current design to your clipboard.\nHold Control to disable applying of customizations for the copied design.\nHold Shift to disable applying of gear for the copied design.", - Icon = FontAwesomeIcon.Copy, - OnClick = ExportToClipboard, - Visible = _state?.ModelData.ModelId == 0, - }; - - private HeaderDrawer.Button SaveAsDesignButton() - => new() - { - Description = - "Save the current state as a design.\nHold Control to disable applying of customizations for the saved design.\nHold Shift to disable applying of gear for the saved design.", - Icon = FontAwesomeIcon.Save, - OnClick = SaveDesignOpen, - Visible = _state?.ModelData.ModelId == 0, - }; - - private HeaderDrawer.Button LockedButton() - => new() - { - Description = "The current state of this actor is locked by external tools.", - Icon = FontAwesomeIcon.Lock, - OnClick = () => { }, - Disabled = true, - Visible = _state?.IsLocked ?? false, - TextColor = ColorId.ActorUnavailable.Value(), - BorderColor = ColorId.ActorUnavailable.Value(), - }; - - private string _newName = string.Empty; - private DesignBase? _newDesign = null; - - private void SaveDesignOpen() - { - ImGui.OpenPopup("Save as Design"); - _newName = _state!.Identifier.ToName(); - var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags(); - _newDesign = _converter.Convert(_state, applyGear, applyCustomize); - } + private string _newName = string.Empty; + private DesignBase? _newDesign; private void SaveDesignDrawPopup() { @@ -343,57 +388,36 @@ public class ActorPanel _newName = string.Empty; } - private void SetFromClipboard() - { - try - { - var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToBool(); - var text = ImGui.GetClipboardText(); - var design = _converter.FromBase64(text, applyCustomize, applyGear, out _) - ?? throw new Exception("The clipboard did not contain valid data."); - _stateManager.ApplyDesign(design, _state!, StateChanged.Source.Manual); - } - catch (Exception ex) - { - Glamourer.Messager.NotificationMessage(ex, $"Could not apply clipboard to {_identifier}.", - $"Could not apply clipboard to design {_identifier.Incognito(null)}", NotificationType.Error, false); - } - } - - private void ExportToClipboard() - { - try - { - var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags(); - var text = _converter.ShareBase64(_state!, applyGear, applyCustomize); - ImGui.SetClipboardText(text); - } - catch (Exception ex) - { - Glamourer.Messager.NotificationMessage(ex, $"Could not copy {_identifier} data to clipboard.", - $"Could not copy data from design {_identifier.Incognito(null)} to clipboard", NotificationType.Error); - } - } - private void RevertButtons() { if (ImGuiUtil.DrawDisabledButton("Revert to Game", Vector2.Zero, "Revert the character to its actual state in the game.", _state!.IsLocked)) - _stateManager.ResetState(_state!, StateChanged.Source.Manual); + _stateManager.ResetState(_state!, StateSource.Manual, isFinal: true); ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Reapply State", Vector2.Zero, "Try to reapply the configured state if something went wrong.", - _state!.IsLocked)) - _stateManager.ReapplyState(_actor); - ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Reapply Automation", Vector2.Zero, + "Reapply the current automation state for the character on top of its current state..", + !_config.EnableAutoDesigns || _state!.IsLocked)) + { + _autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, false, false, out var forcedRedraw); + _stateManager.ReapplyAutomationState(_actor, forcedRedraw, false, StateSource.Manual); + } + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Revert to Automation", Vector2.Zero, "Try to revert the character to the state it would have using automated designs.", !_config.EnableAutoDesigns || _state!.IsLocked)) { - _autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!); - _stateManager.ReapplyState(_actor); + _autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, true, false, out var forcedRedraw); + _stateManager.ReapplyAutomationState(_actor, forcedRedraw, true, StateSource.Manual); } + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Reapply", Vector2.Zero, + "Try to reapply the configured state if something went wrong. Should generally not be necessary.", + _state!.IsLocked)) + _stateManager.ReapplyState(_actor, false, StateSource.Manual, true); } private void DrawApplyToSelf() @@ -404,10 +428,9 @@ public class ActorPanel !data.Valid || id == _identifier || _state!.ModelData.ModelId != 0)) return; - var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags(); if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) - _stateManager.ApplyDesign(_converter.Convert(_state!, applyGear, applyCustomize), state, - StateChanged.Source.Manual); + _stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)), + ApplySettings.Manual with { IsFinal = true }); } private void DrawApplyToTarget() @@ -419,12 +442,132 @@ public class ActorPanel : "The current target can not be manipulated." : "No valid target selected."; if (!ImGuiUtil.DrawDisabledButton("Apply to Target", Vector2.Zero, tt, - !data.Valid || id == _identifier || _state!.ModelData.ModelId != 0 || _objects.IsInGPose)) + !data.Valid || id == _identifier || _state!.ModelData.ModelId != 0)) return; - var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags(); if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) - _stateManager.ApplyDesign(_converter.Convert(_state!, applyGear, applyCustomize), state, - StateChanged.Source.Manual); + _stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)), + ApplySettings.Manual with { IsFinal = true }); + } + + + private sealed class SetFromClipboardButton(ActorPanel panel) + : HeaderDrawer.Button + { + protected override string Description + => "Try to apply a design from your clipboard.\nHold Control to only apply gear.\nHold Shift to only apply customizations."; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.Clipboard; + + public override bool Visible + => panel._state != null; + + protected override bool Disabled + => panel._state?.IsLocked ?? true; + + protected override void OnClick() + { + try + { + var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToBool(); + var text = ImGui.GetClipboardText(); + var design = panel._converter.FromBase64(text, applyCustomize, applyGear, out _) + ?? throw new Exception("The clipboard did not contain valid data."); + panel._stateManager.ApplyDesign(panel._state!, design, ApplySettings.ManualWithLinks with { IsFinal = true }); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Could not apply clipboard to {panel._identifier}.", + $"Could not apply clipboard to design {panel._identifier.Incognito(null)}", NotificationType.Error, false); + } + } + } + + private sealed class ExportToClipboardButton(ActorPanel panel) : HeaderDrawer.Button + { + protected override string Description + => "Copy the current design to your clipboard.\nHold Control to disable applying of customizations for the copied design.\nHold Shift to disable applying of gear for the copied design."; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.Copy; + + public override bool Visible + => panel._state?.ModelData.ModelId == 0; + + protected override void OnClick() + { + try + { + var text = panel._converter.ShareBase64(panel._state!, ApplicationRules.FromModifiers(panel._state!)); + ImGui.SetClipboardText(text); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Could not copy {panel._identifier} data to clipboard.", + $"Could not copy data from design {panel._identifier.Incognito(null)} to clipboard", NotificationType.Error); + } + } + } + + private sealed class SaveAsDesignButton(ActorPanel panel) : HeaderDrawer.Button + { + protected override string Description + => "Save the current state as a design.\nHold Control to disable applying of customizations for the saved design.\nHold Shift to disable applying of gear for the saved design."; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.Save; + + public override bool Visible + => panel._state?.ModelData.ModelId == 0; + + protected override void OnClick() + { + ImGui.OpenPopup("Save as Design"); + panel._newName = panel._state!.Identifier.ToName(); + panel._newDesign = panel._converter.Convert(panel._state, ApplicationRules.FromModifiers(panel._state)); + } + } + + private sealed class UndoButton(ActorPanel panel) : HeaderDrawer.Button + { + protected override string Description + => "Undo the last change."; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.Undo; + + public override bool Visible + => panel._state != null; + + protected override bool Disabled + => (panel._state?.IsLocked ?? true) || !panel._editorHistory.CanUndo(panel._state); + + protected override void OnClick() + => panel._editorHistory.Undo(panel._state!); + } + + private sealed class LockedButton(ActorPanel panel) : HeaderDrawer.Button + { + protected override string Description + => "The current state of this actor is locked by external tools."; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.Lock; + + public override bool Visible + => panel._state?.IsLocked ?? false; + + protected override bool Disabled + => true; + + protected override uint BorderColor + => ColorId.ActorUnavailable.Value(); + + protected override uint TextColor + => ColorId.ActorUnavailable.Value(); + + protected override void OnClick() + { } } } diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs b/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs index 334b78a..7d132a1 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs @@ -1,49 +1,37 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using Dalamud.Interface; -using Glamourer.Interop; -using Glamourer.Interop.Structs; -using Glamourer.Services; -using ImGuiNET; +using Dalamud.Interface; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Tabs.ActorTab; -public class ActorSelector +public class ActorSelector(ActorObjectManager objects, ActorManager actors, EphemeralConfig config) { - private readonly Configuration _config; - private readonly ObjectManager _objects; - private readonly ActorService _actors; - private ActorIdentifier _identifier = ActorIdentifier.Invalid; - public ActorSelector(ObjectManager objects, ActorService actors, Configuration config) - { - _objects = objects; - _actors = actors; - _config = config; - } - public bool IncognitoMode { - get => _config.IncognitoMode; + get => config.IncognitoMode; set { - _config.IncognitoMode = value; - _config.Save(); + config.IncognitoMode = value; + config.Save(); } } private LowerString _actorFilter = LowerString.Empty; private Vector2 _defaultItemSpacing; + private WorldId _world; private float _width; public (ActorIdentifier Identifier, ActorData Data) Selection - => _objects.TryGetValue(_identifier, out var data) ? (_identifier, data) : (_identifier, ActorData.Invalid); + => objects.TryGetValue(_identifier, out var data) ? (_identifier, data) : (_identifier, ActorData.Invalid); public bool HasSelection => _identifier.IsValid; @@ -51,12 +39,43 @@ public class ActorSelector public void Draw(float width) { _width = width; - using var group = ImRaii.Group(); + using var group = ImUtf8.Group(); _defaultItemSpacing = ImGui.GetStyle().ItemSpacing; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) .Push(ImGuiStyleVar.FrameRounding, 0); ImGui.SetNextItemWidth(_width); LowerString.InputWithHint("##actorFilter", "Filter...", ref _actorFilter, 64); + if (ImGui.IsItemHovered()) + { + using var tt = ImUtf8.Tooltip(); + ImUtf8.Text("Filter for names containing the input."u8); + ImGui.Dummy(new Vector2(0, ImGui.GetTextLineHeight() / 2)); + ImUtf8.Text("Special filters are:"u8); + var color = ColorId.HeaderButtons.Value(); + ImUtf8.Text("

"u8, color); + ImGui.SameLine(0, 0); + ImUtf8.Text(": show only player characters."u8); + + ImUtf8.Text(""u8, color); + ImGui.SameLine(0, 0); + ImUtf8.Text(": show only owned game objects."u8); + + ImUtf8.Text(""u8, color); + ImGui.SameLine(0, 0); + ImUtf8.Text(": show only NPCs."u8); + + ImUtf8.Text(""u8, color); + ImGui.SameLine(0, 0); + ImUtf8.Text(": show only retainers."u8); + + ImUtf8.Text(""u8, color); + ImGui.SameLine(0, 0); + ImUtf8.Text(": show only special screen characters."u8); + + ImUtf8.Text(""u8, color); + ImGui.SameLine(0, 0); + ImUtf8.Text(": show only players from your world."u8); + } DrawSelector(); DrawSelectionButtons(); @@ -64,24 +83,35 @@ public class ActorSelector private void DrawSelector() { - using var child = ImRaii.Child("##Selector", new Vector2(_width, -ImGui.GetFrameHeight()), true); + using var child = ImUtf8.Child("##Selector"u8, new Vector2(_width, -ImGui.GetFrameHeight()), true); if (!child) return; - _objects.Update(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _defaultItemSpacing); - var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight()); - var remainder = ImGuiClip.FilteredClippedDraw(_objects, skips, CheckFilter, DrawSelectable); + _world = new WorldId(objects.Player.Valid ? objects.Player.HomeWorld : (ushort)0); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _defaultItemSpacing); + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight()); + var remainder = ImGuiClip.FilteredClippedDraw(objects.Where(p => p.Value.Objects.Any(a => a.Model)), skips, CheckFilter, + DrawSelectable); ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight()); } private bool CheckFilter(KeyValuePair pair) - => _actorFilter.IsEmpty || pair.Value.Label.Contains(_actorFilter.Lower, StringComparison.OrdinalIgnoreCase); + => _actorFilter.Lower switch + { + "" => true, + "

" => pair.Key.Type is IdentifierType.Player, + "" => pair.Key.Type is IdentifierType.Owned, + "" => pair.Key.Type is IdentifierType.Npc, + "" => pair.Key.Type is IdentifierType.Retainer, + "" => pair.Key.Type is IdentifierType.Special, + "" => pair.Key.Type is IdentifierType.Player && pair.Key.HomeWorld == _world, + _ => _actorFilter.IsContained(pair.Value.Label), + }; private void DrawSelectable(KeyValuePair pair) { var equals = pair.Key.Equals(_identifier); - if (ImGui.Selectable(IncognitoMode ? pair.Key.Incognito(pair.Value.Label) : pair.Value.Label, equals) && !equals) + if (ImUtf8.Selectable(IncognitoMode ? pair.Key.Incognito(pair.Value.Label) : pair.Value.Label, equals) && !equals) _identifier = pair.Key.CreatePermanent(); } @@ -91,15 +121,14 @@ public class ActorSelector .Push(ImGuiStyleVar.FrameRounding, 0); var buttonWidth = new Vector2(_width / 2, 0); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth - , "Select the local player character.", !_objects.Player, true)) - _identifier = _objects.Player.GetIdentifier(_actors.AwaitedService); + if (ImUtf8.IconButton(FontAwesomeIcon.UserCircle, "Select the local player character."u8, buttonWidth, !objects.Player)) + _identifier = objects.Player.GetIdentifier(actors); ImGui.SameLine(); - var (id, data) = _objects.TargetData; + var (id, data) = objects.TargetData; var tt = data.Valid ? $"Select the current target {id} in the list." : id.IsValid ? $"The target {id} is not in the list." : "No target selected."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth, tt, _objects.IsInGPose || !data.Valid, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.HandPointer, tt, buttonWidth, objects.IsInGPose || !data.Valid)) _identifier = id; } } diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorTab.cs b/Glamourer/Gui/Tabs/ActorTab/ActorTab.cs index 76b0a55..9751a71 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorTab.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorTab.cs @@ -1,28 +1,18 @@ -using System; -using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Interface.Utility; +using Dalamud.Bindings.ImGui; using OtterGui.Widgets; namespace Glamourer.Gui.Tabs.ActorTab; -public class ActorTab : ITab +public class ActorTab(ActorSelector selector, ActorPanel panel) : ITab { - private readonly ActorSelector _selector; - private readonly ActorPanel _panel; - public ReadOnlySpan Label => "Actors"u8; public void DrawContent() { - _selector.Draw(200 * ImGuiHelpers.GlobalScale); + selector.Draw(200 * ImGuiHelpers.GlobalScale); ImGui.SameLine(); - _panel.Draw(); - } - - public ActorTab(ActorSelector selector, ActorPanel panel) - { - _selector = selector; - _panel = panel; + panel.Draw(); } } diff --git a/Glamourer/Gui/Tabs/AutomationTab/AutomationTab.cs b/Glamourer/Gui/Tabs/AutomationTab/AutomationTab.cs index e5d4b21..da3b636 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/AutomationTab.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/AutomationTab.cs @@ -1,29 +1,22 @@ -using System; -using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Interface.Utility; +using Dalamud.Bindings.ImGui; using OtterGui.Widgets; namespace Glamourer.Gui.Tabs.AutomationTab; -public class AutomationTab : ITab +public class AutomationTab(SetSelector selector, SetPanel panel, Configuration config) : ITab { - private readonly SetSelector _selector; - private readonly SetPanel _panel; - - public AutomationTab(SetSelector selector, SetPanel panel) - { - _selector = selector; - _panel = panel; - } - public ReadOnlySpan Label => "Automation"u8; + public bool IsVisible + => config.EnableAutoDesigns; + public void DrawContent() { - _selector.Draw(GetSetSelectorSize()); + selector.Draw(GetSetSelectorSize()); ImGui.SameLine(); - _panel.Draw(); + panel.Draw(); } public float GetSetSelectorSize() diff --git a/Glamourer/Gui/Tabs/AutomationTab/DesignCombo.cs b/Glamourer/Gui/Tabs/AutomationTab/DesignCombo.cs deleted file mode 100644 index 998270b..0000000 --- a/Glamourer/Gui/Tabs/AutomationTab/DesignCombo.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Linq; -using Dalamud.Interface.Utility; -using Glamourer.Automation; -using Glamourer.Customization; -using Glamourer.Designs; -using Glamourer.Events; -using Glamourer.Services; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Log; -using OtterGui.Widgets; - -namespace Glamourer.Gui.Tabs.AutomationTab; - -public sealed class DesignCombo : FilterComboCache<(Design, string)> -{ - public const int RevertDesignIndex = -1228; - public readonly Design RevertDesign; - - private readonly AutoDesignManager _manager; - private readonly TabSelected _tabSelected; - private float _innerWidth; - - public DesignCombo(AutoDesignManager manager, DesignManager designs, DesignFileSystem fileSystem, TabSelected tabSelected, - ItemManager items, CustomizationService customize, Logger log) - : this(manager, designs, fileSystem, tabSelected, CreateRevertDesign(customize, items), log) - { } - - private DesignCombo(AutoDesignManager manager, DesignManager designs, DesignFileSystem fileSystem, TabSelected tabSelected, - Design revertDesign, Logger log) - : base(() => designs.Designs.Select(d => (d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty)).OrderBy(d => d.Item2) - .Prepend((revertDesign, string.Empty)).ToList(), log) - { - _manager = manager; - _tabSelected = tabSelected; - RevertDesign = revertDesign; - } - - protected override bool DrawSelectable(int globalIdx, bool selected) - { - var ret = base.DrawSelectable(globalIdx, selected); - var (design, path) = Items[globalIdx]; - if (path.Length > 0 && design.Name != path) - { - var start = ImGui.GetItemRectMin(); - var pos = start.X + ImGui.CalcTextSize(design.Name).X; - var maxSize = ImGui.GetWindowPos().X + ImGui.GetWindowContentRegionMax().X; - var remainingSpace = maxSize - pos; - var requiredSize = ImGui.CalcTextSize(path).X + ImGui.GetStyle().ItemInnerSpacing.X; - var offset = remainingSpace - requiredSize; - if (ImGui.GetScrollMaxY() == 0) - offset -= ImGui.GetStyle().ItemInnerSpacing.X; - - if (offset < ImGui.GetStyle().ItemSpacing.X) - ImGuiUtil.HoverTooltip(path); - else - ImGui.GetWindowDrawList().AddText(start with { X = pos + offset }, - ImGui.GetColorU32(ImGuiCol.TextDisabled), path); - } - - return ret; - } - - protected override float GetFilterWidth() - => _innerWidth - 2 * ImGui.GetStyle().FramePadding.X; - - public void Draw(AutoDesignSet set, AutoDesign? design, int autoDesignIndex, bool incognito) - { - _innerWidth = 400 * ImGuiHelpers.GlobalScale; - CurrentSelectionIdx = Math.Max(Items.IndexOf(p => design?.Design == p.Item1), 0); - CurrentSelection = Items[CurrentSelectionIdx]; - var name = design?.Name(incognito) ?? "Select Design Here..."; - if (Draw("##design", name, string.Empty, ImGui.GetContentRegionAvail().X, - ImGui.GetTextLineHeightWithSpacing()) - && CurrentSelection.Item1 != null) - { - if (autoDesignIndex >= 0) - _manager.ChangeDesign(set, autoDesignIndex, CurrentSelection.Item1 == RevertDesign ? null : CurrentSelection.Item1); - else - _manager.AddDesign(set, CurrentSelection.Item1 == RevertDesign ? null : CurrentSelection.Item1); - } - - if (design?.Design != null) - { - if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) - _tabSelected.Invoke(MainWindow.TabType.Designs, design.Design); - ImGuiUtil.HoverTooltip("Control + Right-Click to move to design."); - } - } - - protected override string ToString((Design, string) obj) - => obj.Item1.Name.Text; - - protected override bool IsVisible(int globalIndex, LowerString filter) - { - var (design, path) = Items[globalIndex]; - return filter.IsContained(path) || design.Name.Lower.Contains(filter.Lower); - } - - private static Design CreateRevertDesign(CustomizationService customize, ItemManager items) - => new(customize, items) - { - Index = RevertDesignIndex, - Name = AutoDesign.RevertName, - ApplyCustomize = CustomizeFlagExtensions.AllRelevant, - }; -} diff --git a/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs b/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs index da4fda1..1d3e711 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs @@ -1,25 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Utility; -using Glamourer.Services; -using ImGuiNET; -using OtterGui.Custom; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Extensions; using OtterGui.Log; using OtterGui.Widgets; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; +using OtterGui.Custom; namespace Glamourer.Gui.Tabs.AutomationTab; -public sealed class HumanNpcCombo : FilterComboCache<(string Name, ObjectKind Kind, uint[] Ids)> +public sealed class HumanNpcCombo( + string label, + DictModelChara modelCharaDict, + DictBNpcNames bNpcNames, + DictBNpc bNpcs, + HumanModelList humans, + Logger log) + : FilterComboCache<(string Name, ObjectKind Kind, uint[] Ids)>(() => CreateList(modelCharaDict, bNpcNames, bNpcs, humans), MouseWheelType.None, log) { - private readonly string _label; - - public HumanNpcCombo(string label, IdentifierService service, HumanModelList humans, Logger log) - : base(() => CreateList(service, humans), log) - => _label = label; - protected override string ToString((string Name, ObjectKind Kind, uint[] Ids) obj) => obj.Name; @@ -36,7 +35,8 @@ public sealed class HumanNpcCombo : FilterComboCache<(string Name, ObjectKind Ki } public bool Draw(float width) - => Draw(_label, CurrentSelection.Name.IsNullOrEmpty() ? "Human Non-Player-Characters..." : CurrentSelection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); + => Draw(label, CurrentSelection.Name.IsNullOrEmpty() ? "Human Non-Player-Characters..." : CurrentSelection.Name, string.Empty, width, + ImGui.GetTextLineHeightWithSpacing()); ///

Compare strings in a way that letters and numbers are sorted before any special symbols. @@ -61,15 +61,16 @@ public sealed class HumanNpcCombo : FilterComboCache<(string Name, ObjectKind Ki } } - private static IReadOnlyList<(string Name, ObjectKind Kind, uint[] Ids)> CreateList(IdentifierService service, HumanModelList humans) + private static IReadOnlyList<(string Name, ObjectKind Kind, uint[] Ids)> CreateList(DictModelChara modelCharaDict, DictBNpcNames bNpcNames, + DictBNpc bNpcs, HumanModelList humans) { var ret = new List<(string Name, ObjectKind Kind, uint Id)>(1024); - for (var modelChara = 0u; modelChara < service.AwaitedService.NumModelChara; ++modelChara) + for (var modelChara = 0u; modelChara < modelCharaDict.Count; ++modelChara) { if (!humans.IsHuman(modelChara)) continue; - var list = service.AwaitedService.ModelCharaNames(modelChara); + var list = modelCharaDict[modelChara]; if (list.Count == 0) continue; @@ -78,8 +79,10 @@ public sealed class HumanNpcCombo : FilterComboCache<(string Name, ObjectKind Ki switch (kind) { case ObjectKind.BattleNpc: - var nameIds = service.AwaitedService.GetBnpcNames(id); - ret.AddRange(nameIds.Select(nameId => (service.AwaitedService.Name(ObjectKind.BattleNpc, nameId), kind, nameId.Id))); + if (!bNpcNames.TryGetValue(id, out var nameIds)) + continue; + + ret.AddRange(nameIds.SelectWhere(nameId => (bNpcs.TryGetValue(nameId, out var s), (s!, kind, nameId.Id)))); break; case ObjectKind.EventNpc: ret.Add((name, kind, id)); diff --git a/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs b/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs index e88c55b..ba2e424 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs @@ -1,9 +1,9 @@ using Dalamud.Game.ClientState.Objects.Enums; -using Glamourer.Services; -using ImGuiNET; -using OtterGui.Custom; +using Dalamud.Bindings.ImGui; using Penumbra.GameData.Actors; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; +using Penumbra.GameData.Gui; +using Penumbra.GameData.Structs; using Penumbra.String; namespace Glamourer.Gui.Tabs.AutomationTab; @@ -12,7 +12,7 @@ public class IdentifierDrawer { private readonly WorldCombo _worldCombo; private readonly HumanNpcCombo _humanNpcCombo; - private readonly ActorService _actors; + private readonly ActorManager _actors; private string _characterName = string.Empty; @@ -20,12 +20,14 @@ public class IdentifierDrawer public ActorIdentifier PlayerIdentifier { get; private set; } = ActorIdentifier.Invalid; public ActorIdentifier RetainerIdentifier { get; private set; } = ActorIdentifier.Invalid; public ActorIdentifier MannequinIdentifier { get; private set; } = ActorIdentifier.Invalid; + public ActorIdentifier OwnedIdentifier { get; private set; } = ActorIdentifier.Invalid; - public IdentifierDrawer(ActorService actors, IdentifierService identifier, HumanModelList humans) + public IdentifierDrawer(ActorManager actors, DictWorld dictWorld, DictModelChara dictModelChara, DictBNpcNames bNpcNames, DictBNpc bNpc, + HumanModelList humans) { _actors = actors; - _worldCombo = new WorldCombo(actors.AwaitedService.Data.Worlds, Glamourer.Log); - _humanNpcCombo = new HumanNpcCombo("##npcs", identifier, humans, Glamourer.Log); + _worldCombo = new WorldCombo(dictWorld, Glamourer.Log); + _humanNpcCombo = new HumanNpcCombo("##npcs", dictModelChara, bNpcNames, bNpc, humans, Glamourer.Log); } public void DrawName(float width) @@ -59,17 +61,25 @@ public class IdentifierDrawer public bool CanSetNpc => NpcIdentifier.IsValid; + public bool CanSetOwned + => OwnedIdentifier.IsValid; + private void UpdateIdentifiers() { if (ByteString.FromString(_characterName, out var byteName)) { - PlayerIdentifier = _actors.AwaitedService.CreatePlayer(byteName, _worldCombo.CurrentSelection.Key); - RetainerIdentifier = _actors.AwaitedService.CreateRetainer(byteName, ActorIdentifier.RetainerType.Bell); - MannequinIdentifier = _actors.AwaitedService.CreateRetainer(byteName, ActorIdentifier.RetainerType.Mannequin); + PlayerIdentifier = _actors.CreatePlayer(byteName, _worldCombo.CurrentSelection.Key); + RetainerIdentifier = _actors.CreateRetainer(byteName, ActorIdentifier.RetainerType.Bell); + MannequinIdentifier = _actors.CreateRetainer(byteName, ActorIdentifier.RetainerType.Mannequin); + + if (_humanNpcCombo.CurrentSelection.Kind is ObjectKind.EventNpc or ObjectKind.BattleNpc) + OwnedIdentifier = _actors.CreateOwned(byteName, _worldCombo.CurrentSelection.Key, _humanNpcCombo.CurrentSelection.Kind, _humanNpcCombo.CurrentSelection.Ids[0]); + else + OwnedIdentifier = ActorIdentifier.Invalid; } NpcIdentifier = _humanNpcCombo.CurrentSelection.Kind is ObjectKind.EventNpc or ObjectKind.BattleNpc - ? _actors.AwaitedService.CreateNpc(_humanNpcCombo.CurrentSelection.Kind, _humanNpcCombo.CurrentSelection.Ids[0]) + ? _actors.CreateNpc(_humanNpcCombo.CurrentSelection.Kind, _humanNpcCombo.CurrentSelection.Ids[0]) : ActorIdentifier.Invalid; } } diff --git a/Glamourer/Gui/Tabs/AutomationTab/RandomRestrictionDrawer.cs b/Glamourer/Gui/Tabs/AutomationTab/RandomRestrictionDrawer.cs new file mode 100644 index 0000000..8eba59b --- /dev/null +++ b/Glamourer/Gui/Tabs/AutomationTab/RandomRestrictionDrawer.cs @@ -0,0 +1,438 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Glamourer.Automation; +using Glamourer.Designs; +using Glamourer.Designs.Special; +using Glamourer.Events; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; + +namespace Glamourer.Gui.Tabs.AutomationTab; + +public sealed class RandomRestrictionDrawer : IService, IDisposable +{ + private AutoDesignSet? _set; + private int _designIndex = -1; + + private readonly AutomationChanged _automationChanged; + private readonly Configuration _config; + private readonly AutoDesignManager _autoDesignManager; + private readonly RandomDesignCombo _randomDesignCombo; + private readonly SetSelector _selector; + private readonly DesignStorage _designs; + private readonly DesignFileSystem _designFileSystem; + + private string _newText = string.Empty; + private string? _newDefinition; + private Design? _newDesign = null; + + public RandomRestrictionDrawer(AutomationChanged automationChanged, Configuration config, AutoDesignManager autoDesignManager, + RandomDesignCombo randomDesignCombo, SetSelector selector, DesignFileSystem designFileSystem, DesignStorage designs) + { + _automationChanged = automationChanged; + _config = config; + _autoDesignManager = autoDesignManager; + _randomDesignCombo = randomDesignCombo; + _selector = selector; + _designFileSystem = designFileSystem; + _designs = designs; + _automationChanged.Subscribe(OnAutomationChange, AutomationChanged.Priority.RandomRestrictionDrawer); + } + + public void Dispose() + { + _automationChanged.Unsubscribe(OnAutomationChange); + } + + public void DrawButton(AutoDesignSet set, int designIndex) + { + var isOpen = set == _set && designIndex == _designIndex; + using (var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), isOpen) + .Push(ImGuiCol.Text, ColorId.HeaderButtons.Value(), isOpen) + .Push(ImGuiCol.Border, ColorId.HeaderButtons.Value(), isOpen)) + { + using var frame = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, isOpen); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + string.Empty, false, true)) + { + if (isOpen) + Close(); + else + Open(set, designIndex); + } + } + + ImGuiUtil.HoverTooltip("Edit restrictions for this random design."); + } + + private void Open(AutoDesignSet set, int designIndex) + { + if (designIndex < 0 || designIndex >= set.Designs.Count) + return; + + var design = set.Designs[designIndex]; + if (design.Design is not RandomDesign) + return; + + _set = set; + _designIndex = designIndex; + } + + private void Close() + { + _set = null; + _designIndex = -1; + } + + public void Draw() + { + if (_set == null || _designIndex < 0 || _designIndex >= _set.Designs.Count) + return; + + if (_set != _selector.Selection) + { + Close(); + return; + } + + var design = _set.Designs[_designIndex]; + if (design.Design is not RandomDesign random) + return; + + DrawWindow(random); + } + + private void DrawWindow(RandomDesign random) + { + var flags = ImGuiWindowFlags.NoFocusOnAppearing + | ImGuiWindowFlags.NoCollapse + | ImGuiWindowFlags.NoResize; + + // Set position to the right of the main window when attached + // The downwards offset is implicit through child position. + if (_config.KeepAdvancedDyesAttached) + { + var position = ImGui.GetWindowPos(); + position.X += ImGui.GetWindowSize().X + ImGui.GetStyle().WindowPadding.X; + ImGui.SetNextWindowPos(position); + flags |= ImGuiWindowFlags.NoMove; + } + + using var color = ImRaii.PushColor(ImGuiCol.TitleBgActive, ImGui.GetColorU32(ImGuiCol.TitleBg)); + + var size = new Vector2(7 * ImGui.GetFrameHeight() + 3 * ImGui.GetStyle().ItemInnerSpacing.X + 300 * ImGuiHelpers.GlobalScale, + 18 * ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().WindowPadding.Y + ImGui.GetStyle().ItemSpacing.Y); + ImGui.SetNextWindowSize(size); + + var open = true; + var window = ImGui.Begin($"{_set!.Name} #{_designIndex + 1:D2}###Glamourer Random Design", ref open, flags); + try + { + if (window) + DrawContent(random); + } + finally + { + ImGui.End(); + } + + if (!open) + Close(); + } + + private void DrawTable(RandomDesign random, List list) + { + using var table = ImRaii.Table("##table", 3); + if (!table) + return; + + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); + var buttonSize = new Vector2(ImGui.GetFrameHeight()); + var descWidth = ImGui.CalcTextSize("or that are set to the color").X; + ImGui.TableSetupColumn("desc", ImGuiTableColumnFlags.WidthFixed, descWidth); + ImGui.TableSetupColumn("input", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("del", ImGuiTableColumnFlags.WidthFixed, buttonSize.X * 2 + ImGui.GetStyle().ItemInnerSpacing.X); + + var orSize = ImGui.CalcTextSize("or "); + for (var i = 0; i < random.Predicates.Count; ++i) + { + using var id = ImRaii.PushId(i); + var predicate = random.Predicates[i]; + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + if (i != 0) + ImGui.TextUnformatted("or "); + else + ImGui.Dummy(orSize); + ImGui.SameLine(0, 0); + ImGui.AlignTextToFramePadding(); + switch (predicate) + { + case RandomPredicate.Contains contains: + { + ImGui.TextUnformatted("that contain"); + ImGui.TableNextColumn(); + var data = contains.Value.Text; + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputTextWithHint("##match", "Name, Path, or Identifier Contains...", ref data, 128)) + { + if (data.Length == 0) + list.RemoveAt(i); + else + list[i] = new RandomPredicate.Contains(data); + _autoDesignManager.ChangeData(_set!, _designIndex, list); + } + + break; + } + case RandomPredicate.StartsWith startsWith: + { + ImGui.TextUnformatted("whose path starts with"); + ImGui.TableNextColumn(); + var data = startsWith.Value.Text; + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputTextWithHint("##startsWith", "Path Starts With...", ref data, 128)) + { + if (data.Length == 0) + list.RemoveAt(i); + else + list[i] = new RandomPredicate.StartsWith(data); + _autoDesignManager.ChangeData(_set!, _designIndex, list); + } + + break; + } + case RandomPredicate.Exact { Which: RandomPredicate.Exact.Type.Tag } exact: + { + ImGui.TextUnformatted("that contain the tag"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + var data = exact.Value.Text; + if (ImGui.InputTextWithHint("##color", "Contained tag...", ref data, 128)) + { + if (data.Length == 0) + list.RemoveAt(i); + else + list[i] = new RandomPredicate.Exact(RandomPredicate.Exact.Type.Tag, data); + _autoDesignManager.ChangeData(_set!, _designIndex, list); + } + + break; + } + case RandomPredicate.Exact { Which: RandomPredicate.Exact.Type.Color } exact: + { + ImGui.TextUnformatted("that are set to the color"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + var data = exact.Value.Text; + if (ImGui.InputTextWithHint("##color", "Assigned Color is...", ref data, 128)) + { + if (data.Length == 0) + list.RemoveAt(i); + else + list[i] = new RandomPredicate.Exact(RandomPredicate.Exact.Type.Color, data); + _autoDesignManager.ChangeData(_set!, _designIndex, list); + } + + break; + } + case RandomPredicate.Exact exact: + { + ImGui.TextUnformatted("that are exactly"); + ImGui.TableNextColumn(); + if (_randomDesignCombo.Draw(exact, ImGui.GetContentRegionAvail().X) && _randomDesignCombo.Design is Design d) + { + list[i] = new RandomPredicate.Exact(RandomPredicate.Exact.Type.Identifier, d.Identifier.ToString()); + _autoDesignManager.ChangeData(_set!, _designIndex, list); + } + + break; + } + } + + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), buttonSize, "Delete this restriction.", false, true)) + { + list.RemoveAt(i); + _autoDesignManager.ChangeData(_set!, _designIndex, list); + } + + ImGui.SameLine(); + DrawLookup(predicate, buttonSize); + } + } + + private void DrawLookup(IDesignPredicate predicate, Vector2 buttonSize) + { + ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.MagnifyingGlassChart.ToIconString(), buttonSize, string.Empty, false, true); + if (!ImGui.IsItemHovered()) + return; + + var designs = predicate.Get(_designs, _designFileSystem); + LookupTooltip(designs); + } + + private void LookupTooltip(IEnumerable designs) + { + using var _ = ImRaii.Tooltip(); + var tt = string.Join('\n', designs.Select(d => _designFileSystem.TryGetValue(d, out var l) ? l.FullName() : d.Name.Text).OrderBy(t => t)); + ImGui.TextUnformatted(tt.Length == 0 + ? "Matches no currently existing designs." + : "Matches the following designs:"); + ImGui.Separator(); + ImGui.TextUnformatted(tt); + } + + private void DrawNewButtons(List list) + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + ImGui.InputTextWithHint("##newText", "Add New Restriction...", ref _newText, 128); + var spacing = ImGui.GetStyle().ItemInnerSpacing.X; + var invalid = _newText.Length == 0; + + var buttonSize = new Vector2((ImGui.GetContentRegionAvail().X - 3 * spacing) / 4, 0); + var changed = ImGuiUtil.DrawDisabledButton("Starts With", buttonSize, + "Add a new condition that design paths must start with the given text.", invalid) + && Add(new RandomPredicate.StartsWith(_newText)); + + ImGui.SameLine(0, spacing); + changed |= ImGuiUtil.DrawDisabledButton("Contains", buttonSize, + "Add a new condition that design paths, names or identifiers must contain the given text.", invalid) + && Add(new RandomPredicate.Contains(_newText)); + + ImGui.SameLine(0, spacing); + changed |= ImGuiUtil.DrawDisabledButton("Has Tag", buttonSize, + "Add a new condition that the design must contain the given tag.", invalid) + && Add(new RandomPredicate.Exact(RandomPredicate.Exact.Type.Tag, _newText)); + + ImGui.SameLine(0, spacing); + changed |= ImGuiUtil.DrawDisabledButton("Assigned Color", buttonSize, + "Add a new condition that the design must be assigned to the given color.", invalid) + && Add(new RandomPredicate.Exact(RandomPredicate.Exact.Type.Color, _newText)); + + if (_randomDesignCombo.Draw(_newDesign, ImGui.GetContentRegionAvail().X - spacing - buttonSize.X)) + _newDesign = _randomDesignCombo.CurrentSelection?.Item1 as Design; + ImGui.SameLine(0, spacing); + if (ImGuiUtil.DrawDisabledButton("Exact Design", buttonSize, "Add a single, specific design.", _newDesign == null)) + { + Add(new RandomPredicate.Exact(RandomPredicate.Exact.Type.Identifier, _newDesign!.Identifier.ToString())); + changed = true; + _newDesign = null; + } + + if (changed) + _autoDesignManager.ChangeData(_set!, _designIndex, list); + + return; + + bool Add(IDesignPredicate predicate) + { + list.Add(predicate); + return true; + } + } + + private void DrawManualInput(IReadOnlyList list) + { + ImGui.Dummy(Vector2.Zero); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); + DrawTotalPreview(list); + var currentDefinition = RandomPredicate.GeneratePredicateString(list); + var definition = _newDefinition ?? currentDefinition; + definition = definition.Replace(";", ";\n\t").Replace("{", "{\n\t").Replace("}", "\n}"); + var lines = definition.Count(c => c is '\n'); + if (ImGui.InputTextMultiline("##definition", ref definition, 2000, + new Vector2(ImGui.GetContentRegionAvail().X, (lines + 1) * ImGui.GetTextLineHeight() + ImGui.GetFrameHeight()), + ImGuiInputTextFlags.CtrlEnterForNewLine)) + _newDefinition = definition; + if (ImGui.IsItemDeactivatedAfterEdit() && _newDefinition != null && _newDefinition != currentDefinition) + { + var predicates = RandomPredicate.GeneratePredicates(_newDefinition.Replace("\n", string.Empty).Replace("\t", string.Empty)); + _autoDesignManager.ChangeData(_set!, _designIndex, predicates); + _newDefinition = null; + } + + if (ImGui.Button("Copy to Clipboard Without Line Breaks", new Vector2(ImGui.GetContentRegionAvail().X, 0))) + { + try + { + ImGui.SetClipboardText(currentDefinition); + } + catch + { + // ignored + } + } + } + + private void DrawTotalPreview(IReadOnlyList list) + { + var designs = IDesignPredicate.Get(list, _designs, _designFileSystem).ToList(); + var button = designs.Count > 0 + ? $"All Restrictions Combined Match {designs.Count} Designs" + : "None of the Restrictions Matches Any Designs"; + ImGuiUtil.DrawDisabledButton(button, new Vector2(ImGui.GetContentRegionAvail().X, 0), + string.Empty, false, false); + if (ImGui.IsItemHovered()) + LookupTooltip(designs); + } + + private void DrawContent(RandomDesign random) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().WindowPadding.Y + ImGuiHelpers.GlobalScale); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); + var reset = random.ResetOnRedraw; + if (ImUtf8.Checkbox("Reset Chosen Design On Every Redraw"u8, ref reset)) + _autoDesignManager.ChangeData(_set!, _designIndex, reset); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); + + var list = random.Predicates.ToList(); + if (list.Count == 0) + { + ImGui.TextUnformatted("No Restrictions Set. Selects among all existing Designs."); + } + else + { + ImGui.TextUnformatted("Select among designs..."); + DrawTable(random, list); + } + + ImGui.Dummy(Vector2.Zero); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); + + DrawNewButtons(list); + DrawManualInput(list); + } + + private void OnAutomationChange(AutomationChanged.Type type, AutoDesignSet? set, object? data) + { + if (set != _set || _set == null) + return; + + switch (type) + { + case AutomationChanged.Type.DeletedSet: + case AutomationChanged.Type.DeletedDesign when data is int index && _designIndex == index: + Close(); + break; + case AutomationChanged.Type.MovedDesign when data is (int from, int to): + if (_designIndex == from) + _designIndex = to; + else if (_designIndex < from && _designIndex > to) + _designIndex++; + else if (_designIndex > to && _designIndex < from) + _designIndex--; + break; + case AutomationChanged.Type.ChangedDesign when data is (int index, IDesignStandIn _, IDesignStandIn _) && index == _designIndex: + Close(); + break; + } + } +} diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs index a1c1fcf..8a85a45 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs @@ -1,58 +1,43 @@ -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Text; -using Dalamud.Interface; +using Dalamud.Interface; using Dalamud.Interface.Utility; using Glamourer.Automation; -using Glamourer.Customization; +using Glamourer.Designs; +using Glamourer.Designs.Special; using Glamourer.Interop; using Glamourer.Services; -using Glamourer.Structs; using Glamourer.Unlocks; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; +using OtterGui.Extensions; using OtterGui.Log; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Action = System.Action; -using CustomizeIndex = Glamourer.Customization.CustomizeIndex; namespace Glamourer.Gui.Tabs.AutomationTab; -public class SetPanel +public class SetPanel( + SetSelector _selector, + AutoDesignManager _manager, + JobService _jobs, + ItemUnlockManager _itemUnlocks, + SpecialDesignCombo _designCombo, + CustomizeUnlockManager _customizeUnlocks, + CustomizeService _customizations, + IdentifierDrawer _identifierDrawer, + Configuration _config, + RandomRestrictionDrawer _randomDrawer) { - private readonly AutoDesignManager _manager; - private readonly SetSelector _selector; - private readonly ItemUnlockManager _itemUnlocks; - private readonly CustomizeUnlockManager _customizeUnlocks; - private readonly CustomizationService _customizations; - - private readonly Configuration _config; - private readonly DesignCombo _designCombo; - private readonly JobGroupCombo _jobGroupCombo; - private readonly IdentifierDrawer _identifierDrawer; - - private string? _tempName; - private int _dragIndex = -1; + private readonly JobGroupCombo _jobGroupCombo = new(_manager, _jobs, Glamourer.Log); + private readonly HeaderDrawer.Button[] _rightButtons = [new HeaderDrawer.IncognitoButton(_config)]; + private string? _tempName; + private int _dragIndex = -1; private Action? _endAction; - public SetPanel(SetSelector selector, AutoDesignManager manager, JobService jobs, ItemUnlockManager itemUnlocks, DesignCombo designCombo, - CustomizeUnlockManager customizeUnlocks, CustomizationService customizations, IdentifierDrawer identifierDrawer, Configuration config) - { - _selector = selector; - _manager = manager; - _itemUnlocks = itemUnlocks; - _customizeUnlocks = customizeUnlocks; - _customizations = customizations; - _identifierDrawer = identifierDrawer; - _config = config; - _designCombo = designCombo; - _jobGroupCombo = new JobGroupCombo(manager, jobs, Glamourer.Log); - } - private AutoDesignSet Selection => _selector.Selection!; @@ -64,49 +49,63 @@ public class SetPanel } private void DrawHeader() - => HeaderDrawer.Draw(_selector.SelectionName, 0, ImGui.GetColorU32(ImGuiCol.FrameBg), 0, - HeaderDrawer.Button.IncognitoButton(_selector.IncognitoMode, v => _selector.IncognitoMode = v)); + => HeaderDrawer.Draw(_selector.SelectionName, 0, ImGui.GetColorU32(ImGuiCol.FrameBg), [], _rightButtons); private void DrawPanel() { - using var child = ImRaii.Child("##Panel", -Vector2.One, true); + using var child = ImUtf8.Child("##Panel"u8, -Vector2.One, true); if (!child || !_selector.HasSelection) return; var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }; - using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) + using (ImUtf8.Group()) { - var enabled = Selection.Enabled; - if (ImGui.Checkbox("##Enabled", ref enabled)) - _manager.SetState(_selector.SelectionIndex, enabled); - ImGuiUtil.LabeledHelpMarker("Enabled", - "Whether the designs in this set should be applied at all. Only one set can be enabled for a character at the same time."); - } - - ImGui.SameLine(); - using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) - { - var useGame = _selector.Selection!.BaseState is AutoDesignSet.Base.Game; - if (ImGui.Checkbox("##gameState", ref useGame)) - _manager.ChangeBaseState(_selector.SelectionIndex, useGame ? AutoDesignSet.Base.Game : AutoDesignSet.Base.Current); - ImGuiUtil.LabeledHelpMarker("Use Game State as Base", - "When this is enabled, the designs matching conditions will be applied successively on top of what your character is supposed to look like for the game. " - + "Otherwise, they will be applied on top of the characters actual current look using Glamourer."); - } - - ImGui.SameLine(); - using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) - { - var editing = _config.ShowAutomationSetEditing; - if (ImGui.Checkbox("##Show Editing", ref editing)) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) { - _config.ShowAutomationSetEditing = editing; - _config.Save(); + var enabled = Selection.Enabled; + if (ImUtf8.Checkbox("##Enabled"u8, ref enabled)) + _manager.SetState(_selector.SelectionIndex, enabled); + ImUtf8.LabeledHelpMarker("Enabled"u8, + "Whether the designs in this set should be applied at all. Only one set can be enabled for a character at the same time."u8); } - ImGuiUtil.LabeledHelpMarker("Show Editing", - "Show options to change the name or the associated character or NPC of this design set."); + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) + { + var useGame = _selector.Selection!.BaseState is AutoDesignSet.Base.Game; + if (ImUtf8.Checkbox("##gameState"u8, ref useGame)) + _manager.ChangeBaseState(_selector.SelectionIndex, useGame ? AutoDesignSet.Base.Game : AutoDesignSet.Base.Current); + ImUtf8.LabeledHelpMarker("Use Game State as Base"u8, + "When this is enabled, the designs matching conditions will be applied successively on top of what your character is supposed to look like for the game. "u8 + + "Otherwise, they will be applied on top of the characters actual current look using Glamourer."u8); + } + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) + { + var editing = _config.ShowAutomationSetEditing; + if (ImUtf8.Checkbox("##Show Editing"u8, ref editing)) + { + _config.ShowAutomationSetEditing = editing; + _config.Save(); + } + + ImUtf8.LabeledHelpMarker("Show Editing"u8, + "Show options to change the name or the associated character or NPC of this design set."u8); + } + + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) + { + var resetSettings = _selector.Selection!.ResetTemporarySettings; + if (ImGui.Checkbox("##resetSettings", ref resetSettings)) + _manager.ChangeResetSettings(_selector.SelectionIndex, resetSettings); + + ImUtf8.LabeledHelpMarker("Reset Temporary Settings"u8, + "Always reset all temporary settings applied by Glamourer when this automation set is applied, regardless of active designs."u8); + } } if (_config.ShowAutomationSetEditing) @@ -134,6 +133,7 @@ public class SetPanel ImGui.Separator(); ImGui.Dummy(Vector2.Zero); DrawDesignTable(); + _randomDrawer.Draw(); } @@ -161,42 +161,43 @@ public class SetPanel (false, false) => 4, }; - using var table = ImRaii.Table("SetTable", numRows, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX | ImGuiTableFlags.ScrollY); + using var table = ImUtf8.Table("SetTable"u8, numRows, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX | ImGuiTableFlags.ScrollY); if (!table) return; - ImGui.TableSetupColumn("##del", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); - ImGui.TableSetupColumn("##Index", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); + ImUtf8.TableSetupColumn("##del"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); + ImUtf8.TableSetupColumn("##Index"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); if (singleRow) { - ImGui.TableSetupColumn("Design", ImGuiTableColumnFlags.WidthFixed, 220 * ImGuiHelpers.GlobalScale); + ImUtf8.TableSetupColumn("Design"u8, ImGuiTableColumnFlags.WidthFixed, 220 * ImGuiHelpers.GlobalScale); if (_config.ShowAllAutomatedApplicationRules) - ImGui.TableSetupColumn("Application", ImGuiTableColumnFlags.WidthFixed, + ImUtf8.TableSetupColumn("Application"u8, ImGuiTableColumnFlags.WidthFixed, 6 * ImGui.GetFrameHeight() + 10 * ImGuiHelpers.GlobalScale); else - ImGui.TableSetupColumn("Use", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Use").X); + ImUtf8.TableSetupColumn("Use"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Use").X); } else { - ImGui.TableSetupColumn("Design / Job Restrictions", ImGuiTableColumnFlags.WidthFixed, 250 * ImGuiHelpers.GlobalScale); + ImUtf8.TableSetupColumn("Design / Job Restrictions"u8, ImGuiTableColumnFlags.WidthFixed, + 250 * ImGuiHelpers.GlobalScale - (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0)); if (_config.ShowAllAutomatedApplicationRules) - ImGui.TableSetupColumn("Application", ImGuiTableColumnFlags.WidthFixed, + ImUtf8.TableSetupColumn("Application"u8, ImGuiTableColumnFlags.WidthFixed, 3 * ImGui.GetFrameHeight() + 4 * ImGuiHelpers.GlobalScale); else - ImGui.TableSetupColumn("Use", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Use").X); + ImUtf8.TableSetupColumn("Use"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Use").X); } if (singleRow) - ImGui.TableSetupColumn("Job Restrictions", ImGuiTableColumnFlags.WidthStretch); + ImUtf8.TableSetupColumn("Job Restrictions"u8, ImGuiTableColumnFlags.WidthStretch); if (_config.ShowUnlockedItemWarnings) - ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 2 * ImGui.GetFrameHeight() + 4 * ImGuiHelpers.GlobalScale); + ImUtf8.TableSetupColumn(""u8, ImGuiTableColumnFlags.WidthFixed, 2 * ImGui.GetFrameHeight() + 4 * ImGuiHelpers.GlobalScale); ImGui.TableHeadersRow(); foreach (var (design, idx) in Selection.Designs.WithIndex()) { - using var id = ImRaii.PushId(idx); + using var id = ImUtf8.PushId(idx); ImGui.TableNextColumn(); var keyValid = _config.DeleteDesignModifier.IsActive(); var tt = keyValid @@ -206,21 +207,22 @@ public class SetPanel if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), new Vector2(ImGui.GetFrameHeight()), tt, !keyValid, true)) _endAction = () => _manager.DeleteDesign(Selection, idx); ImGui.TableNextColumn(); - ImGui.Selectable($"#{idx + 1:D2}"); - DrawDragDrop(Selection, idx); + DrawSelectable(idx, design.Design); + ImGui.TableNextColumn(); - _designCombo.Draw(Selection, design, idx, _selector.IncognitoMode); + DrawRandomEditing(Selection, design, idx); + _designCombo.Draw(Selection, design, idx); DrawDragDrop(Selection, idx); if (singleRow) { ImGui.TableNextColumn(); DrawApplicationTypeBoxes(Selection, design, idx, singleRow); ImGui.TableNextColumn(); - _jobGroupCombo.Draw(Selection, design, idx); + DrawConditions(design, idx); } else { - _jobGroupCombo.Draw(Selection, design, idx); + DrawConditions(design, idx); ImGui.TableNextColumn(); DrawApplicationTypeBoxes(Selection, design, idx, singleRow); } @@ -228,66 +230,130 @@ public class SetPanel if (_config.ShowUnlockedItemWarnings) { ImGui.TableNextColumn(); - DrawWarnings(design, idx); + DrawWarnings(design); } } ImGui.TableNextColumn(); ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("New"); + ImUtf8.TextFrameAligned("New"u8); ImGui.TableNextColumn(); - _designCombo.Draw(Selection, null, -1, _selector.IncognitoMode); + _designCombo.Draw(Selection, null, -1); ImGui.TableNextRow(); _endAction?.Invoke(); _endAction = null; } - private void DrawWarnings(AutoDesign design, int idx) + private void DrawSelectable(int idx, IDesignStandIn design) { - if (design.Revert) + var highlight = 0u; + var sb = new StringBuilder(); + if (design is Design d) + { + var count = design.AllLinks(true).Count(); + if (count > 1) + { + sb.AppendLine($"This design contains {count - 1} links to other designs."); + highlight = ColorId.HeaderButtons.Value(); + } + + count = d.AssociatedMods.Count; + if (count > 0) + { + sb.AppendLine($"This design contains {count} mod associations."); + highlight = ColorId.ModdedItemMarker.Value(); + } + + count = design.GetMaterialData().Count(p => p.Item2.Enabled); + if (count > 0) + { + sb.AppendLine($"This design contains {count} enabled advanced dyes."); + highlight = ColorId.AdvancedDyeActive.Value(); + } + } + + using (ImRaii.PushColor(ImGuiCol.Text, highlight, highlight != 0)) + { + ImUtf8.Selectable($"#{idx + 1:D2}"); + } + + ImUtf8.HoverTooltip($"{sb}"); + + DrawDragDrop(Selection, idx); + } + + private int _tmpGearset = int.MaxValue; + private int _whichIndex = -1; + + private void DrawConditions(AutoDesign design, int idx) + { + var usingGearset = design.GearsetIndex >= 0; + if (ImUtf8.Button($"{(usingGearset ? "Gearset:" : "Jobs:")}##usingGearset")) + { + usingGearset = !usingGearset; + _manager.ChangeGearsetCondition(Selection, idx, (short)(usingGearset ? 0 : -1)); + } + + ImUtf8.HoverTooltip("Click to switch between Job and Gearset restrictions."u8); + + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + if (usingGearset) + { + var set = 1 + (_tmpGearset == int.MaxValue || _whichIndex != idx ? design.GearsetIndex : _tmpGearset); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImUtf8.InputScalar("##whichGearset"u8, ref set)) + { + _whichIndex = idx; + _tmpGearset = Math.Clamp(set, 1, 100); + } + + if (ImGui.IsItemDeactivatedAfterEdit()) + { + _manager.ChangeGearsetCondition(Selection, idx, (short)(_tmpGearset - 1)); + _tmpGearset = int.MaxValue; + _whichIndex = -1; + } + } + else + { + _jobGroupCombo.Draw(Selection, design, idx); + } + } + + private void DrawRandomEditing(AutoDesignSet set, AutoDesign design, int designIdx) + { + if (design.Design is not RandomDesign) + return; + + _randomDrawer.DrawButton(set, designIdx); + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + } + + private void DrawWarnings(AutoDesign design) + { + if (design.Design is not DesignBase) return; var size = new Vector2(ImGui.GetFrameHeight()); size.X += ImGuiHelpers.GlobalScale; - var (equipFlags, customizeFlags, _, _, _, _) = design.ApplyWhat(); - var sb = new StringBuilder(); + var collection = design.ApplyWhat(); + var sb = new StringBuilder(); + var designData = design.Design.GetDesignData(default); foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand)) { var flag = slot.ToFlag(); - if (!equipFlags.HasFlag(flag)) + if (!collection.Equip.HasFlag(flag)) continue; - var item = design.Design!.DesignData.Item(slot); + var item = designData.Item(slot); if (!_itemUnlocks.IsUnlocked(item.Id, out _)) sb.AppendLine($"{item.Name} in {slot.ToName()} slot is not unlocked. Consider obtaining it via gameplay means!"); } using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * ImGuiHelpers.GlobalScale, 0)); - - static void DrawWarning(StringBuilder sb, uint color, Vector2 size, string suffix, string good) - { - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); - if (sb.Length > 0) - { - sb.Append(suffix); - using (var font = ImRaii.PushFont(UiBuilder.IconFont)) - { - ImGuiUtil.DrawTextButton(FontAwesomeIcon.ExclamationCircle.ToIconString(), size, color); - } - - ImGuiUtil.HoverTooltip(sb.ToString()); - } - else - { - ImGuiUtil.DrawTextButton(string.Empty, size, 0); - ImGuiUtil.HoverTooltip(good); - } - } - var tt = _config.UnlockedItemMode ? "\nThese items will be skipped when applied automatically.\n\nTo change this, disable the Obtained Item Mode setting." : string.Empty; @@ -295,15 +361,15 @@ public class SetPanel sb.Clear(); var sb2 = new StringBuilder(); - var customize = design.Design!.DesignData.Customize; - if (!design.Design.DesignData.IsHuman) + var customize = designData.Customize; + if (!designData.IsHuman) sb.AppendLine("The base model id can not be changed automatically to something non-human."); - var set = _customizations.AwaitedService.GetList(customize.Clan, customize.Gender); + var set = _customizations.Manager.GetSet(customize.Clan, customize.Gender); foreach (var type in CustomizationExtensions.All) { var flag = type.ToFlag(); - if (!customizeFlags.HasFlag(flag)) + if (!collection.Customize.HasFlag(flag)) continue; if (flag.RequiresRedraw()) @@ -321,12 +387,33 @@ public class SetPanel : string.Empty; DrawWarning(sb2, _config.UnlockedItemMode ? 0xA03030F0 : 0x0, size, tt, "All customizations to be applied are unlocked."); ImGui.SameLine(); + return; + + static void DrawWarning(StringBuilder sb, uint color, Vector2 size, string suffix, string good) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); + if (sb.Length > 0) + { + sb.Append(suffix); + using (_ = ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGuiUtil.DrawTextButton(FontAwesomeIcon.ExclamationCircle.ToIconString(), size, color); + } + + ImUtf8.HoverTooltip($"{sb}"); + } + else + { + ImGuiUtil.DrawTextButton(string.Empty, size, 0); + ImUtf8.HoverTooltip(good); + } + } } private void DrawDragDrop(AutoDesignSet set, int index) { const string dragDropLabel = "DesignDragDrop"; - using (var target = ImRaii.DragDropTarget()) + using (var target = ImUtf8.DragDropTarget()) { if (target.Success && ImGuiUtil.IsDropping(dragDropLabel)) { @@ -340,15 +427,15 @@ public class SetPanel } } - using (var source = ImRaii.DragDropSource()) + using (var source = ImUtf8.DragDropSource()) { if (source) { - ImGui.TextUnformatted($"Moving design #{index + 1:D2}..."); - if (ImGui.SetDragDropPayload(dragDropLabel, nint.Zero, 0)) + ImUtf8.Text($"Moving design #{index + 1:D2}..."); + if (ImGui.SetDragDropPayload(dragDropLabel, null, 0)) { _dragIndex = index; - _selector._dragDesignIndex = index; + _selector.DragDesignIndex = index; } } } @@ -357,26 +444,26 @@ public class SetPanel private void DrawApplicationTypeBoxes(AutoDesignSet set, AutoDesign design, int autoDesignIndex, bool singleLine) { using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * ImGuiHelpers.GlobalScale)); - var newType = design.ApplicationType; + var newType = design.Type; var newTypeInt = (uint)newType; style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); - using (var c = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value())) + using (_ = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value())) { - if (ImGui.CheckboxFlags("##all", ref newTypeInt, (uint)AutoDesign.Type.All)) - newType = (AutoDesign.Type)newTypeInt; + if (ImGui.CheckboxFlags("##all", ref newTypeInt, (uint)ApplicationType.All)) + newType = (ApplicationType)newTypeInt; } style.Pop(); - ImGuiUtil.HoverTooltip("Toggle all application modes at once."); + ImUtf8.HoverTooltip("Toggle all application modes at once."u8); if (_config.ShowAllAutomatedApplicationRules) { void Box(int idx) { - var (type, description) = Types[idx]; - var value = design.ApplicationType.HasFlag(type); - if (ImGui.Checkbox($"##{(byte)type}", ref value)) + var (type, description) = ApplicationTypeExtensions.Types[idx]; + var value = design.Type.HasFlag(type); + if (ImUtf8.Checkbox($"##{(byte)type}", ref value)) newType = value ? newType | type : newType & ~type; - ImGuiUtil.HoverTooltip(description); + ImUtf8.HoverTooltip(description); } ImGui.SameLine(); @@ -398,58 +485,42 @@ public class SetPanel private void DrawIdentifierSelection(int setIndex) { - using var id = ImRaii.PushId("Identifiers"); + using var id = ImUtf8.PushId("Identifiers"u8); _identifierDrawer.DrawWorld(130); ImGui.SameLine(); _identifierDrawer.DrawName(200 - ImGui.GetStyle().ItemSpacing.X); _identifierDrawer.DrawNpcs(330); var buttonWidth = new Vector2(165 * ImGuiHelpers.GlobalScale - ImGui.GetStyle().ItemSpacing.X / 2, 0); - if (ImGuiUtil.DrawDisabledButton("Set to Character", buttonWidth, string.Empty, !_identifierDrawer.CanSetPlayer)) + if (ImUtf8.ButtonEx("Set to Character"u8, string.Empty, buttonWidth, !_identifierDrawer.CanSetPlayer)) _manager.ChangeIdentifier(setIndex, _identifierDrawer.PlayerIdentifier); ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Set to NPC", buttonWidth, string.Empty, !_identifierDrawer.CanSetNpc)) + if (ImUtf8.ButtonEx("Set to NPC"u8, string.Empty, buttonWidth, !_identifierDrawer.CanSetNpc)) _manager.ChangeIdentifier(setIndex, _identifierDrawer.NpcIdentifier); - if (ImGuiUtil.DrawDisabledButton("Set to Retainer", buttonWidth, string.Empty, !_identifierDrawer.CanSetRetainer)) + + if (ImUtf8.ButtonEx("Set to Retainer"u8, string.Empty, buttonWidth, !_identifierDrawer.CanSetRetainer)) _manager.ChangeIdentifier(setIndex, _identifierDrawer.RetainerIdentifier); ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Set to Mannequin", buttonWidth, string.Empty, !_identifierDrawer.CanSetRetainer)) + if (ImUtf8.ButtonEx("Set to Mannequin"u8, string.Empty, buttonWidth, !_identifierDrawer.CanSetRetainer)) _manager.ChangeIdentifier(setIndex, _identifierDrawer.MannequinIdentifier); + + if (ImUtf8.ButtonEx("Set to Owned NPC"u8, string.Empty, buttonWidth, !_identifierDrawer.CanSetOwned)) + _manager.ChangeIdentifier(setIndex, _identifierDrawer.OwnedIdentifier); } - - private static readonly IReadOnlyList<(AutoDesign.Type, string)> Types = new[] + private sealed class JobGroupCombo(AutoDesignManager manager, JobService jobs, Logger log) + : FilterComboCache(() => jobs.JobGroups.Values.ToList(), MouseWheelType.None, log) { - (AutoDesign.Type.Customizations, - "Apply all customization changes that are enabled in this design and that are valid in a fixed design and for the given race and gender."), - (AutoDesign.Type.Armor, "Apply all armor piece changes that are enabled in this design and that are valid in a fixed design."), - (AutoDesign.Type.Accessories, "Apply all accessory changes that are enabled in this design and that are valid in a fixed design."), - (AutoDesign.Type.Stains, "Apply all dye changes that are enabled in this design."), - (AutoDesign.Type.Weapons, "Apply all weapon changes that are enabled in this design and that are valid with the current weapon worn."), - }; - - private sealed class JobGroupCombo : FilterComboCache - { - private readonly AutoDesignManager _manager; - private readonly JobService _jobs; - - public JobGroupCombo(AutoDesignManager manager, JobService jobs, Logger log) - : base(() => jobs.JobGroups.Values.ToList(), log) - { - _manager = manager; - _jobs = jobs; - } - public void Draw(AutoDesignSet set, AutoDesign design, int autoDesignIndex) { CurrentSelection = design.Jobs; - CurrentSelectionIdx = _jobs.JobGroups.Values.IndexOf(j => j.Id == design.Jobs.Id); + CurrentSelectionIdx = jobs.JobGroups.Values.IndexOf(j => j.Id == design.Jobs.Id); if (Draw("##JobGroups", design.Jobs.Name, "Select for which job groups this design should be applied.\nControl + Right-Click to set to all classes.", ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelectionIdx >= 0) - _manager.ChangeJobCondition(set, autoDesignIndex, CurrentSelection); + manager.ChangeJobCondition(set, autoDesignIndex, CurrentSelection); else if (ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked(ImGuiMouseButton.Right)) - _manager.ChangeJobCondition(set, autoDesignIndex, _jobs.JobGroups[1]); + manager.ChangeJobCondition(set, autoDesignIndex, jobs.JobGroups[1]); } protected override string ToString(JobGroup obj) diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs b/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs index 2aaf120..8a235ae 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs @@ -1,17 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; +using Dalamud.Interface; using Dalamud.Interface.Utility; using Glamourer.Automation; using Glamourer.Events; -using Glamourer.Interop; -using Glamourer.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; +using Penumbra.GameData.Interop; using Penumbra.String; using ImGuiClip = OtterGui.ImGuiClip; @@ -22,34 +18,32 @@ public class SetSelector : IDisposable private readonly Configuration _config; private readonly AutoDesignManager _manager; private readonly AutomationChanged _event; - private readonly ActorService _actors; - private readonly ObjectManager _objects; - private readonly List<(AutoDesignSet, int)> _list = new(); + private readonly ActorObjectManager _objects; + private readonly List<(AutoDesignSet, int)> _list = []; public AutoDesignSet? Selection { get; private set; } public int SelectionIndex { get; private set; } = -1; public bool IncognitoMode { - get => _config.IncognitoMode; + get => _config.Ephemeral.IncognitoMode; set { - _config.IncognitoMode = value; - _config.Save(); + _config.Ephemeral.IncognitoMode = value; + _config.Ephemeral.Save(); } } private int _dragIndex = -1; private Action? _endAction; - internal int _dragDesignIndex = -1; + internal int DragDesignIndex = -1; - public SetSelector(AutoDesignManager manager, AutomationChanged @event, Configuration config, ActorService actors, ObjectManager objects) + public SetSelector(AutoDesignManager manager, AutomationChanged @event, Configuration config, ActorObjectManager objects) { _manager = manager; _event = @event; _config = config; - _actors = actors; _objects = objects; _event.Subscribe(OnAutomationChange, AutomationChanged.Priority.SetSelector); } @@ -98,7 +92,7 @@ public class SetSelector : IDisposable } private LowerString _filter = LowerString.Empty; - private uint _enabledFilter = 0; + private uint _enabledFilter; private float _width; private Vector2 _defaultItemSpacing; private Vector2 _selectableSize; @@ -150,7 +144,7 @@ public class SetSelector : IDisposable ImGui.SameLine(); var f = _enabledFilter; - if (ImGui.CheckboxFlags("##enabledFilter", ref f, 3)) + if (ImGui.CheckboxFlags("##enabledFilter", ref f, 3u)) { _enabledFilter = _enabledFilter switch { @@ -181,7 +175,6 @@ public class SetSelector : IDisposable UpdateList(); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _defaultItemSpacing); _selectableSize = new Vector2(0, 2 * ImGui.GetTextLineHeight() + ImGui.GetStyle().ItemSpacing.Y); - _objects.Update(); ImGuiClip.ClippedDraw(_list, DrawSetSelectable, _selectableSize.Y + 2 * ImGui.GetStyle().ItemSpacing.Y); _endAction?.Invoke(); _endAction = null; @@ -190,7 +183,7 @@ public class SetSelector : IDisposable private void DrawSetSelectable((AutoDesignSet Set, int Index) pair) { using var id = ImRaii.PushId(pair.Index); - using (var color = ImRaii.PushColor(ImGuiCol.Text, pair.Set.Enabled ? ColorId.EnabledAutoSet.Value() : ColorId.DisabledAutoSet.Value())) + using (ImRaii.PushColor(ImGuiCol.Text, pair.Set.Enabled ? ColorId.EnabledAutoSet.Value() : ColorId.DisabledAutoSet.Value())) { if (ImGui.Selectable(GetSetName(pair.Set, pair.Index), pair.Set == Selection, ImGuiSelectableFlags.None, _selectableSize)) { @@ -289,12 +282,12 @@ public class SetSelector : IDisposable private void NewSetButton(Vector2 size) { - var id = _actors.AwaitedService.GetCurrentPlayer(); + var id = _objects.Actors.GetCurrentPlayer(); if (!id.IsValid) - id = _actors.AwaitedService.CreatePlayer(ByteString.FromSpanUnsafe("New Design"u8, true, false, true), ushort.MaxValue); + id = _objects.Actors.CreatePlayer(ByteString.FromSpanUnsafe("New Design"u8, true, false, true), ushort.MaxValue); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, $"Create a new Automatic Design Set for {id}. The associated player can be changed later.", !id.IsValid, true)) - _manager.AddDesignSet("New Design", id); + _manager.AddDesignSet("New Automation Set", id); } private void DuplicateSetButton(Vector2 size) @@ -336,15 +329,15 @@ public class SetSelector : IDisposable } else if (ImGuiUtil.IsDropping("DesignDragDrop")) { - if (_dragDesignIndex >= 0) + if (DragDesignIndex >= 0) { - var idx = _dragDesignIndex; + var idx = DragDesignIndex; var setTo = set; var setFrom = Selection!; _endAction = () => _manager.MoveDesignToSet(setFrom, idx, setTo); } - _dragDesignIndex = -1; + DragDesignIndex = -1; } } } @@ -354,7 +347,7 @@ public class SetSelector : IDisposable if (source) { ImGui.TextUnformatted($"Moving design set {GetSetName(set, index)} from position {index + 1}..."); - if (ImGui.SetDragDropPayload(dragDropLabel, nint.Zero, 0)) + if (ImGui.SetDragDropPayload(dragDropLabel, null, 0)) _dragIndex = index; } } diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs deleted file mode 100644 index 05e313e..0000000 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ /dev/null @@ -1,1717 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Numerics; -using System.Text; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Interface; -using Dalamud.Interface.Utility; -using Dalamud.Plugin; -using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Glamourer.Api; -using Glamourer.Automation; -using Glamourer.Customization; -using Glamourer.Designs; -using Glamourer.Events; -using Glamourer.Interop; -using Glamourer.Interop.Penumbra; -using Glamourer.Interop.Structs; -using Glamourer.Services; -using Glamourer.State; -using Glamourer.Unlocks; -using Glamourer.Utility; -using ImGuiNET; -using Newtonsoft.Json.Linq; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Api.Enums; -using Penumbra.GameData.Data; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using ImGuiClip = OtterGui.ImGuiClip; - -namespace Glamourer.Gui.Tabs; - -public unsafe class DebugTab : ITab -{ - private readonly DalamudPluginInterface _pluginInterface; - private readonly Configuration _config; - private readonly VisorService _visorService; - private readonly ChangeCustomizeService _changeCustomizeService; - private readonly UpdateSlotService _updateSlotService; - private readonly WeaponService _weaponService; - private readonly MetaService _metaService; - private readonly InventoryService _inventoryService; - private readonly PenumbraService _penumbra; - private readonly ObjectManager _objectManager; - private readonly GlamourerIpc _ipc; - private readonly CodeService _code; - private readonly DatFileService _datFileService; - - private readonly ItemManager _items; - private readonly ActorService _actors; - private readonly CustomizationService _customization; - private readonly JobService _jobs; - private readonly CustomizeUnlockManager _customizeUnlocks; - private readonly ItemUnlockManager _itemUnlocks; - - private readonly DesignManager _designManager; - private readonly DesignFileSystem _designFileSystem; - private readonly AutoDesignManager _autoDesignManager; - private readonly DesignConverter _designConverter; - private readonly HumanModelList _humans; - - private readonly PenumbraChangedItemTooltip _penumbraTooltip; - - private readonly StateManager _state; - private readonly FunModule _funModule; - - private int _gameObjectIndex; - - public bool IsVisible - => _config.DebugMode; - - public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, UpdateSlotService updateSlotService, - WeaponService weaponService, PenumbraService penumbra, - ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager, - DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config, - PenumbraChangedItemTooltip penumbraTooltip, MetaService metaService, GlamourerIpc ipc, DalamudPluginInterface pluginInterface, - AutoDesignManager autoDesignManager, JobService jobs, CodeService code, CustomizeUnlockManager customizeUnlocks, - ItemUnlockManager itemUnlocks, DesignConverter designConverter, DatFileService datFileService, InventoryService inventoryService, - HumanModelList humans, FunModule funModule) - { - _changeCustomizeService = changeCustomizeService; - _visorService = visorService; - _updateSlotService = updateSlotService; - _weaponService = weaponService; - _penumbra = penumbra; - _actors = actors; - _items = items; - _customization = customization; - _objectManager = objectManager; - _designFileSystem = designFileSystem; - _designManager = designManager; - _state = state; - _config = config; - _penumbraTooltip = penumbraTooltip; - _metaService = metaService; - _ipc = ipc; - _pluginInterface = pluginInterface; - _autoDesignManager = autoDesignManager; - _jobs = jobs; - _code = code; - _customizeUnlocks = customizeUnlocks; - _itemUnlocks = itemUnlocks; - _designConverter = designConverter; - _datFileService = datFileService; - _inventoryService = inventoryService; - _humans = humans; - _funModule = funModule; - } - - public ReadOnlySpan Label - => "Debug"u8; - - public void DrawContent() - { - using var child = ImRaii.Child("MainWindowChild"); - if (!child) - return; - - DrawInteropHeader(); - DrawGameDataHeader(); - DrawPenumbraHeader(); - DrawDesigns(); - DrawState(); - DrawAutoDesigns(); - DrawInventory(); - DrawUnlocks(); - DrawFun(); - DrawIpc(); - } - - #region Interop - - private void DrawInteropHeader() - { - if (!ImGui.CollapsingHeader("Interop")) - return; - - DrawModelEvaluation(); - DrawObjectManager(); - DrawDatFiles(); - } - - private void DrawModelEvaluation() - { - using var tree = ImRaii.TreeNode("Model Evaluation"); - if (!tree) - return; - - ImGui.InputInt("Game Object Index", ref _gameObjectIndex, 0, 0); - var actor = (Actor)_objectManager.Objects.GetObjectAddress(_gameObjectIndex); - var model = actor.Model; - using var table = ImRaii.Table("##evaluationTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableHeader("Actor"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Model"); - ImGui.TableNextColumn(); - - ImGuiUtil.DrawTableColumn("Address"); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(actor.ToString()); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(model.ToString()); - ImGui.TableNextColumn(); - if (actor.IsCharacter) - { - ImGui.TextUnformatted(actor.AsCharacter->CharacterData.ModelCharaId.ToString()); - if (actor.AsCharacter->CharacterData.TransformationId != 0) - ImGui.TextUnformatted($"Transformation Id: {actor.AsCharacter->CharacterData.TransformationId}"); - if (actor.AsCharacter->CharacterData.ModelCharaId_2 != -1) - ImGui.TextUnformatted($"ModelChara2 {actor.AsCharacter->CharacterData.ModelCharaId_2}"); - if (actor.AsCharacter->CharacterData.StatusEffectVFXId != 0) - ImGui.TextUnformatted($"Status Id: {actor.AsCharacter->CharacterData.StatusEffectVFXId}"); - } - - ImGuiUtil.DrawTableColumn("Mainhand"); - ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetMainhand().ToString() : "No Character"); - - var (mainhand, offhand, mainModel, offModel) = model.GetWeapons(actor); - ImGuiUtil.DrawTableColumn(mainModel.ToString()); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(mainhand.ToString()); - - ImGuiUtil.DrawTableColumn("Offhand"); - ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetOffhand().ToString() : "No Character"); - ImGuiUtil.DrawTableColumn(offModel.ToString()); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(offhand.ToString()); - - DrawVisor(actor, model); - DrawHatState(actor, model); - DrawWeaponState(actor, model); - DrawWetness(actor, model); - DrawEquip(actor, model); - DrawCustomize(actor, model); - } - - private string _objectFilter = string.Empty; - - private void DrawObjectManager() - { - using var tree = ImRaii.TreeNode("Object Manager"); - if (!tree) - return; - - _objectManager.Update(); - - using (var table = ImRaii.Table("##data", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) - { - if (!table) - return; - - ImGuiUtil.DrawTableColumn("Last Update"); - ImGuiUtil.DrawTableColumn(_objectManager.LastUpdate.ToString(CultureInfo.InvariantCulture)); - ImGui.TableNextColumn(); - - ImGuiUtil.DrawTableColumn("World"); - ImGuiUtil.DrawTableColumn(_actors.Valid ? _actors.AwaitedService.Data.ToWorldName(_objectManager.World) : "Service Missing"); - ImGuiUtil.DrawTableColumn(_objectManager.World.ToString()); - - ImGuiUtil.DrawTableColumn("Player Character"); - ImGuiUtil.DrawTableColumn($"{_objectManager.Player.Utf8Name} ({_objectManager.Player.Index})"); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(_objectManager.Player.ToString()); - - ImGuiUtil.DrawTableColumn("In GPose"); - ImGuiUtil.DrawTableColumn(_objectManager.IsInGPose.ToString()); - ImGui.TableNextColumn(); - - if (_objectManager.IsInGPose) - { - ImGuiUtil.DrawTableColumn("GPose Player"); - ImGuiUtil.DrawTableColumn($"{_objectManager.GPosePlayer.Utf8Name} ({_objectManager.GPosePlayer.Index})"); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(_objectManager.GPosePlayer.ToString()); - } - - ImGuiUtil.DrawTableColumn("Number of Players"); - ImGuiUtil.DrawTableColumn(_objectManager.Count.ToString()); - ImGui.TableNextColumn(); - } - - var filterChanged = ImGui.InputTextWithHint("##Filter", "Filter...", ref _objectFilter, 64); - using var table2 = ImRaii.Table("##data2", 3, - ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.ScrollY, - new Vector2(-1, 20 * ImGui.GetTextLineHeightWithSpacing())); - if (!table2) - return; - - if (filterChanged) - ImGui.SetScrollY(0); - - ImGui.TableNextColumn(); - var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); - ImGui.TableNextRow(); - - var remainder = ImGuiClip.FilteredClippedDraw(_objectManager, skips, - p => p.Value.Label.Contains(_objectFilter, StringComparison.OrdinalIgnoreCase), p - => - { - ImGuiUtil.DrawTableColumn(p.Key.ToString()); - ImGuiUtil.DrawTableColumn(p.Value.Label); - ImGuiUtil.DrawTableColumn(string.Join(", ", p.Value.Objects.OrderBy(a => a.Index).Select(a => a.Index.ToString()))); - }); - ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeightWithSpacing()); - } - - private string _datFilePath = string.Empty; - private DatCharacterFile? _datFile = null; - - private void DrawDatFiles() - { - using var tree = ImRaii.TreeNode("Character Dat File"); - if (!tree) - return; - - ImGui.InputTextWithHint("##datFilePath", "Dat File Path...", ref _datFilePath, 256); - var exists = _datFilePath.Length > 0 && File.Exists(_datFilePath); - if (ImGuiUtil.DrawDisabledButton("Load##Dat", Vector2.Zero, string.Empty, !exists)) - _datFile = _datFileService.LoadDesign(_datFilePath, out var tmp) ? tmp : null; - - if (ImGuiUtil.DrawDisabledButton("Save##Dat", Vector2.Zero, string.Empty, _datFilePath.Length == 0 || _datFile == null)) - _datFileService.SaveDesign(_datFilePath, _datFile!.Value.Customize, _datFile!.Value.Description); - - if (_datFile != null) - { - ImGui.TextUnformatted(_datFile.Value.Magic.ToString()); - ImGui.TextUnformatted(_datFile.Value.Version.ToString()); - ImGui.TextUnformatted(_datFile.Value.Time.LocalDateTime.ToString("g")); - ImGui.TextUnformatted(_datFile.Value.Voice.ToString()); - ImGui.TextUnformatted(_datFile.Value.Customize.Data.ToString()); - ImGui.TextUnformatted(_datFile.Value.Description); - } - } - - private void DrawVisor(Actor actor, Model model) - { - using var id = ImRaii.PushId("Visor"); - ImGuiUtil.DrawTableColumn("Visor State"); - ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->DrawData.IsVisorToggled.ToString() : "No Character"); - ImGuiUtil.DrawTableColumn(model.IsHuman ? VisorService.GetVisorState(model).ToString() : "No Human"); - ImGui.TableNextColumn(); - if (!model.IsHuman) - return; - - if (ImGui.SmallButton("Set True")) - _visorService.SetVisorState(model, true); - ImGui.SameLine(); - if (ImGui.SmallButton("Set False")) - _visorService.SetVisorState(model, false); - ImGui.SameLine(); - if (ImGui.SmallButton("Toggle")) - _visorService.SetVisorState(model, !VisorService.GetVisorState(model)); - } - - private void DrawHatState(Actor actor, Model model) - { - using var id = ImRaii.PushId("HatState"); - ImGuiUtil.DrawTableColumn("Hat State"); - ImGuiUtil.DrawTableColumn(actor.IsCharacter - ? actor.AsCharacter->DrawData.IsHatHidden ? "Hidden" : actor.GetArmor(EquipSlot.Head).ToString() - : "No Character"); - ImGuiUtil.DrawTableColumn(model.IsHuman - ? model.AsHuman->Head.Value == 0 ? "No Hat" : model.GetArmor(EquipSlot.Head).ToString() - : "No Human"); - ImGui.TableNextColumn(); - if (!model.IsHuman) - return; - - if (ImGui.SmallButton("Hide")) - _updateSlotService.UpdateSlot(model, EquipSlot.Head, CharacterArmor.Empty); - ImGui.SameLine(); - if (ImGui.SmallButton("Show")) - _updateSlotService.UpdateSlot(model, EquipSlot.Head, actor.GetArmor(EquipSlot.Head)); - ImGui.SameLine(); - if (ImGui.SmallButton("Toggle")) - _updateSlotService.UpdateSlot(model, EquipSlot.Head, - model.AsHuman->Head.Value == 0 ? actor.GetArmor(EquipSlot.Head) : CharacterArmor.Empty); - } - - private void DrawWeaponState(Actor actor, Model model) - { - using var id = ImRaii.PushId("WeaponState"); - ImGuiUtil.DrawTableColumn("Weapon State"); - ImGuiUtil.DrawTableColumn(actor.IsCharacter - ? actor.AsCharacter->DrawData.IsWeaponHidden ? "Hidden" : "Visible" - : "No Character"); - var text = string.Empty; - - if (!model.IsHuman) - { - text = "No Model"; - } - else if (model.AsDrawObject->Object.ChildObject == null) - { - text = "No Weapon"; - } - else - { - var weapon = (DrawObject*)model.AsDrawObject->Object.ChildObject; - if ((weapon->Flags & 0x09) == 0x09) - text = "Visible"; - else - text = "Hidden"; - } - - ImGuiUtil.DrawTableColumn(text); - ImGui.TableNextColumn(); - if (!model.IsHuman) - return; - } - - private void DrawWetness(Actor actor, Model model) - { - using var id = ImRaii.PushId("Wetness"); - ImGuiUtil.DrawTableColumn("Wetness"); - ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->IsGPoseWet ? "GPose" : "None" : "No Character"); - var modelString = model.IsCharacterBase - ? $"{model.AsCharacterBase->SwimmingWetness:F4} Swimming\n" - + $"{model.AsCharacterBase->WeatherWetness:F4} Weather\n" - + $"{model.AsCharacterBase->ForcedWetness:F4} Forced\n" - + $"{model.AsCharacterBase->WetnessDepth:F4} Depth\n" - : "No CharacterBase"; - ImGuiUtil.DrawTableColumn(modelString); - ImGui.TableNextColumn(); - if (!actor.IsCharacter) - return; - - if (ImGui.SmallButton("GPose On")) - actor.AsCharacter->IsGPoseWet = true; - ImGui.SameLine(); - if (ImGui.SmallButton("GPose Off")) - actor.AsCharacter->IsGPoseWet = false; - ImGui.SameLine(); - if (ImGui.SmallButton("GPose Toggle")) - actor.AsCharacter->IsGPoseWet = !actor.AsCharacter->IsGPoseWet; - } - - private void DrawEquip(Actor actor, Model model) - { - using var id = ImRaii.PushId("Equipment"); - foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - using var id2 = ImRaii.PushId((int)slot); - ImGuiUtil.DrawTableColumn(slot.ToName()); - ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetArmor(slot).ToString() : "No Character"); - ImGuiUtil.DrawTableColumn(model.IsHuman ? model.GetArmor(slot).ToString() : "No Human"); - ImGui.TableNextColumn(); - if (!model.IsHuman) - continue; - - if (ImGui.SmallButton("Change Piece")) - _updateSlotService.UpdateArmor(model, slot, - new CharacterArmor((SetId)(slot == EquipSlot.Hands ? 6064 : slot == EquipSlot.Head ? 6072 : 1), 1, 0)); - ImGui.SameLine(); - if (ImGui.SmallButton("Change Stain")) - _updateSlotService.UpdateStain(model, slot, 5); - ImGui.SameLine(); - if (ImGui.SmallButton("Reset")) - _updateSlotService.UpdateSlot(model, slot, actor.GetArmor(slot)); - } - } - - private void DrawCustomize(Actor actor, Model model) - { - using var id = ImRaii.PushId("Customize"); - var actorCustomize = new Customize(actor.IsCharacter - ? *(Penumbra.GameData.Structs.CustomizeData*)&actor.AsCharacter->DrawData.CustomizeData - : new Penumbra.GameData.Structs.CustomizeData()); - var modelCustomize = new Customize(model.IsHuman - ? *(Penumbra.GameData.Structs.CustomizeData*)model.AsHuman->Customize.Data - : new Penumbra.GameData.Structs.CustomizeData()); - foreach (var type in Enum.GetValues()) - { - using var id2 = ImRaii.PushId((int)type); - ImGuiUtil.DrawTableColumn(type.ToDefaultName()); - ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actorCustomize[type].Value.ToString("X2") : "No Character"); - ImGuiUtil.DrawTableColumn(model.IsHuman ? modelCustomize[type].Value.ToString("X2") : "No Human"); - ImGui.TableNextColumn(); - if (!model.IsHuman || type.ToFlag().RequiresRedraw()) - continue; - - if (ImGui.SmallButton("++")) - { - var value = modelCustomize[type].Value; - var (_, mask) = type.ToByteAndMask(); - var shift = BitOperations.TrailingZeroCount(mask); - var newValue = value + (1 << shift); - modelCustomize.Set(type, (CustomizeValue)newValue); - _changeCustomizeService.UpdateCustomize(model, modelCustomize.Data); - } - - ImGui.SameLine(); - if (ImGui.SmallButton("--")) - { - var value = modelCustomize[type].Value; - var (_, mask) = type.ToByteAndMask(); - var shift = BitOperations.TrailingZeroCount(mask); - var newValue = value - (1 << shift); - modelCustomize.Set(type, (CustomizeValue)newValue); - _changeCustomizeService.UpdateCustomize(model, modelCustomize.Data); - } - - ImGui.SameLine(); - if (ImGui.SmallButton("Reset")) - { - modelCustomize.Set(type, actorCustomize[type]); - _changeCustomizeService.UpdateCustomize(model, modelCustomize.Data); - } - } - } - - #endregion - - #region Penumbra - - private Model _drawObject = Model.Null; - - private void DrawPenumbraHeader() - { - if (!ImGui.CollapsingHeader("Penumbra")) - return; - - using var table = ImRaii.Table("##PenumbraTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (!table) - return; - - ImGuiUtil.DrawTableColumn("Available"); - ImGuiUtil.DrawTableColumn(_penumbra.Available.ToString()); - ImGui.TableNextColumn(); - if (ImGui.SmallButton("Unattach")) - _penumbra.Unattach(); - ImGui.SameLine(); - if (ImGui.SmallButton("Reattach")) - _penumbra.Reattach(); - - ImGuiUtil.DrawTableColumn("Draw Object"); - ImGui.TableNextColumn(); - var address = _drawObject.Address; - ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); - if (ImGui.InputScalar("##drawObjectPtr", ImGuiDataType.U64, (nint)(&address), IntPtr.Zero, IntPtr.Zero, "%llx", - ImGuiInputTextFlags.CharsHexadecimal)) - _drawObject = address; - ImGuiUtil.DrawTableColumn(_penumbra.Available - ? $"0x{_penumbra.GameObjectFromDrawObject(_drawObject).Address:X}" - : "Penumbra Unavailable"); - - ImGuiUtil.DrawTableColumn("Cutscene Object"); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); - ImGui.InputInt("##CutsceneIndex", ref _gameObjectIndex, 0, 0); - ImGuiUtil.DrawTableColumn(_penumbra.Available - ? _penumbra.CutsceneParent(_gameObjectIndex).ToString() - : "Penumbra Unavailable"); - - ImGuiUtil.DrawTableColumn("Redraw Object"); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); - ImGui.InputInt("##redrawObject", ref _gameObjectIndex, 0, 0); - ImGui.TableNextColumn(); - using (var disabled = ImRaii.Disabled(!_penumbra.Available)) - { - if (ImGui.SmallButton("Redraw")) - _penumbra.RedrawObject((ObjectIndex)_gameObjectIndex, RedrawType.Redraw); - } - - ImGuiUtil.DrawTableColumn("Last Tooltip Date"); - ImGuiUtil.DrawTableColumn(_penumbraTooltip.LastTooltip > DateTime.MinValue ? _penumbraTooltip.LastTooltip.ToLongTimeString() : "Never"); - ImGui.TableNextColumn(); - - ImGuiUtil.DrawTableColumn("Last Click Date"); - ImGuiUtil.DrawTableColumn(_penumbraTooltip.LastClick > DateTime.MinValue ? _penumbraTooltip.LastClick.ToLongTimeString() : "Never"); - ImGui.TableNextColumn(); - - ImGui.Separator(); - ImGui.Separator(); - foreach (var (slot, item) in _penumbraTooltip.LastItems) - { - ImGuiUtil.DrawTableColumn($"{slot.ToName()} Revert-Item"); - ImGuiUtil.DrawTableColumn(item.Valid ? item.Name : "None"); - ImGui.TableNextColumn(); - } - } - - #endregion - - #region GameData - - private void DrawGameDataHeader() - { - if (!ImGui.CollapsingHeader("Game Data")) - return; - - DrawIdentifierService(); - DrawRestrictedGear(); - DrawActorService(); - DrawItemService(); - DrawStainService(); - DrawCustomizationService(); - DrawJobService(); - } - - private void DrawJobService() - { - using var tree = ImRaii.TreeNode("Job Service"); - if (!tree) - return; - - using (var t = ImRaii.TreeNode("Jobs")) - { - if (t) - { - using var table = ImRaii.Table("##jobs", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (table) - foreach (var (id, job) in _jobs.Jobs) - { - ImGuiUtil.DrawTableColumn(id.ToString("D3")); - ImGuiUtil.DrawTableColumn(job.Name); - ImGuiUtil.DrawTableColumn(job.Abbreviation); - } - } - } - - using (var t = ImRaii.TreeNode("All Job Groups")) - { - if (t) - { - using var table = ImRaii.Table("##groups", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (table) - foreach (var (group, idx) in _jobs.AllJobGroups.WithIndex()) - { - ImGuiUtil.DrawTableColumn(idx.ToString("D3")); - ImGuiUtil.DrawTableColumn(group.Name); - ImGuiUtil.DrawTableColumn(group.Count.ToString()); - } - } - } - - using (var t = ImRaii.TreeNode("Valid Job Groups")) - { - if (t) - { - using var table = ImRaii.Table("##groups", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (table) - foreach (var (id, group) in _jobs.JobGroups) - { - ImGuiUtil.DrawTableColumn(id.ToString("D3")); - ImGuiUtil.DrawTableColumn(group.Name); - ImGuiUtil.DrawTableColumn(group.Count.ToString()); - } - } - } - } - - private string _gamePath = string.Empty; - private int _setId; - private int _secondaryId; - private int _variant; - - private void DrawIdentifierService() - { - using var disabled = ImRaii.Disabled(!_items.IdentifierService.Valid); - using var tree = ImRaii.TreeNode("Identifier Service"); - if (!tree || !_items.IdentifierService.Valid) - return; - - disabled.Dispose(); - - - static void Text(string text) - { - if (text.Length > 0) - ImGui.TextUnformatted(text); - } - - ImGui.TextUnformatted("Parse Game Path"); - ImGui.SameLine(); - ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); - ImGui.InputTextWithHint("##gamePath", "Enter game path...", ref _gamePath, 256); - var fileInfo = _items.IdentifierService.AwaitedService.GamePathParser.GetFileInfo(_gamePath); - ImGui.TextUnformatted( - $"{fileInfo.ObjectType} {fileInfo.EquipSlot} {fileInfo.PrimaryId} {fileInfo.SecondaryId} {fileInfo.Variant} {fileInfo.BodySlot} {fileInfo.CustomizationType}"); - Text(string.Join("\n", _items.IdentifierService.AwaitedService.Identify(_gamePath).Keys)); - - ImGui.Separator(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Identify Model"); - ImGui.SameLine(); - DrawInputModelSet(true); - - foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - var identified = _items.Identify(slot, (SetId)_setId, (Variant)_variant); - Text(identified.Name); - ImGuiUtil.HoverTooltip(string.Join("\n", - _items.IdentifierService.AwaitedService.Identify((SetId)_setId, (Variant)_variant, slot) - .Select(i => $"{i.Name} {i.Id} {i.ItemId} {i.IconId}"))); - } - - var weapon = _items.Identify(EquipSlot.MainHand, (SetId)_setId, (WeaponType)_secondaryId, (Variant)_variant); - Text(weapon.Name); - ImGuiUtil.HoverTooltip(string.Join("\n", - _items.IdentifierService.AwaitedService.Identify((SetId)_setId, (WeaponType)_secondaryId, (Variant)_variant, EquipSlot.MainHand))); - } - - private void DrawRestrictedGear() - { - using var tree = ImRaii.TreeNode("Restricted Gear Service"); - if (!tree) - return; - - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Resolve Model"); - DrawInputModelSet(false); - foreach (var race in Enum.GetValues().Skip(1)) - { - foreach (var gender in new[] - { - Gender.Male, - Gender.Female, - }) - { - foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - var (replaced, model) = - _items.RestrictedGear.ResolveRestricted(new CharacterArmor((SetId)_setId, (Variant)_variant, 0), slot, race, gender); - if (replaced) - ImGui.TextUnformatted($"{race.ToName()} - {gender} - {slot.ToName()} resolves to {model}."); - } - } - } - } - - private void DrawInputModelSet(bool withWeapon) - { - ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - ImGui.InputInt("##SetId", ref _setId, 0, 0); - if (withWeapon) - { - ImGui.SameLine(); - ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - ImGui.InputInt("##TypeId", ref _secondaryId, 0, 0); - } - - ImGui.SameLine(); - ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); - ImGui.InputInt("##Variant", ref _variant, 0, 0); - } - - private string _bnpcFilter = string.Empty; - private string _enpcFilter = string.Empty; - private string _companionFilter = string.Empty; - private string _mountFilter = string.Empty; - private string _ornamentFilter = string.Empty; - private string _worldFilter = string.Empty; - - private void DrawActorService() - { - using var disabled = ImRaii.Disabled(!_actors.Valid); - using var tree = ImRaii.TreeNode("Actor Service"); - if (!tree || !_actors.Valid) - return; - - disabled.Dispose(); - - DrawNameTable("BNPCs", ref _bnpcFilter, _actors.AwaitedService.Data.BNpcs.Select(kvp => (kvp.Key, kvp.Value))); - DrawNameTable("ENPCs", ref _enpcFilter, _actors.AwaitedService.Data.ENpcs.Select(kvp => (kvp.Key, kvp.Value))); - DrawNameTable("Companions", ref _companionFilter, _actors.AwaitedService.Data.Companions.Select(kvp => (kvp.Key, kvp.Value))); - DrawNameTable("Mounts", ref _mountFilter, _actors.AwaitedService.Data.Mounts.Select(kvp => (kvp.Key, kvp.Value))); - DrawNameTable("Ornaments", ref _ornamentFilter, _actors.AwaitedService.Data.Ornaments.Select(kvp => (kvp.Key, kvp.Value))); - DrawNameTable("Worlds", ref _worldFilter, _actors.AwaitedService.Data.Worlds.Select(kvp => ((uint)kvp.Key, kvp.Value))); - } - - private static void DrawNameTable(string label, ref string filter, IEnumerable<(uint, string)> names) - { - using var _ = ImRaii.PushId(label); - using var tree = ImRaii.TreeNode(label); - if (!tree) - return; - - var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref filter, 256); - var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - using var table = ImRaii.Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter, - new Vector2(-1, 10 * height)); - if (!table) - return; - - if (resetScroll) - ImGui.SetScrollY(0); - ImGui.TableSetupColumn("1", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("2", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableNextColumn(); - var skips = ImGuiClip.GetNecessarySkips(height); - ImGui.TableNextColumn(); - var f = filter; - var remainder = ImGuiClip.FilteredClippedDraw(names.Select(p => (p.Item1.ToString("D5"), p.Item2)), skips, - p => p.Item1.Contains(f) || p.Item2.Contains(f, StringComparison.OrdinalIgnoreCase), - p => - { - ImGuiUtil.DrawTableColumn(p.Item1); - ImGuiUtil.DrawTableColumn(p.Item2); - }); - ImGuiClip.DrawEndDummy(remainder, height); - } - - private string _itemFilter = string.Empty; - - private void DrawItemService() - { - using var disabled = ImRaii.Disabled(!_items.ItemService.Valid); - using var tree = ImRaii.TreeNode("Item Manager"); - if (!tree || !_items.ItemService.Valid) - return; - - disabled.Dispose(); - ImRaii.TreeNode($"Default Sword: {_items.DefaultSword.Name} ({_items.DefaultSword.ItemId}) ({_items.DefaultSword.Weapon()})", - ImGuiTreeNodeFlags.Leaf).Dispose(); - DrawNameTable("All Items (Main)", ref _itemFilter, - _items.ItemService.AwaitedService.AllItems(true).Select(p => (p.Item1.Id, - $"{p.Item2.Name} ({(p.Item2.WeaponType == 0 ? p.Item2.Armor().ToString() : p.Item2.Weapon().ToString())})")) - .OrderBy(p => p.Item1)); - DrawNameTable("All Items (Off)", ref _itemFilter, - _items.ItemService.AwaitedService.AllItems(false).Select(p => (p.Item1.Id, - $"{p.Item2.Name} ({(p.Item2.WeaponType == 0 ? p.Item2.Armor().ToString() : p.Item2.Weapon().ToString())})")) - .OrderBy(p => p.Item1)); - foreach (var type in Enum.GetValues().Skip(1)) - { - DrawNameTable(type.ToName(), ref _itemFilter, - _items.ItemService.AwaitedService[type] - .Select(p => (Id: p.ItemId.Id, $"{p.Name} ({(p.WeaponType == 0 ? p.Armor().ToString() : p.Weapon().ToString())})"))); - } - } - - private string _stainFilter = string.Empty; - - private void DrawStainService() - { - using var tree = ImRaii.TreeNode("Stain Service"); - if (!tree) - return; - - var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref _stainFilter, 256); - var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - using var table = ImRaii.Table("##table", 4, - ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.SizingFixedFit, - new Vector2(-1, 10 * height)); - if (!table) - return; - - if (resetScroll) - ImGui.SetScrollY(0); - - ImGui.TableNextColumn(); - var skips = ImGuiClip.GetNecessarySkips(height); - ImGui.TableNextRow(); - var remainder = ImGuiClip.FilteredClippedDraw(_items.Stains, skips, - p => p.Key.Id.ToString().Contains(_stainFilter) || p.Value.Name.Contains(_stainFilter, StringComparison.OrdinalIgnoreCase), - p => - { - ImGuiUtil.DrawTableColumn(p.Key.Id.ToString("D3")); - ImGui.TableNextColumn(); - ImGui.GetWindowDrawList().AddRectFilled(ImGui.GetCursorScreenPos(), - ImGui.GetCursorScreenPos() + new Vector2(ImGui.GetTextLineHeight()), - p.Value.RgbaColor, 5 * ImGuiHelpers.GlobalScale); - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight())); - ImGuiUtil.DrawTableColumn(p.Value.Name); - ImGuiUtil.DrawTableColumn($"#{p.Value.R:X2}{p.Value.G:X2}{p.Value.B:X2}{(p.Value.Gloss ? ", Glossy" : string.Empty)}"); - }); - ImGuiClip.DrawEndDummy(remainder, height); - } - - private void DrawCustomizationService() - { - using var disabled = ImRaii.Disabled(!_customization.Valid); - using var tree = ImRaii.TreeNode("Customization Service"); - if (!tree || !_customization.Valid) - return; - - disabled.Dispose(); - - foreach (var clan in _customization.AwaitedService.Clans) - { - foreach (var gender in _customization.AwaitedService.Genders) - { - var set = _customization.AwaitedService.GetList(clan, gender); - DrawCustomizationInfo(set); - DrawNpcCustomizationInfo(set); - } - } - } - - private void DrawCustomizationInfo(CustomizationSet set) - { - using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender}"); - if (!tree) - return; - - using var table = ImRaii.Table("data", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (!table) - return; - - foreach (var index in Enum.GetValues()) - { - ImGuiUtil.DrawTableColumn(index.ToString()); - ImGuiUtil.DrawTableColumn(set.Option(index)); - ImGuiUtil.DrawTableColumn(set.IsAvailable(index) ? "Available" : "Unavailable"); - ImGuiUtil.DrawTableColumn(set.Type(index).ToString()); - ImGuiUtil.DrawTableColumn(set.Count(index).ToString()); - } - } - - private void DrawNpcCustomizationInfo(CustomizationSet set) - { - using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender} (NPC Options)"); - if (!tree) - return; - - using var table = ImRaii.Table("npc", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (!table) - return; - - foreach (var (index, value) in set.NpcOptions) - { - ImGuiUtil.DrawTableColumn(index.ToString()); - ImGuiUtil.DrawTableColumn(value.Value.ToString()); - } - } - - #endregion - - #region Designs - - private string _base64 = string.Empty; - private string _restore = string.Empty; - private byte[] _base64Bytes = Array.Empty(); - private byte[] _restoreBytes = Array.Empty(); - private DesignData _parse64 = new(); - private Exception? _parse64Failure; - - private void DrawDesigns() - { - if (!ImGui.CollapsingHeader("Designs")) - return; - - DrawDesignManager(); - DrawDesignTester(); - DrawDesignConverter(); - } - - private void DrawDesignManager() - { - using var tree = ImRaii.TreeNode($"Design Manager ({_designManager.Designs.Count} Designs)###Design Manager"); - if (!tree) - return; - - foreach (var (design, idx) in _designManager.Designs.WithIndex()) - { - using var t = ImRaii.TreeNode($"{design.Name}##{idx}"); - if (!t) - continue; - - DrawDesign(design); - var base64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.ApplyEquip, design.ApplyCustomize, - design.DoApplyHatVisible(), - design.DoApplyVisorToggle(), design.DoApplyWeaponVisible(), design.WriteProtected()); - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - ImGuiUtil.TextWrapped(base64); - if (ImGui.IsItemClicked()) - ImGui.SetClipboardText(base64); - } - } - - private void DrawDesignTester() - { - using var tree = ImRaii.TreeNode("Base64 Design Tester"); - if (!tree) - return; - - ImGui.SetNextItemWidth(-1); - ImGui.InputTextWithHint("##base64", "Base 64 input...", ref _base64, 2047); - if (ImGui.IsItemDeactivatedAfterEdit()) - { - try - { - _base64Bytes = Convert.FromBase64String(_base64); - _parse64Failure = null; - } - catch (Exception ex) - { - _base64Bytes = Array.Empty(); - _parse64Failure = ex; - } - - if (_parse64Failure == null) - try - { - _parse64 = DesignBase64Migration.MigrateBase64(_items, _humans, _base64, out var ef, out var cf, out var wp, out var ah, - out var av, - out var aw); - _restore = DesignBase64Migration.CreateOldBase64(in _parse64, ef, cf, ah, av, aw, wp); - _restoreBytes = Convert.FromBase64String(_restore); - } - catch (Exception ex) - { - _parse64Failure = ex; - _restore = string.Empty; - } - } - - if (_parse64Failure != null) - { - ImGuiUtil.TextWrapped(_parse64Failure.ToString()); - } - else if (_restore.Length > 0) - { - DrawDesignData(_parse64); - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - ImGui.TextUnformatted(_base64); - using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 0 })) - { - foreach (var (c1, c2) in _restore.Zip(_base64)) - { - using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF4040D0, c1 != c2); - ImGui.TextUnformatted(c1.ToString()); - ImGui.SameLine(); - } - } - - ImGui.NewLine(); - - foreach (var ((b1, b2), idx) in _base64Bytes.Zip(_restoreBytes).WithIndex()) - { - using (var group = ImRaii.Group()) - { - ImGui.TextUnformatted(idx.ToString("D2")); - ImGui.TextUnformatted(b1.ToString("X2")); - using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF4040D0, b1 != b2); - ImGui.TextUnformatted(b2.ToString("X2")); - } - - ImGui.SameLine(); - } - - ImGui.NewLine(); - } - - if (_parse64Failure != null && _base64Bytes.Length > 0) - { - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - foreach (var (b, idx) in _base64Bytes.WithIndex()) - { - using (var group = ImRaii.Group()) - { - ImGui.TextUnformatted(idx.ToString("D2")); - ImGui.TextUnformatted(b.ToString("X2")); - } - - ImGui.SameLine(); - } - - ImGui.NewLine(); - } - } - - private string _clipboardText = string.Empty; - private byte[] _clipboardData = Array.Empty(); - private byte[] _dataUncompressed = Array.Empty(); - private byte _version = 0; - private string _textUncompressed = string.Empty; - private JObject? _json = null; - private DesignBase? _tmpDesign = null; - private Exception? _clipboardProblem = null; - - private void DrawDesignConverter() - { - using var tree = ImRaii.TreeNode("Design Converter"); - if (!tree) - return; - - if (ImGui.Button("Import Clipboard")) - { - _clipboardText = string.Empty; - _clipboardData = Array.Empty(); - _dataUncompressed = Array.Empty(); - _textUncompressed = string.Empty; - _json = null; - _tmpDesign = null; - _clipboardProblem = null; - - try - { - _clipboardText = ImGui.GetClipboardText(); - _clipboardData = Convert.FromBase64String(_clipboardText); - _version = _clipboardData[0]; - if (_version == 5) - _clipboardData = _clipboardData[DesignBase64Migration.Base64SizeV4..]; - _version = _clipboardData.Decompress(out _dataUncompressed); - _textUncompressed = Encoding.UTF8.GetString(_dataUncompressed); - _json = JObject.Parse(_textUncompressed); - _tmpDesign = _designConverter.FromBase64(_clipboardText, true, true, out _); - } - catch (Exception ex) - { - _clipboardProblem = ex; - } - } - - if (_clipboardText.Length > 0) - { - using var f = ImRaii.PushFont(UiBuilder.MonoFont); - ImGuiUtil.TextWrapped(_clipboardText); - } - - if (_clipboardData.Length > 0) - { - using var f = ImRaii.PushFont(UiBuilder.MonoFont); - ImGuiUtil.TextWrapped(string.Join(" ", _clipboardData.Select(b => b.ToString("X2")))); - } - - if (_dataUncompressed.Length > 0) - { - using var f = ImRaii.PushFont(UiBuilder.MonoFont); - ImGuiUtil.TextWrapped(string.Join(" ", _dataUncompressed.Select(b => b.ToString("X2")))); - } - - if (_textUncompressed.Length > 0) - { - using var f = ImRaii.PushFont(UiBuilder.MonoFont); - ImGuiUtil.TextWrapped(_textUncompressed); - } - - if (_json != null) - ImGui.TextUnformatted("JSON Parsing Successful!"); - - if (_tmpDesign != null) - DrawDesign(_tmpDesign); - - if (_clipboardProblem != null) - { - using var f = ImRaii.PushFont(UiBuilder.MonoFont); - ImGuiUtil.TextWrapped(_clipboardProblem.ToString()); - } - } - - public void DrawState(ActorData data, ActorState state) - { - using var table = ImRaii.Table("##state", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - ImGuiUtil.DrawTableColumn("Name"); - ImGuiUtil.DrawTableColumn(state.Identifier.ToString()); - ImGui.TableNextColumn(); - if (ImGui.Button("Reset")) - _state.ResetState(state, StateChanged.Source.Manual); - - ImGui.TableNextRow(); - - static void PrintRow(string label, T actor, T model, StateChanged.Source source) where T : notnull - { - ImGuiUtil.DrawTableColumn(label); - ImGuiUtil.DrawTableColumn(actor.ToString()!); - ImGuiUtil.DrawTableColumn(model.ToString()!); - ImGuiUtil.DrawTableColumn(source.ToString()); - } - - static string ItemString(in DesignData data, EquipSlot slot) - { - var item = data.Item(slot); - return $"{item.Name} ({item.ModelId.Id}{(item.WeaponType != 0 ? $"-{item.WeaponType.Id}" : string.Empty)}-{item.Variant})"; - } - - PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state[ActorState.MetaIndex.ModelId]); - ImGui.TableNextRow(); - PrintRow("Wetness", state.BaseData.IsWet(), state.ModelData.IsWet(), state[ActorState.MetaIndex.Wetness]); - ImGui.TableNextRow(); - - if (state.BaseData.IsHuman && state.ModelData.IsHuman) - { - PrintRow("Hat Visible", state.BaseData.IsHatVisible(), state.ModelData.IsHatVisible(), state[ActorState.MetaIndex.HatState]); - ImGui.TableNextRow(); - PrintRow("Visor Toggled", state.BaseData.IsVisorToggled(), state.ModelData.IsVisorToggled(), - state[ActorState.MetaIndex.VisorState]); - ImGui.TableNextRow(); - PrintRow("Weapon Visible", state.BaseData.IsWeaponVisible(), state.ModelData.IsWeaponVisible(), - state[ActorState.MetaIndex.WeaponState]); - ImGui.TableNextRow(); - foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) - { - PrintRow(slot.ToName(), ItemString(state.BaseData, slot), ItemString(state.ModelData, slot), state[slot, false]); - ImGuiUtil.DrawTableColumn(state.BaseData.Stain(slot).Id.ToString()); - ImGuiUtil.DrawTableColumn(state.ModelData.Stain(slot).Id.ToString()); - ImGuiUtil.DrawTableColumn(state[slot, true].ToString()); - } - - foreach (var type in Enum.GetValues()) - { - PrintRow(type.ToDefaultName(), state.BaseData.Customize[type].Value, state.ModelData.Customize[type].Value, state[type]); - ImGui.TableNextRow(); - } - } - else - { - ImGuiUtil.DrawTableColumn(string.Join(" ", state.BaseData.GetCustomizeBytes().Select(b => b.ToString("X2")))); - ImGuiUtil.DrawTableColumn(string.Join(" ", state.ModelData.GetCustomizeBytes().Select(b => b.ToString("X2")))); - ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn(string.Join(" ", state.BaseData.GetEquipmentBytes().Select(b => b.ToString("X2")))); - ImGuiUtil.DrawTableColumn(string.Join(" ", state.ModelData.GetEquipmentBytes().Select(b => b.ToString("X2")))); - } - } - - public static void DrawDesignData(in DesignData data) - { - if (data.IsHuman) - { - using var table = ImRaii.Table("##equip", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) - { - var item = data.Item(slot); - var stain = data.Stain(slot); - ImGuiUtil.DrawTableColumn(slot.ToName()); - ImGuiUtil.DrawTableColumn(item.Name); - ImGuiUtil.DrawTableColumn(item.ItemId.ToString()); - ImGuiUtil.DrawTableColumn(stain.ToString()); - } - - ImGuiUtil.DrawTableColumn("Hat Visible"); - ImGuiUtil.DrawTableColumn(data.IsHatVisible().ToString()); - ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn("Visor Toggled"); - ImGuiUtil.DrawTableColumn(data.IsVisorToggled().ToString()); - ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn("Weapon Visible"); - ImGuiUtil.DrawTableColumn(data.IsWeaponVisible().ToString()); - ImGui.TableNextRow(); - - ImGuiUtil.DrawTableColumn("Model ID"); - ImGuiUtil.DrawTableColumn(data.ModelId.ToString()); - ImGui.TableNextRow(); - - foreach (var index in Enum.GetValues()) - { - var value = data.Customize[index]; - ImGuiUtil.DrawTableColumn(index.ToDefaultName()); - ImGuiUtil.DrawTableColumn(value.Value.ToString()); - ImGui.TableNextRow(); - } - - ImGuiUtil.DrawTableColumn("Is Wet"); - ImGuiUtil.DrawTableColumn(data.IsWet().ToString()); - ImGui.TableNextRow(); - } - else - { - ImGui.TextUnformatted($"Model ID {data.ModelId}"); - ImGui.Separator(); - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - ImGui.TextUnformatted("Customize Array"); - ImGui.Separator(); - ImGuiUtil.TextWrapped(string.Join(" ", data.GetCustomizeBytes().Select(b => b.ToString("X2")))); - - ImGui.TextUnformatted("Equipment Array"); - ImGui.Separator(); - ImGuiUtil.TextWrapped(string.Join(" ", data.GetEquipmentBytes().Select(b => b.ToString("X2")))); - } - } - - private void DrawDesign(DesignBase design) - { - using var table = ImRaii.Table("##equip", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); - if (design is Design d) - { - ImGuiUtil.DrawTableColumn("Name"); - ImGuiUtil.DrawTableColumn(d.Name); - ImGuiUtil.DrawTableColumn($"({d.Index})"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted("Description (Hover)"); - ImGuiUtil.HoverTooltip(d.Description); - ImGui.TableNextRow(); - - ImGuiUtil.DrawTableColumn("Identifier"); - ImGuiUtil.DrawTableColumn(d.Identifier.ToString()); - ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn("Design File System Path"); - ImGuiUtil.DrawTableColumn(_designFileSystem.FindLeaf(d, out var leaf) ? leaf.FullName() : "No Path Known"); - ImGui.TableNextRow(); - - ImGuiUtil.DrawTableColumn("Creation"); - ImGuiUtil.DrawTableColumn(d.CreationDate.ToString()); - ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn("Update"); - ImGuiUtil.DrawTableColumn(d.LastEdit.ToString()); - ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn("Tags"); - ImGuiUtil.DrawTableColumn(string.Join(", ", d.Tags)); - ImGui.TableNextRow(); - } - - foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) - { - var item = design.DesignData.Item(slot); - var apply = design.DoApplyEquip(slot); - var stain = design.DesignData.Stain(slot); - var applyStain = design.DoApplyStain(slot); - ImGuiUtil.DrawTableColumn(slot.ToName()); - ImGuiUtil.DrawTableColumn(item.Name); - ImGuiUtil.DrawTableColumn(item.ItemId.ToString()); - ImGuiUtil.DrawTableColumn(apply ? "Apply" : "Keep"); - ImGuiUtil.DrawTableColumn(stain.ToString()); - ImGuiUtil.DrawTableColumn(applyStain ? "Apply" : "Keep"); - } - - ImGuiUtil.DrawTableColumn("Hat Visible"); - ImGuiUtil.DrawTableColumn(design.DesignData.IsHatVisible().ToString()); - ImGuiUtil.DrawTableColumn(design.DoApplyHatVisible() ? "Apply" : "Keep"); - ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn("Visor Toggled"); - ImGuiUtil.DrawTableColumn(design.DesignData.IsVisorToggled().ToString()); - ImGuiUtil.DrawTableColumn(design.DoApplyVisorToggle() ? "Apply" : "Keep"); - ImGui.TableNextRow(); - ImGuiUtil.DrawTableColumn("Weapon Visible"); - ImGuiUtil.DrawTableColumn(design.DesignData.IsWeaponVisible().ToString()); - ImGuiUtil.DrawTableColumn(design.DoApplyWeaponVisible() ? "Apply" : "Keep"); - ImGui.TableNextRow(); - - ImGuiUtil.DrawTableColumn("Model ID"); - ImGuiUtil.DrawTableColumn(design.DesignData.ModelId.ToString()); - ImGui.TableNextRow(); - - foreach (var index in Enum.GetValues()) - { - var value = design.DesignData.Customize[index]; - var apply = design.DoApplyCustomize(index); - ImGuiUtil.DrawTableColumn(index.ToDefaultName()); - ImGuiUtil.DrawTableColumn(value.Value.ToString()); - ImGuiUtil.DrawTableColumn(apply ? "Apply" : "Keep"); - ImGui.TableNextRow(); - } - - ImGuiUtil.DrawTableColumn("Is Wet"); - ImGuiUtil.DrawTableColumn(design.DesignData.IsWet().ToString()); - ImGui.TableNextRow(); - } - - #endregion - - #region State - - private void DrawState() - { - if (!ImGui.CollapsingHeader($"State ({_state.Count})###State")) - return; - - DrawActorTrees(); - DrawRetainedStates(); - } - - private void DrawActorTrees() - { - using var tree = ImRaii.TreeNode("Active Actors"); - if (!tree) - return; - - _objectManager.Update(); - foreach (var (identifier, actors) in _objectManager) - { - if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Trash.ToIconString()}##{actors.Label}", new Vector2(ImGui.GetFrameHeight()), - string.Empty, !_state.ContainsKey(identifier), true)) - _state.DeleteState(identifier); - - ImGui.SameLine(); - using var t = ImRaii.TreeNode(actors.Label); - if (!t) - continue; - - if (_state.GetOrCreate(identifier, actors.Objects[0], out var state)) - DrawState(actors, state); - else - ImGui.TextUnformatted("Invalid actor."); - } - } - - private void DrawRetainedStates() - { - using var tree = ImRaii.TreeNode("Retained States (Inactive Actors)"); - if (!tree) - return; - - foreach (var (identifier, state) in _state.Where(kvp => !_objectManager.ContainsKey(kvp.Key))) - { - using var t = ImRaii.TreeNode(identifier.ToString()); - if (t) - DrawState(ActorData.Invalid, state); - } - } - - #endregion - - #region Auto Designs - - private void DrawAutoDesigns() - { - if (!ImGui.CollapsingHeader("Auto Designs")) - return; - - foreach (var (set, idx) in _autoDesignManager.WithIndex()) - { - using var id = ImRaii.PushId(idx); - using var tree = ImRaii.TreeNode(set.Name); - if (!tree) - continue; - - using var table = ImRaii.Table("##autoDesign", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (!table) - continue; - - ImGuiUtil.DrawTableColumn("Name"); - ImGuiUtil.DrawTableColumn(set.Name); - - ImGuiUtil.DrawTableColumn("Index"); - ImGuiUtil.DrawTableColumn(idx.ToString()); - - ImGuiUtil.DrawTableColumn("Enabled"); - ImGuiUtil.DrawTableColumn(set.Enabled.ToString()); - - ImGuiUtil.DrawTableColumn("Actor"); - ImGuiUtil.DrawTableColumn(set.Identifiers[0].ToString()); - - foreach (var (design, designIdx) in set.Designs.WithIndex()) - { - ImGuiUtil.DrawTableColumn($"{design.Name(false)} ({designIdx})"); - ImGuiUtil.DrawTableColumn($"{design.ApplicationType} {design.Jobs.Name}"); - } - } - } - - #endregion - - #region Unlocks - - private void DrawUnlocks() - { - if (!ImGui.CollapsingHeader("Unlocks")) - return; - - DrawCustomizationUnlocks(); - DrawItemUnlocks(); - DrawUnlockableItems(); - } - - private void DrawCustomizationUnlocks() - { - using var tree = ImRaii.TreeNode("Customization"); - if (!tree) - return; - - - using var table = ImRaii.Table("customizationUnlocks", 6, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter, - new Vector2(ImGui.GetContentRegionAvail().X, 12 * ImGui.GetTextLineHeight())); - if (!table) - return; - - ImGui.TableNextColumn(); - var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); - ImGui.TableNextRow(); - var remainder = ImGuiClip.ClippedDraw(_customizeUnlocks.Unlockable, skips, t => - { - ImGuiUtil.DrawTableColumn(t.Key.Index.ToDefaultName()); - ImGuiUtil.DrawTableColumn(t.Key.CustomizeId.ToString()); - ImGuiUtil.DrawTableColumn(t.Key.Value.Value.ToString()); - ImGuiUtil.DrawTableColumn(t.Value.Data.ToString()); - ImGuiUtil.DrawTableColumn(t.Value.Name); - ImGuiUtil.DrawTableColumn(_customizeUnlocks.IsUnlocked(t.Key, out var time) - ? time == DateTimeOffset.MinValue - ? "Always" - : time.LocalDateTime.ToString("g") - : "Never"); - }, _customizeUnlocks.Unlockable.Count); - ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight()); - } - - private void DrawItemUnlocks() - { - using var tree = ImRaii.TreeNode("Unlocked Items"); - if (!tree) - return; - - using var table = ImRaii.Table("itemUnlocks", 5, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter, - new Vector2(ImGui.GetContentRegionAvail().X, 12 * ImGui.GetTextLineHeight())); - if (!table) - return; - - ImGui.TableSetupColumn("ItemId", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, 400 * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Slot", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Model", ImGuiTableColumnFlags.WidthFixed, 80 * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Unlock", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale); - - ImGui.TableNextColumn(); - var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); - ImGui.TableNextRow(); - var remainder = ImGuiClip.ClippedDraw(_itemUnlocks, skips, t => - { - ImGuiUtil.DrawTableColumn(t.Key.ToString()); - if (_items.ItemService.AwaitedService.TryGetValue(t.Key, EquipSlot.MainHand, out var equip)) - { - ImGuiUtil.DrawTableColumn(equip.Name); - ImGuiUtil.DrawTableColumn(equip.Type.ToName()); - ImGuiUtil.DrawTableColumn(equip.Weapon().ToString()); - } - else - { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - } - - ImGuiUtil.DrawTableColumn(_itemUnlocks.IsUnlocked(t.Key, out var time) - ? time == DateTimeOffset.MinValue - ? "Always" - : time.LocalDateTime.ToString("g") - : "Never"); - }, _itemUnlocks.Count); - ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight()); - } - - private void DrawUnlockableItems() - { - using var tree = ImRaii.TreeNode("Unlockable Items"); - if (!tree) - return; - - using var table = ImRaii.Table("unlockableItem", 6, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter, - new Vector2(ImGui.GetContentRegionAvail().X, 12 * ImGui.GetTextLineHeight())); - if (!table) - return; - - ImGui.TableSetupColumn("ItemId", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, 400 * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Slot", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Model", ImGuiTableColumnFlags.WidthFixed, 80 * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Unlock", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Criteria", ImGuiTableColumnFlags.WidthStretch); - - ImGui.TableNextColumn(); - var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); - ImGui.TableNextRow(); - var remainder = ImGuiClip.ClippedDraw(_itemUnlocks.Unlockable, skips, t => - { - ImGuiUtil.DrawTableColumn(t.Key.ToString()); - if (_items.ItemService.AwaitedService.TryGetValue(t.Key, EquipSlot.MainHand, out var equip)) - { - ImGuiUtil.DrawTableColumn(equip.Name); - ImGuiUtil.DrawTableColumn(equip.Type.ToName()); - ImGuiUtil.DrawTableColumn(equip.Weapon().ToString()); - } - else - { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - } - - ImGuiUtil.DrawTableColumn(_itemUnlocks.IsUnlocked(t.Key, out var time) - ? time == DateTimeOffset.MinValue - ? "Always" - : time.LocalDateTime.ToString("g") - : "Never"); - ImGuiUtil.DrawTableColumn(t.Value.ToString()); - }, _itemUnlocks.Unlockable.Count); - ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight()); - } - - #endregion - - #region Inventory - - private void DrawInventory() - { - if (!ImGui.CollapsingHeader("Inventory")) - return; - - var inventory = InventoryManager.Instance(); - if (inventory == null) - return; - - ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)inventory:X}"); - - var equip = inventory->GetInventoryContainer(InventoryType.EquippedItems); - if (equip == null || equip->Loaded == 0) - return; - - ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)equip:X}"); - - using var table = ImRaii.Table("items", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - for (var i = 0; i < equip->Size; ++i) - { - ImGuiUtil.DrawTableColumn(i.ToString()); - var item = equip->GetInventorySlot(i); - if (item == null) - { - ImGuiUtil.DrawTableColumn("NULL"); - ImGui.TableNextRow(); - } - else - { - ImGuiUtil.DrawTableColumn(item->ItemID.ToString()); - ImGuiUtil.DrawTableColumn(item->GlamourID.ToString()); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)item:X}"); - } - } - } - - #endregion - - #region Fun - - private void DrawFun() - { - if (!ImGui.CollapsingHeader("Fun Module")) - return; - - ImGui.TextUnformatted($"Current Festival: {_funModule.CurrentFestival}"); - ImGui.TextUnformatted($"Festivals Enabled: {_config.DisableFestivals switch { 1 => "Undecided", 0 => "Enabled", _ => "Disabled" }}"); - ImGui.TextUnformatted($"Popup Open: {ImGui.IsPopupOpen("FestivalPopup", ImGuiPopupFlags.AnyPopup)}"); - if (ImGui.Button("Force Christmas")) - _funModule.ForceFestival(FunModule.FestivalType.Christmas); - if (ImGui.Button("Force Halloween")) - _funModule.ForceFestival(FunModule.FestivalType.Halloween); - if (ImGui.Button("Force April First")) - _funModule.ForceFestival(FunModule.FestivalType.AprilFirst); - if (ImGui.Button("Force None")) - _funModule.ForceFestival(FunModule.FestivalType.None); - if (ImGui.Button("Revert")) - _funModule.ResetFestival(); - if (ImGui.Button("Reset Popup")) - { - _config.DisableFestivals = 1; - _config.Save(); - } - } - - #endregion - - #region IPC - - private string _gameObjectName = string.Empty; - private string _base64Apply = string.Empty; - - private void DrawIpc() - { - if (!ImGui.CollapsingHeader("IPC Tester")) - return; - - ImGui.InputInt("Game Object Index", ref _gameObjectIndex, 0, 0); - ImGui.InputTextWithHint("##gameObject", "Character Name...", ref _gameObjectName, 64); - ImGui.InputTextWithHint("##base64", "Design Base64...", ref _base64Apply, 2047); - using var table = ImRaii.Table("##ipc", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (!table) - return; - - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApiVersions); - var (major, minor) = GlamourerIpc.ApiVersionsSubscriber(_pluginInterface).Invoke(); - ImGuiUtil.DrawTableColumn($"({major}, {minor})"); - - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelGetAllCustomization); - ImGui.TableNextColumn(); - var base64 = GlamourerIpc.GetAllCustomizationSubscriber(_pluginInterface).Invoke(_gameObjectName); - if (base64 != null) - ImGuiUtil.CopyOnClickSelectable(base64); - else - ImGui.TextUnformatted("Error"); - - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelGetAllCustomizationFromCharacter); - ImGui.TableNextColumn(); - base64 = GlamourerIpc.GetAllCustomizationFromCharacterSubscriber(_pluginInterface) - .Invoke(_objectManager.Objects[_gameObjectIndex] as Character); - if (base64 != null) - ImGuiUtil.CopyOnClickSelectable(base64); - else - ImGui.TextUnformatted("Error"); - - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelRevert); - ImGui.TableNextColumn(); - if (ImGui.Button("Revert##Name")) - GlamourerIpc.RevertSubscriber(_pluginInterface).Invoke(_gameObjectName); - - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelRevertCharacter); - ImGui.TableNextColumn(); - if (ImGui.Button("Revert##Character")) - GlamourerIpc.RevertCharacterSubscriber(_pluginInterface).Invoke(_objectManager.Objects[_gameObjectIndex] as Character); - - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyAll); - ImGui.TableNextColumn(); - if (ImGui.Button("Apply##AllName")) - GlamourerIpc.ApplyAllSubscriber(_pluginInterface).Invoke(_base64Apply, _gameObjectName); - - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyAllToCharacter); - ImGui.TableNextColumn(); - if (ImGui.Button("Apply##AllCharacter")) - GlamourerIpc.ApplyAllToCharacterSubscriber(_pluginInterface) - .Invoke(_base64Apply, _objectManager.Objects[_gameObjectIndex] as Character); - - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyOnlyEquipment); - ImGui.TableNextColumn(); - if (ImGui.Button("Apply##EquipName")) - GlamourerIpc.ApplyOnlyEquipmentSubscriber(_pluginInterface).Invoke(_base64Apply, _gameObjectName); - - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyOnlyEquipmentToCharacter); - ImGui.TableNextColumn(); - if (ImGui.Button("Apply##EquipCharacter")) - GlamourerIpc.ApplyOnlyEquipmentToCharacterSubscriber(_pluginInterface) - .Invoke(_base64Apply, _objectManager.Objects[_gameObjectIndex] as Character); - - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyOnlyCustomization); - ImGui.TableNextColumn(); - if (ImGui.Button("Apply##CustomizeName")) - GlamourerIpc.ApplyOnlyCustomizationSubscriber(_pluginInterface).Invoke(_base64Apply, _gameObjectName); - - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyOnlyCustomizationToCharacter); - ImGui.TableNextColumn(); - if (ImGui.Button("Apply##CustomizeCharacter")) - GlamourerIpc.ApplyOnlyCustomizationToCharacterSubscriber(_pluginInterface) - .Invoke(_base64Apply, _objectManager.Objects[_gameObjectIndex] as Character); - - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelUnlock); - ImGui.TableNextColumn(); - if (ImGui.Button("Unlock##CustomizeCharacter")) - GlamourerIpc.UnlockSubscriber(_pluginInterface) - .Invoke(_objectManager.Objects[_gameObjectIndex] as Character, 1337); - - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelRevertToAutomation); - ImGui.TableNextColumn(); - if (ImGui.Button("Revert##CustomizeCharacter")) - GlamourerIpc.RevertToAutomationCharacterSubscriber(_pluginInterface) - .Invoke(_objectManager.Objects[_gameObjectIndex] as Character, 1337); - } - - #endregion -} diff --git a/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs new file mode 100644 index 0000000..35642a7 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs @@ -0,0 +1,138 @@ +using Dalamud.Interface; +using Glamourer.GameData; +using Glamourer.Designs; +using Glamourer.State; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Interop; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class ActiveStatePanel(StateManager _stateManager, ActorObjectManager _objectManager) : IGameDataDrawer +{ + public string Label + => $"Active Actors ({_stateManager.Count})###Active Actors"; + + public bool Disabled + => false; + + public void Draw() + { + foreach (var (identifier, actors) in _objectManager) + { + if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Trash.ToIconString()}##{actors.Label}", new Vector2(ImGui.GetFrameHeight()), + string.Empty, !_stateManager.ContainsKey(identifier), true)) + _stateManager.DeleteState(identifier); + + ImGui.SameLine(); + using var t = ImRaii.TreeNode(actors.Label); + if (!t) + continue; + + if (_stateManager.GetOrCreate(identifier, actors.Objects[0], out var state)) + DrawState(_stateManager, actors, state); + else + ImGui.TextUnformatted("Invalid actor."); + } + } + + public static void DrawState(StateManager stateManager, ActorData data, ActorState state) + { + using var table = ImRaii.Table("##state", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImGuiUtil.DrawTableColumn("Name"); + ImGuiUtil.DrawTableColumn(state.Identifier.ToString()); + ImGui.TableNextColumn(); + if (ImGui.Button("Reset")) + stateManager.ResetState(state, StateSource.Manual); + + ImGui.TableNextRow(); + + static void PrintRow(string label, T actor, T model, StateSource source) where T : notnull + { + ImGuiUtil.DrawTableColumn(label); + ImGuiUtil.DrawTableColumn(actor.ToString()!); + ImGuiUtil.DrawTableColumn(model.ToString()!); + ImGuiUtil.DrawTableColumn(source.ToString()); + } + + static string ItemString(in DesignData data, EquipSlot slot) + { + var item = data.Item(slot); + return + $"{item.Name} ({item.Id.ToDiscriminatingString()} {item.PrimaryId.Id}{(item.SecondaryId != 0 ? $"-{item.SecondaryId.Id}" : string.Empty)}-{item.Variant})"; + } + + static string BonusItemString(in DesignData data, BonusItemFlag slot) + { + var item = data.BonusItem(slot); + return + $"{item.Name} ({item.Id.ToDiscriminatingString()} {item.PrimaryId.Id}{(item.SecondaryId != 0 ? $"-{item.SecondaryId.Id}" : string.Empty)}-{item.Variant})"; + } + + PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state.Sources[MetaIndex.ModelId]); + ImGui.TableNextRow(); + PrintRow("Wetness", state.BaseData.IsWet(), state.ModelData.IsWet(), state.Sources[MetaIndex.Wetness]); + ImGui.TableNextRow(); + + if (state.BaseData.IsHuman && state.ModelData.IsHuman) + { + PrintRow("Hat Visible", state.BaseData.IsHatVisible(), state.ModelData.IsHatVisible(), state.Sources[MetaIndex.HatState]); + ImGui.TableNextRow(); + PrintRow("Visor Toggled", state.BaseData.IsVisorToggled(), state.ModelData.IsVisorToggled(), + state.Sources[MetaIndex.VisorState]); + ImGui.TableNextRow(); + PrintRow("Viera Ears Visible", state.BaseData.AreEarsVisible(), state.ModelData.AreEarsVisible(), + state.Sources[MetaIndex.EarState]); + ImGui.TableNextRow(); + PrintRow("Weapon Visible", state.BaseData.IsWeaponVisible(), state.ModelData.IsWeaponVisible(), + state.Sources[MetaIndex.WeaponState]); + ImGui.TableNextRow(); + foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) + { + PrintRow(slot.ToName(), ItemString(state.BaseData, slot), ItemString(state.ModelData, slot), state.Sources[slot, false]); + ImGuiUtil.DrawTableColumn(state.BaseData.Stain(slot).ToString()); + ImGuiUtil.DrawTableColumn(state.ModelData.Stain(slot).ToString()); + ImGuiUtil.DrawTableColumn(state.Sources[slot, true].ToString()); + } + + foreach (var slot in BonusExtensions.AllFlags) + { + PrintRow(slot.ToName(), BonusItemString(state.BaseData, slot), BonusItemString(state.ModelData, slot), state.Sources[slot]); + ImGui.TableNextRow(); + } + + foreach (var type in Enum.GetValues()) + { + PrintRow(type.ToDefaultName(), state.BaseData.Customize[type].Value, state.ModelData.Customize[type].Value, + state.Sources[type]); + ImGui.TableNextRow(); + } + + foreach (var crest in CrestExtensions.AllRelevantSet) + { + PrintRow(crest.ToLabel(), state.BaseData.Crest(crest), state.ModelData.Crest(crest), state.Sources[crest]); + ImGui.TableNextRow(); + } + + foreach (var flag in CustomizeParameterExtensions.AllFlags) + { + PrintRow(flag.ToString(), state.BaseData.Parameters[flag], state.ModelData.Parameters[flag], state.Sources[flag]); + ImGui.TableNextRow(); + } + } + else + { + ImGuiUtil.DrawTableColumn(string.Join(" ", state.BaseData.GetCustomizeBytes().Select(b => b.ToString("X2")))); + ImGuiUtil.DrawTableColumn(string.Join(" ", state.ModelData.GetCustomizeBytes().Select(b => b.ToString("X2")))); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn(string.Join(" ", state.BaseData.GetEquipmentBytes().Select(b => b.ToString("X2")))); + ImGuiUtil.DrawTableColumn(string.Join(" ", state.ModelData.GetEquipmentBytes().Select(b => b.ToString("X2")))); + } + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/AdvancedCustomizationDrawer.cs b/Glamourer/Gui/Tabs/DebugTab/AdvancedCustomizationDrawer.cs new file mode 100644 index 0000000..2202ceb --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/AdvancedCustomizationDrawer.cs @@ -0,0 +1,135 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using Dalamud.Bindings.ImGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Interop; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public unsafe class AdvancedCustomizationDrawer(ActorObjectManager objects) : IGameDataDrawer +{ + public string Label + => "Advanced Customizations"; + + public bool Disabled + => false; + + public void Draw() + { + var (player, data) = objects.PlayerData; + if (!data.Valid) + { + ImUtf8.Text("Invalid player."u8); + return; + } + + var model = data.Objects[0].Model; + if (!model.IsHuman) + { + ImUtf8.Text("Invalid model."u8); + return; + } + + DrawCBuffer("Customize"u8, model.AsHuman->CustomizeParameterCBuffer, 0); + DrawCBuffer("Decal"u8, model.AsHuman->DecalColorCBuffer, 1); + DrawCBuffer("Unk1"u8, *(ConstantBuffer**)((byte*)model.AsHuman + 0xBA0), 2); + DrawCBuffer("Unk2"u8, *(ConstantBuffer**)((byte*)model.AsHuman + 0xBA8), 3); + } + + + private static void DrawCBuffer(ReadOnlySpan label, ConstantBuffer* cBuffer, int type) + { + using var tree = ImUtf8.TreeNode(label); + if (!tree) + return; + + if (cBuffer == null) + { + ImUtf8.Text("Invalid CBuffer."u8); + return; + } + + ImUtf8.Text($"{cBuffer->ByteSize / 4}"); + ImUtf8.Text($"{cBuffer->Flags}"); + ImUtf8.Text($"0x{(ulong)cBuffer:X}"); + var parameters = (float*)cBuffer->UnsafeSourcePointer; + if (parameters == null) + { + ImUtf8.Text("No Parameters."u8); + return; + } + + var start = parameters; + using (ImUtf8.Group()) + { + for (var end = start + cBuffer->ByteSize / 4; parameters < end; parameters += 2) + DrawParameters(parameters, type, (int)(parameters - start)); + } + + ImGui.SameLine(0, 50 * ImUtf8.GlobalScale); + parameters = start + 1; + using (ImUtf8.Group()) + { + for (var end = start + cBuffer->ByteSize / 4; parameters < end; parameters += 2) + DrawParameters(parameters, type, (int)(parameters - start)); + } + } + + private static void DrawParameters(float* param, int type, int idx) + { + using var id = ImUtf8.PushId((nint)param); + ImUtf8.TextFrameAligned($"{idx:D2}: "); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(200 * ImUtf8.GlobalScale); + if (TryGetKnown(type, idx, out var known)) + { + ImUtf8.DragScalar(known, ref *param, float.MinValue, float.MaxValue, 0.01f); + } + else + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)); + ImUtf8.DragScalar($"+0x{idx * 4:X2}", ref *param, float.MinValue, float.MaxValue, 0.01f); + } + } + + private static bool TryGetKnown(int type, int idx, out ReadOnlySpan text) + { + if (type == 0) + text = idx switch + { + 0 => "Diffuse.R"u8, + 1 => "Diffuse.G"u8, + 2 => "Diffuse.B"u8, + 3 => "Muscle Tone"u8, + 8 => "Lipstick.R"u8, + 9 => "Lipstick.G"u8, + 10 => "Lipstick.B"u8, + 11 => "Lipstick.Opacity"u8, + 12 => "Hair.R"u8, + 13 => "Hair.G"u8, + 14 => "Hair.B"u8, + 15 => "Facepaint.Offset"u8, + 20 => "Highlight.R"u8, + 21 => "Highlight.G"u8, + 22 => "Highlight.B"u8, + 23 => "Facepaint.Multiplier"u8, + 24 => "LeftEye.R"u8, + 25 => "LeftEye.G"u8, + 26 => "LeftEye.B"u8, + 27 => "LeftLimbal"u8, + 28 => "RightEye.R"u8, + 29 => "RightEye.G"u8, + 30 => "RightEye.B"u8, + 31 => "RightLimbal"u8, + 32 => "Feature.R"u8, + 33 => "Feature.G"u8, + 34 => "Feature.B"u8, + _ => [], + }; + else + text = []; + + return text.Length > 0; + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs b/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs new file mode 100644 index 0000000..aee59b6 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs @@ -0,0 +1,50 @@ +using Glamourer.Automation; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Extensions; +using OtterGui.Raii; +using Penumbra.GameData.Gui.Debug; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class AutoDesignPanel(AutoDesignManager _autoDesignManager) : IGameDataDrawer +{ + public string Label + => "Auto Designs"; + + public bool Disabled + => false; + + public void Draw() + { + foreach (var (set, idx) in _autoDesignManager.WithIndex()) + { + using var id = ImRaii.PushId(idx); + using var tree = ImRaii.TreeNode(set.Name); + if (!tree) + continue; + + using var table = ImRaii.Table("##autoDesign", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + continue; + + ImGuiUtil.DrawTableColumn("Name"); + ImGuiUtil.DrawTableColumn(set.Name); + + ImGuiUtil.DrawTableColumn("Index"); + ImGuiUtil.DrawTableColumn(idx.ToString()); + + ImGuiUtil.DrawTableColumn("Enabled"); + ImGuiUtil.DrawTableColumn(set.Enabled.ToString()); + + ImGuiUtil.DrawTableColumn("Actor"); + ImGuiUtil.DrawTableColumn(set.Identifiers[0].ToString()); + + foreach (var (design, designIdx) in set.Designs.WithIndex()) + { + ImGuiUtil.DrawTableColumn($"{design.Design.ResolveName(false)} ({designIdx})"); + ImGuiUtil.DrawTableColumn($"{design.Type} {design.Jobs.Name}"); + } + } + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs b/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs new file mode 100644 index 0000000..6c0995c --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs @@ -0,0 +1,137 @@ +using Dalamud.Interface; +using Glamourer.GameData; +using Glamourer.Services; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class CustomizationServicePanel(CustomizeService customize) : IGameDataDrawer +{ + public string Label + => "Customization Service"; + + public bool Disabled + => !customize.Finished; + + public void Draw() + { + foreach (var (clan, gender) in CustomizeManager.AllSets()) + { + var set = customize.Manager.GetSet(clan, gender); + DrawCustomizationInfo(set); + DrawNpcCustomizationInfo(set); + } + + DrawFacepaintInfo(); + DrawColorInfo(); + } + + private void DrawFacepaintInfo() + { + using var tree = ImUtf8.TreeNode("NPC Facepaints"u8); + if (!tree) + return; + + using var table = ImUtf8.Table("data"u8, 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Id"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Facepaint"u8); + + for (var i = 0; i < 128; ++i) + { + var index = new CustomizeValue((byte)i); + ImUtf8.DrawTableColumn($"{i:D3}"); + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImUtf8.DrawTableColumn(customize.NpcCustomizeSet.CheckValue(CustomizeIndex.FacePaint, index) + ? FontAwesomeIcon.Check.ToIconString() + : FontAwesomeIcon.Times.ToIconString()); + } + } + private void DrawColorInfo() + { + using var tree = ImUtf8.TreeNode("NPC Colors"u8); + if (!tree) + return; + + using var table = ImUtf8.Table("data"u8, 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Id"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Hair"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Eyes"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Facepaint"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Tattoos"u8); + + for (var i = 192; i < 256; ++i) + { + var index = new CustomizeValue((byte)i); + ImUtf8.DrawTableColumn($"{i:D3}"); + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImUtf8.DrawTableColumn(customize.NpcCustomizeSet.CheckValue(CustomizeIndex.HairColor, index) + ? FontAwesomeIcon.Check.ToIconString() + : FontAwesomeIcon.Times.ToIconString()); + ImUtf8.DrawTableColumn(customize.NpcCustomizeSet.CheckValue(CustomizeIndex.EyeColorLeft, index) + ? FontAwesomeIcon.Check.ToIconString() + : FontAwesomeIcon.Times.ToIconString()); + ImUtf8.DrawTableColumn(customize.NpcCustomizeSet.CheckValue(CustomizeIndex.FacePaintColor, index) + ? FontAwesomeIcon.Check.ToIconString() + : FontAwesomeIcon.Times.ToIconString()); + ImUtf8.DrawTableColumn(customize.NpcCustomizeSet.CheckValue(CustomizeIndex.TattooColor, index) + ? FontAwesomeIcon.Check.ToIconString() + : FontAwesomeIcon.Times.ToIconString()); + } + } + + private void DrawCustomizationInfo(CustomizeSet set) + { + using var tree = ImRaii.TreeNode($"{customize.ClanName(set.Clan, set.Gender)} {set.Gender}"); + if (!tree) + return; + + using var table = ImRaii.Table("data", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + foreach (var index in Enum.GetValues()) + { + ImGuiUtil.DrawTableColumn(index.ToString()); + ImGuiUtil.DrawTableColumn(set.Option(index)); + ImGuiUtil.DrawTableColumn(set.IsAvailable(index) ? "Available" : "Unavailable"); + ImGuiUtil.DrawTableColumn(set.Type(index).ToString()); + ImGuiUtil.DrawTableColumn(set.Count(index).ToString()); + } + } + + private void DrawNpcCustomizationInfo(CustomizeSet set) + { + using var tree = ImRaii.TreeNode($"{customize.ClanName(set.Clan, set.Gender)} {set.Gender} (NPC Options)"); + if (!tree) + return; + + using var table = ImRaii.Table("npc", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + foreach (var (index, value) in set.NpcOptions) + { + ImGuiUtil.DrawTableColumn(index.ToString()); + ImGuiUtil.DrawTableColumn(value.Value.ToString()); + } + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/CustomizationUnlockPanel.cs b/Glamourer/Gui/Tabs/DebugTab/CustomizationUnlockPanel.cs new file mode 100644 index 0000000..4bf7d7b --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/CustomizationUnlockPanel.cs @@ -0,0 +1,44 @@ +using Glamourer.Unlocks; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class CustomizationUnlockPanel(CustomizeUnlockManager _customizeUnlocks) : IGameDataDrawer +{ + public string Label + => "Customizations"; + + public bool Disabled + => false; + + public void Draw() + { + using var table = ImRaii.Table("customizationUnlocks", 6, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter, + new Vector2(ImGui.GetContentRegionAvail().X, 12 * ImGui.GetTextLineHeight())); + if (!table) + return; + + ImGui.TableNextColumn(); + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); + ImGui.TableNextRow(); + var remainder = ImGuiClip.ClippedDraw(_customizeUnlocks.Unlockable, skips, t => + { + ImGuiUtil.DrawTableColumn(t.Key.Index.ToDefaultName()); + ImGuiUtil.DrawTableColumn(t.Key.CustomizeId.ToString()); + ImGuiUtil.DrawTableColumn(t.Key.Value.Value.ToString()); + ImGuiUtil.DrawTableColumn(t.Value.Data.ToString()); + ImGuiUtil.DrawTableColumn(t.Value.Name); + ImGuiUtil.DrawTableColumn(_customizeUnlocks.IsUnlocked(t.Key, out var time) + ? time == DateTimeOffset.MinValue + ? "Always" + : time.LocalDateTime.ToString("g") + : "Never"); + }, _customizeUnlocks.Unlockable.Count); + ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight()); + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs b/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs new file mode 100644 index 0000000..7c61392 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs @@ -0,0 +1,40 @@ +using Glamourer.Interop; +using Dalamud.Bindings.ImGui; +using OtterGui; +using Penumbra.GameData.Files; +using Penumbra.GameData.Gui.Debug; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class DatFilePanel(ImportService _importService) : IGameDataDrawer +{ + public string Label + => "Character Dat File"; + + public bool Disabled + => false; + + private string _datFilePath = string.Empty; + private DatCharacterFile? _datFile = null; + + public void Draw() + { + ImGui.InputTextWithHint("##datFilePath", "Dat File Path...", ref _datFilePath, 256); + var exists = _datFilePath.Length > 0 && File.Exists(_datFilePath); + if (ImGuiUtil.DrawDisabledButton("Load##Dat", Vector2.Zero, string.Empty, !exists)) + _datFile = _importService.LoadDat(_datFilePath, out var tmp) ? tmp : null; + + if (ImGuiUtil.DrawDisabledButton("Save##Dat", Vector2.Zero, string.Empty, _datFilePath.Length == 0 || _datFile == null)) + _importService.SaveDesignAsDat(_datFilePath, _datFile!.Value.Customize, _datFile!.Value.Description); + + if (_datFile != null) + { + ImGui.TextUnformatted(_datFile.Value.Magic.ToString()); + ImGui.TextUnformatted(_datFile.Value.Version.ToString()); + ImGui.TextUnformatted(_datFile.Value.Time.LocalDateTime.ToString("g")); + ImGui.TextUnformatted(_datFile.Value.Voice.ToString()); + ImGui.TextUnformatted(_datFile.Value.Customize.ToString()); + ImGui.TextUnformatted(_datFile.Value.Description); + } + } +} \ No newline at end of file diff --git a/Glamourer/Gui/Tabs/DebugTab/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab/DebugTab.cs new file mode 100644 index 0000000..b760221 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/DebugTab.cs @@ -0,0 +1,41 @@ +using Dalamud.Bindings.ImGui; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Widgets; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public unsafe class DebugTab(ServiceManager manager) : ITab +{ + private readonly Configuration _config = manager.GetService(); + + public bool IsVisible + => _config.DebugMode; + + public ReadOnlySpan Label + => "Debug"u8; + + private readonly DebugTabHeader[] _headers = + [ + DebugTabHeader.CreateInterop(manager.Provider!), + DebugTabHeader.CreateGameData(manager.Provider!), + DebugTabHeader.CreateDesigns(manager.Provider!), + DebugTabHeader.CreateState(manager.Provider!), + DebugTabHeader.CreateUnlocks(manager.Provider!), + ]; + + public void DrawContent() + { + using var child = ImRaii.Child("MainWindowChild"); + if (!child) + return; + + if (ImGui.CollapsingHeader("General")) + { + manager.Timers.Draw("Timers"); + } + + foreach (var header in _headers) + header.Draw(); + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs b/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs new file mode 100644 index 0000000..3df425f --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs @@ -0,0 +1,89 @@ +using Glamourer.Gui.Tabs.DebugTab.IpcTester; +using Microsoft.Extensions.DependencyInjection; +using OtterGui.Raii; +using Penumbra.GameData.Gui.Debug; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class DebugTabHeader(string label, params IGameDataDrawer[] subTrees) +{ + public string Label { get; } = label; + public IReadOnlyList SubTrees { get; } = subTrees; + + public void Draw() + { + using var h = ImRaii.CollapsingHeader(Label); + if (!h) + return; + + foreach (var subTree in SubTrees) + { + using var disabled = ImRaii.Disabled(subTree.Disabled); + using var tree = ImRaii.TreeNode(subTree.Label); + if (tree) + { + disabled.Dispose(); + subTree.Draw(); + } + } + } + + public static DebugTabHeader CreateInterop(IServiceProvider provider) + => new + ( + "Interop", + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService() + ); + + public static DebugTabHeader CreateGameData(IServiceProvider provider) + => new + ( + "Game Data", + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService() + ); + + public static DebugTabHeader CreateDesigns(IServiceProvider provider) + => new + ( + "Designs", + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService() + ); + + public static DebugTabHeader CreateState(IServiceProvider provider) + => new + ( + "State", + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService() + ); + + public static DebugTabHeader CreateUnlocks(IServiceProvider provider) + => new + ( + "Unlocks", + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService() + ); +} diff --git a/Glamourer/Gui/Tabs/DebugTab/DesignConverterPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DesignConverterPanel.cs new file mode 100644 index 0000000..287d373 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/DesignConverterPanel.cs @@ -0,0 +1,97 @@ +using Dalamud.Interface; +using Glamourer.Designs; +using Glamourer.Utility; +using Dalamud.Bindings.ImGui; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Gui.Debug; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class DesignConverterPanel(DesignConverter _designConverter) : IGameDataDrawer +{ + public string Label + => "Design Converter"; + + public bool Disabled + => false; + + private string _clipboardText = string.Empty; + private byte[] _clipboardData = []; + private byte[] _dataUncompressed = []; + private byte _version = 0; + private string _textUncompressed = string.Empty; + private JObject? _json = null; + private DesignBase? _tmpDesign = null; + private Exception? _clipboardProblem = null; + + public void Draw() + { + if (ImGui.Button("Import Clipboard")) + { + _clipboardText = string.Empty; + _clipboardData = []; + _dataUncompressed = []; + _textUncompressed = string.Empty; + _json = null; + _tmpDesign = null; + _clipboardProblem = null; + + try + { + _clipboardText = ImGui.GetClipboardText(); + _clipboardData = Convert.FromBase64String(_clipboardText); + _version = _clipboardData[0]; + if (_version == 5) + _clipboardData = _clipboardData[DesignBase64Migration.Base64SizeV4..]; + _version = _clipboardData.Decompress(out _dataUncompressed); + _textUncompressed = Encoding.UTF8.GetString(_dataUncompressed); + _json = JObject.Parse(_textUncompressed); + _tmpDesign = _designConverter.FromBase64(_clipboardText, true, true, out _); + } + catch (Exception ex) + { + _clipboardProblem = ex; + } + } + + if (_clipboardText.Length > 0) + { + using var f = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtil.TextWrapped(_clipboardText); + } + + if (_clipboardData.Length > 0) + { + using var f = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtil.TextWrapped(string.Join(" ", _clipboardData.Select(b => b.ToString("X2")))); + } + + if (_dataUncompressed.Length > 0) + { + using var f = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtil.TextWrapped(string.Join(" ", _dataUncompressed.Select(b => b.ToString("X2")))); + } + + if (_textUncompressed.Length > 0) + { + using var f = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtil.TextWrapped(_textUncompressed); + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(_textUncompressed); + } + + if (_json != null) + ImGui.TextUnformatted("JSON Parsing Successful!"); + + if (_tmpDesign != null) + DesignManagerPanel.DrawDesign(_tmpDesign, null); + + if (_clipboardProblem != null) + { + using var f = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtil.TextWrapped(_clipboardProblem.ToString()); + } + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs new file mode 100644 index 0000000..7c60dda --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs @@ -0,0 +1,134 @@ +using Dalamud.Interface; +using Glamourer.Designs; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Extensions; +using OtterGui.Filesystem; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _designFileSystem) : IGameDataDrawer +{ + public string Label + => $"Design Manager ({_designManager.Designs.Count} Designs)###Design Manager"; + + public bool Disabled + => false; + + public void Draw() + { + DrawButtons(); + foreach (var (design, idx) in _designManager.Designs.WithIndex()) + { + using var t = ImRaii.TreeNode($"{design.Name}##{idx}"); + if (!t) + continue; + + DrawDesign(design, _designFileSystem); + var base64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.Application.Equip, design.Application.Customize, + design.Application.Meta, + design.WriteProtected()); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtil.TextWrapped(base64); + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(base64); + } + } + + private void DrawButtons() + { + if (ImUtf8.Button("Generate 500 Test Designs"u8)) + for (var i = 0; i < 500; ++i) + { + var design = _designManager.CreateEmpty($"Test Designs/Test Design {i}", true); + _designManager.AddTag(design, "_DebugTest"); + } + + ImUtf8.SameLineInner(); + if (ImUtf8.Button("Remove All Test Designs"u8)) + { + var designs = _designManager.Designs.Where(d => d.Tags.Contains("_DebugTest")).ToArray(); + foreach (var design in designs) + _designManager.Delete(design); + if (_designFileSystem.Find("Test Designs", out var path) && path is DesignFileSystem.Folder { TotalChildren: 0 }) + _designFileSystem.Delete(path); + } + } + + public static void DrawDesign(DesignBase design, DesignFileSystem? fileSystem) + { + using var table = ImRaii.Table("##equip", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (design is Design d) + { + ImGuiUtil.DrawTableColumn("Name"); + ImGuiUtil.DrawTableColumn(d.Name); + ImGuiUtil.DrawTableColumn($"({d.Index})"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted("Description (Hover)"); + ImGuiUtil.HoverTooltip(d.Description); + ImGui.TableNextRow(); + + ImGuiUtil.DrawTableColumn("Identifier"); + ImGuiUtil.DrawTableColumn(d.Identifier.ToString()); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Design File System Path"); + if (fileSystem != null) + ImGuiUtil.DrawTableColumn(fileSystem.TryGetValue(d, out var leaf) ? leaf.FullName() : "No Path Known"); + ImGui.TableNextRow(); + + ImGuiUtil.DrawTableColumn("Creation"); + ImGuiUtil.DrawTableColumn(d.CreationDate.ToString()); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Update"); + ImGuiUtil.DrawTableColumn(d.LastEdit.ToString()); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Tags"); + ImGuiUtil.DrawTableColumn(string.Join(", ", d.Tags)); + ImGui.TableNextRow(); + } + + foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) + { + var item = design.DesignData.Item(slot); + var apply = design.DoApplyEquip(slot); + var stain = design.DesignData.Stain(slot); + var applyStain = design.DoApplyStain(slot); + var crest = design.DesignData.Crest(slot.ToCrestFlag()); + var applyCrest = design.DoApplyCrest(slot.ToCrestFlag()); + ImGuiUtil.DrawTableColumn(slot.ToName()); + ImGuiUtil.DrawTableColumn(item.Name); + ImGuiUtil.DrawTableColumn(item.ItemId.ToString()); + ImGuiUtil.DrawTableColumn(apply ? "Apply" : "Keep"); + ImGuiUtil.DrawTableColumn(stain.ToString()); + ImGuiUtil.DrawTableColumn(applyStain ? "Apply" : "Keep"); + ImGuiUtil.DrawTableColumn(crest.ToString()); + ImGuiUtil.DrawTableColumn(applyCrest ? "Apply" : "Keep"); + } + + foreach (var index in MetaExtensions.AllRelevant) + { + ImGuiUtil.DrawTableColumn(index.ToName()); + ImGuiUtil.DrawTableColumn(design.DesignData.GetMeta(index).ToString()); + ImGuiUtil.DrawTableColumn(design.DoApplyMeta(index) ? "Apply" : "Keep"); + ImGui.TableNextRow(); + } + + ImGuiUtil.DrawTableColumn("Model ID"); + ImGuiUtil.DrawTableColumn(design.DesignData.ModelId.ToString()); + ImGui.TableNextRow(); + + foreach (var index in Enum.GetValues()) + { + var value = design.DesignData.Customize[index]; + var apply = design.DoApplyCustomize(index); + ImGuiUtil.DrawTableColumn(index.ToDefaultName()); + ImGuiUtil.DrawTableColumn(value.Value.ToString()); + ImGuiUtil.DrawTableColumn(apply ? "Apply" : "Keep"); + ImGui.TableNextRow(); + } + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs new file mode 100644 index 0000000..cf45077 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs @@ -0,0 +1,198 @@ +using Dalamud.Interface; +using Glamourer.Designs; +using Glamourer.Services; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Extensions; +using OtterGui.Raii; +using Penumbra.GameData.DataContainers; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class DesignTesterPanel(ItemManager _items, HumanModelList _humans) : IGameDataDrawer +{ + public string Label + => "Base64 Design Tester"; + + public bool Disabled + => false; + + private string _base64 = string.Empty; + private string _restore = string.Empty; + private byte[] _base64Bytes = []; + private byte[] _restoreBytes = []; + private DesignData _parse64 = new(); + private Exception? _parse64Failure; + + public void Draw() + { + DrawBase64Input(); + DrawDesignData(); + DrawBytes(); + } + + private void DrawBase64Input() + { + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##base64", "Base 64 input...", ref _base64, 2047); + if (!ImGui.IsItemDeactivatedAfterEdit()) + return; + + try + { + _base64Bytes = Convert.FromBase64String(_base64); + _parse64Failure = null; + } + catch (Exception ex) + { + _base64Bytes = Array.Empty(); + _parse64Failure = ex; + } + + if (_parse64Failure != null) + return; + + try + { + _parse64 = DesignBase64Migration.MigrateBase64(_items, _humans, _base64, out var ef, out var cf, out var wp, out var meta); + _restore = DesignBase64Migration.CreateOldBase64(in _parse64, ef, cf, meta, wp); + _restoreBytes = Convert.FromBase64String(_restore); + } + catch (Exception ex) + { + _parse64Failure = ex; + _restore = string.Empty; + } + } + + private void DrawDesignData() + { + if (_parse64Failure != null) + { + ImGuiUtil.TextWrapped(_parse64Failure.ToString()); + return; + } + + if (_restore.Length <= 0) + return; + + DrawDesignData(_parse64); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted(_base64); + using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 0 })) + { + foreach (var (c1, c2) in _restore.Zip(_base64)) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF4040D0, c1 != c2); + ImGui.TextUnformatted(c1.ToString()); + ImGui.SameLine(); + } + } + + ImGui.NewLine(); + + foreach (var ((b1, b2), idx) in _base64Bytes.Zip(_restoreBytes).WithIndex()) + { + using (_ = ImRaii.Group()) + { + ImGui.TextUnformatted(idx.ToString("D2")); + ImGui.TextUnformatted(b1.ToString("X2")); + using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF4040D0, b1 != b2); + ImGui.TextUnformatted(b2.ToString("X2")); + } + + ImGui.SameLine(); + } + + ImGui.NewLine(); + } + + private void DrawBytes() + { + if (_parse64Failure == null || _base64Bytes.Length <= 0) + return; + + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + foreach (var (b, idx) in _base64Bytes.WithIndex()) + { + using (_ = ImRaii.Group()) + { + ImGui.TextUnformatted(idx.ToString("D2")); + ImGui.TextUnformatted(b.ToString("X2")); + } + + ImGui.SameLine(); + } + + ImGui.NewLine(); + } + + public static void DrawDesignData(in DesignData data) + { + if (data.IsHuman) + DrawHumanData(data); + else + DrawMonsterData(data); + } + + private static void DrawHumanData(in DesignData data) + { + using var table = ImRaii.Table("##equip", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) + { + var item = data.Item(slot); + var stain = data.Stain(slot); + var crest = data.Crest(slot.ToCrestFlag()); + ImGuiUtil.DrawTableColumn(slot.ToName()); + ImGuiUtil.DrawTableColumn(item.Name); + ImGuiUtil.DrawTableColumn(item.ItemId.ToString()); + ImGuiUtil.DrawTableColumn(stain.ToString()); + ImGuiUtil.DrawTableColumn(crest.ToString()); + } + + ImGuiUtil.DrawTableColumn("Hat Visible"); + ImGuiUtil.DrawTableColumn(data.IsHatVisible().ToString()); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Visor Toggled"); + ImGuiUtil.DrawTableColumn(data.IsVisorToggled().ToString()); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Weapon Visible"); + ImGuiUtil.DrawTableColumn(data.IsWeaponVisible().ToString()); + ImGui.TableNextRow(); + + ImGuiUtil.DrawTableColumn("Model ID"); + ImGuiUtil.DrawTableColumn(data.ModelId.ToString()); + ImGui.TableNextRow(); + + foreach (var index in Enum.GetValues()) + { + var value = data.Customize[index]; + ImGuiUtil.DrawTableColumn(index.ToDefaultName()); + ImGuiUtil.DrawTableColumn(value.Value.ToString()); + ImGui.TableNextRow(); + } + + ImGuiUtil.DrawTableColumn("Is Wet"); + ImGuiUtil.DrawTableColumn(data.IsWet().ToString()); + ImGui.TableNextRow(); + } + + private static void DrawMonsterData(in DesignData data) + { + ImGui.TextUnformatted($"Model ID {data.ModelId}"); + ImGui.Separator(); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted("Customize Array"); + ImGui.Separator(); + ImGuiUtil.TextWrapped(string.Join(" ", data.GetCustomizeBytes().Select(b => b.ToString("X2")))); + + ImGui.TextUnformatted("Equipment Array"); + ImGui.Separator(); + ImGuiUtil.TextWrapped(string.Join(" ", data.GetEquipmentBytes().Select(b => b.ToString("X2")))); + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/DynamisPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DynamisPanel.cs new file mode 100644 index 0000000..92cd777 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/DynamisPanel.cs @@ -0,0 +1,16 @@ +using OtterGui.Services; +using Penumbra.GameData.Gui.Debug; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class DynamisPanel(DynamisIpc dynamis) : IGameDataDrawer +{ + public string Label + => "Dynamis Interop"; + + public void Draw() + => dynamis.DrawDebugInfo(); + + public bool Disabled + => false; +} diff --git a/Glamourer/Gui/Tabs/DebugTab/FunPanel.cs b/Glamourer/Gui/Tabs/DebugTab/FunPanel.cs new file mode 100644 index 0000000..370c4e5 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/FunPanel.cs @@ -0,0 +1,36 @@ +using Glamourer.State; +using Dalamud.Bindings.ImGui; +using Penumbra.GameData.Gui.Debug; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class FunPanel(FunModule _funModule, Configuration _config) : IGameDataDrawer +{ + public string Label + => "Fun Module"; + + public bool Disabled + => false; + + public void Draw() + { + ImGui.TextUnformatted($"Current Festival: {_funModule.CurrentFestival}"); + ImGui.TextUnformatted($"Festivals Enabled: {_config.DisableFestivals switch { 1 => "Undecided", 0 => "Enabled", _ => "Disabled" }}"); + ImGui.TextUnformatted($"Popup Open: {ImGui.IsPopupOpen("FestivalPopup", ImGuiPopupFlags.AnyPopup)}"); + if (ImGui.Button("Force Christmas")) + _funModule.ForceFestival(FunModule.FestivalType.Christmas); + if (ImGui.Button("Force Halloween")) + _funModule.ForceFestival(FunModule.FestivalType.Halloween); + if (ImGui.Button("Force April First")) + _funModule.ForceFestival(FunModule.FestivalType.AprilFirst); + if (ImGui.Button("Force None")) + _funModule.ForceFestival(FunModule.FestivalType.None); + if (ImGui.Button("Revert")) + _funModule.ResetFestival(); + if (ImGui.Button("Reset Popup")) + { + _config.DisableFestivals = 1; + _config.Save(); + } + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs new file mode 100644 index 0000000..f480f6d --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs @@ -0,0 +1,136 @@ +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game; +using Glamourer.Designs; +using Glamourer.Services; +using Glamourer.State; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Extensions; +using OtterGui.Text; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public unsafe class GlamourPlatePanel : IGameDataDrawer +{ + private readonly DesignManager _design; + private readonly ItemManager _items; + private readonly StateManager _state; + private readonly ActorObjectManager _objects; + + public string Label + => "Glamour Plates"; + + public bool Disabled + => false; + + public GlamourPlatePanel(IGameInteropProvider interop, ItemManager items, DesignManager design, StateManager state, + ActorObjectManager objects) + { + _items = items; + _design = design; + _state = state; + _objects = objects; + interop.InitializeFromAttributes(this); + } + + public void Draw() + { + var manager = MirageManager.Instance(); + using (ImRaii.Group()) + { + ImUtf8.Text("Address:"u8); + ImUtf8.Text("Number of Glamour Plates:"u8); + ImUtf8.Text("Glamour Plates Requested:"u8); + ImUtf8.Text("Glamour Plates Loaded:"u8); + ImUtf8.Text("Is Applying Glamour Plates:"u8); + } + + ImGui.SameLine(); + using (ImRaii.Group()) + { + ImUtf8.CopyOnClickSelectable($"0x{(ulong)manager:X}"); + ImUtf8.Text(manager == null ? "-" : manager->GlamourPlates.Length.ToString()); + ImUtf8.Text(manager == null ? "-" : manager->GlamourPlatesRequested.ToString()); + ImGui.SameLine(); + if (ImUtf8.SmallButton("Request Update"u8)) + RequestGlamour(); + ImUtf8.Text(manager == null ? "-" : manager->GlamourPlatesLoaded.ToString()); + ImUtf8.Text(manager == null ? "-" : manager->IsApplyingGlamourPlate.ToString()); + } + + if (manager == null) + return; + + ActorState? state = null; + var (identifier, data) = _objects.PlayerData; + var enabled = data.Valid && _state.GetOrCreate(identifier, data.Objects[0], out state); + + for (var i = 0; i < manager->GlamourPlates.Length; ++i) + { + using var tree = ImUtf8.TreeNode($"Plate #{i + 1:D2}"); + if (!tree) + continue; + + ref var plate = ref manager->GlamourPlates[i]; + if (ImUtf8.ButtonEx("Apply to Player"u8, ""u8, Vector2.Zero, !enabled)) + { + var design = CreateDesign(plate); + _state.ApplyDesign(state!, design, ApplySettings.Manual with { IsFinal = true }); + } + + using (ImRaii.Group()) + { + foreach (var slot in EquipSlotExtensions.FullSlots) + ImUtf8.Text(slot.ToName()); + } + + ImGui.SameLine(); + using (ImRaii.Group()) + { + foreach (var (_, index) in EquipSlotExtensions.FullSlots.WithIndex()) + ImUtf8.Text($"{plate.ItemIds[index]:D6}, {StainIds.FromGlamourPlate(plate, index)}"); + } + } + } + + [Signature(Sigs.RequestGlamourPlates)] + private readonly delegate* unmanaged _requestUpdate = null!; + + public void RequestGlamour() + { + var manager = MirageManager.Instance(); + if (manager == null) + return; + + _requestUpdate(manager); + } + + public DesignBase CreateDesign(in MirageManager.GlamourPlate plate) + { + var design = _design.CreateTemporary(); + design.Application = ApplicationCollection.None; + foreach (var (slot, index) in EquipSlotExtensions.FullSlots.WithIndex()) + { + var itemId = plate.ItemIds[index]; + if (itemId == 0) + continue; + + var item = _items.Resolve(slot, itemId); + if (!item.Valid) + continue; + + design.GetDesignDataRef().SetItem(slot, item); + design.GetDesignDataRef().SetStain(slot, StainIds.FromGlamourPlate(plate, index)); + design.Application.Equip |= slot.ToBothFlags(); + } + + return design; + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/InventoryPanel.cs b/Glamourer/Gui/Tabs/DebugTab/InventoryPanel.cs new file mode 100644 index 0000000..ca9ff7b --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/InventoryPanel.cs @@ -0,0 +1,53 @@ +using FFXIVClientStructs.FFXIV.Client.Game; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Gui.Debug; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public unsafe class InventoryPanel : IGameDataDrawer +{ + public string Label + => "Inventory"; + + public bool Disabled + => false; + + public void Draw() + { + var inventory = InventoryManager.Instance(); + if (inventory == null) + return; + + ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)inventory:X}"); + + var equip = inventory->GetInventoryContainer(InventoryType.EquippedItems); + if (equip == null || equip->IsLoaded) + return; + + ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)equip:X}"); + + using var table = ImRaii.Table("items", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + for (var i = 0; i < equip->Size; ++i) + { + ImGuiUtil.DrawTableColumn(i.ToString()); + var item = equip->GetInventorySlot(i); + if (item == null) + { + ImGuiUtil.DrawTableColumn("NULL"); + ImGui.TableNextRow(); + } + else + { + ImGuiUtil.DrawTableColumn(item->ItemId.ToString()); + ImGuiUtil.DrawTableColumn(item->GlamourId.ToString()); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable($"0x{(ulong)item:X}"); + } + } + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/IpcTester/DesignIpcTester.cs b/Glamourer/Gui/Tabs/DebugTab/IpcTester/DesignIpcTester.cs new file mode 100644 index 0000000..8cbf57a --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/IpcTester/DesignIpcTester.cs @@ -0,0 +1,123 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Plugin; +using Glamourer.Api.Enums; +using Glamourer.Api.IpcSubscribers; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; + +namespace Glamourer.Gui.Tabs.DebugTab.IpcTester; + +public class DesignIpcTester(IDalamudPluginInterface pluginInterface) : IUiService +{ + private Dictionary _designs = []; + private int _gameObjectIndex; + private string _gameObjectName = string.Empty; + private string _designName = string.Empty; + private uint _key; + private ApplyFlag _flags = ApplyFlagEx.DesignDefault; + private Guid? _design; + private string _designText = string.Empty; + private GlamourerApiEc _lastError; + + public void Draw() + { + using var tree = ImRaii.TreeNode("Designs"); + if (!tree) + return; + + IpcTesterHelpers.IndexInput(ref _gameObjectIndex); + IpcTesterHelpers.KeyInput(ref _key); + IpcTesterHelpers.NameInput(ref _gameObjectName); + ImUtf8.InputText("##designName"u8, ref _designName, "Design Name..."u8); + ImGuiUtil.GuidInput("##identifier", "Design Identifier...", string.Empty, ref _design, ref _designText, + ImGui.GetContentRegionAvail().X); + IpcTesterHelpers.DrawFlagInput(ref _flags); + + using var table = ImRaii.Table("##table", 2, ImGuiTableFlags.SizingFixedFit); + + IpcTesterHelpers.DrawIntro("Last Error"); + ImGui.TextUnformatted(_lastError.ToString()); + + IpcTesterHelpers.DrawIntro(GetDesignList.Label); + DrawDesignsPopup(); + if (ImGui.Button("Get##Designs")) + { + _designs = new GetDesignList(pluginInterface).Invoke(); + ImGui.OpenPopup("Designs"); + } + + IpcTesterHelpers.DrawIntro(ApplyDesign.Label); + if (ImGuiUtil.DrawDisabledButton("Apply##Idx", Vector2.Zero, string.Empty, !_design.HasValue)) + _lastError = new ApplyDesign(pluginInterface).Invoke(_design!.Value, _gameObjectIndex, _key, _flags); + + IpcTesterHelpers.DrawIntro(ApplyDesignName.Label); + if (ImGuiUtil.DrawDisabledButton("Apply##Name", Vector2.Zero, string.Empty, !_design.HasValue)) + _lastError = new ApplyDesignName(pluginInterface).Invoke(_design!.Value, _gameObjectName, _key, _flags); + + IpcTesterHelpers.DrawIntro(GetExtendedDesignData.Label); + if (_design.HasValue) + { + var (display, path, color, draw) = new GetExtendedDesignData(pluginInterface).Invoke(_design.Value); + if (path.Length > 0) + ImUtf8.Text($"{display} ({path}){(draw ? " in QDB"u8 : ""u8)}", color); + else + ImUtf8.Text("No Data"u8); + } + else + { + ImUtf8.Text("No Data"u8); + } + + IpcTesterHelpers.DrawIntro(GetDesignBase64.Label); + if (ImUtf8.Button("To Clipboard##Base64"u8) && _design.HasValue) + { + var data = new GetDesignBase64(pluginInterface).Invoke(_design.Value); + ImUtf8.SetClipboardText(data); + } + + IpcTesterHelpers.DrawIntro(AddDesign.Label); + if (ImUtf8.Button("Add from Clipboard"u8)) + try + { + var data = ImUtf8.GetClipboardText(); + _lastError = new AddDesign(pluginInterface).Invoke(data, _designName, out var newDesign); + if (_lastError is GlamourerApiEc.Success) + { + _design = newDesign; + _designText = newDesign.ToString(); + } + } + catch + { + _lastError = GlamourerApiEc.UnknownError; + } + + IpcTesterHelpers.DrawIntro(DeleteDesign.Label); + if (ImUtf8.Button("Delete##Design"u8) && _design.HasValue) + _lastError = new DeleteDesign(pluginInterface).Invoke(_design.Value); + } + + private void DrawDesignsPopup() + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var p = ImRaii.Popup("Designs"); + if (!p) + return; + + using var table = ImRaii.Table("Designs", 2, ImGuiTableFlags.SizingFixedFit); + foreach (var (guid, name) in _designs) + { + ImGuiUtil.DrawTableColumn(name); + using var f = ImRaii.PushFont(UiBuilder.MonoFont); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(guid.ToString()); + } + + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + ImGui.CloseCurrentPopup(); + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterHelpers.cs b/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterHelpers.cs new file mode 100644 index 0000000..61dad53 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterHelpers.cs @@ -0,0 +1,57 @@ +using Glamourer.Api.Enums; +using Dalamud.Bindings.ImGui; + +namespace Glamourer.Gui.Tabs.DebugTab.IpcTester; + +public static class IpcTesterHelpers +{ + public static void DrawFlagInput(ref ApplyFlag flags) + { + var value = (flags & ApplyFlag.Once) != 0; + if (ImGui.Checkbox("Apply Once", ref value)) + flags = value ? flags | ApplyFlag.Once : flags & ~ApplyFlag.Once; + + ImGui.SameLine(); + value = (flags & ApplyFlag.Equipment) != 0; + if (ImGui.Checkbox("Apply Equipment", ref value)) + flags = value ? flags | ApplyFlag.Equipment : flags & ~ApplyFlag.Equipment; + + ImGui.SameLine(); + value = (flags & ApplyFlag.Customization) != 0; + if (ImGui.Checkbox("Apply Customization", ref value)) + flags = value ? flags | ApplyFlag.Customization : flags & ~ApplyFlag.Customization; + + ImGui.SameLine(); + value = (flags & ApplyFlag.Lock) != 0; + if (ImGui.Checkbox("Lock Actor", ref value)) + flags = value ? flags | ApplyFlag.Lock : flags & ~ApplyFlag.Lock; + } + + public static void IndexInput(ref int index) + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X / 2); + ImGui.InputInt("Game Object Index", ref index, 0, 0); + } + + public static void KeyInput(ref uint key) + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X / 2); + var keyI = (int)key; + if (ImGui.InputInt("Key", ref keyI, 0, 0)) + key = (uint)keyI; + } + + public static void NameInput(ref string name) + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + ImGui.InputTextWithHint("##gameObject", "Character Name...", ref name, 64); + } + + public static void DrawIntro(string intro) + { + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(intro); + ImGui.TableNextColumn(); + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterPanel.cs b/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterPanel.cs new file mode 100644 index 0000000..22c7597 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterPanel.cs @@ -0,0 +1,86 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using Glamourer.Api.IpcSubscribers; +using Dalamud.Bindings.ImGui; +using Penumbra.GameData.Gui.Debug; + +namespace Glamourer.Gui.Tabs.DebugTab.IpcTester; + +public class IpcTesterPanel( + IDalamudPluginInterface pluginInterface, + DesignIpcTester designs, + ItemsIpcTester items, + StateIpcTester state, + IFramework framework) : IGameDataDrawer +{ + public string Label + => "IPC Tester"; + + public bool Disabled + => false; + + private DateTime _lastUpdate; + private bool _subscribed = false; + + public void Draw() + { + try + { + _lastUpdate = framework.LastUpdateUTC.AddSeconds(1); + Subscribe(); + ImGui.TextUnformatted(ApiVersion.Label); + var (major, minor) = new ApiVersion(pluginInterface).Invoke(); + ImGui.SameLine(); + ImGui.TextUnformatted($"({major}.{minor:D4})"); + + ImGui.TextUnformatted(AutoReloadGearEnabled.Label); + var autoRedraw = new AutoReloadGearEnabled(pluginInterface).Invoke(); + ImGui.SameLine(); + ImGui.TextUnformatted(autoRedraw ? "Enabled" : "Disabled"); + + designs.Draw(); + items.Draw(); + state.Draw(); + } + catch (Exception e) + { + Glamourer.Log.Error($"Error during IPC Tests:\n{e}"); + } + } + + private void Subscribe() + { + if (_subscribed) + return; + + Glamourer.Log.Debug("[IPCTester] Subscribed to IPC events for IPC tester."); + state.AutoRedrawChanged.Enable(); + state.GPoseChanged.Enable(); + state.StateChanged.Enable(); + state.StateFinalized.Enable(); + framework.Update += CheckUnsubscribe; + _subscribed = true; + } + + private void CheckUnsubscribe(IFramework framework1) + { + if (_lastUpdate > framework.LastUpdateUTC) + return; + + Unsubscribe(); + framework.Update -= CheckUnsubscribe; + } + + private void Unsubscribe() + { + if (!_subscribed) + return; + + Glamourer.Log.Debug("[IPCTester] Unsubscribed from IPC events for IPC tester."); + _subscribed = false; + state.AutoRedrawChanged.Disable(); + state.GPoseChanged.Disable(); + state.StateChanged.Disable(); + state.StateFinalized.Disable(); + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/IpcTester/ItemsIpcTester.cs b/Glamourer/Gui/Tabs/DebugTab/IpcTester/ItemsIpcTester.cs new file mode 100644 index 0000000..ea95a9d --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/IpcTester/ItemsIpcTester.cs @@ -0,0 +1,87 @@ +using Dalamud.Plugin; +using Glamourer.Api.Enums; +using Glamourer.Api.IpcSubscribers; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Tabs.DebugTab.IpcTester; + +public class ItemsIpcTester(IDalamudPluginInterface pluginInterface) : IUiService +{ + private int _gameObjectIndex; + private string _gameObjectName = string.Empty; + private uint _key; + private ApplyFlag _flags = ApplyFlagEx.DesignDefault; + private CustomItemId _customItemId; + private StainId _stainId; + private EquipSlot _slot = EquipSlot.Head; + private BonusItemFlag _bonusSlot = BonusItemFlag.Glasses; + private GlamourerApiEc _lastError; + + public void Draw() + { + using var tree = ImRaii.TreeNode("Items"); + if (!tree) + return; + + IpcTesterHelpers.IndexInput(ref _gameObjectIndex); + IpcTesterHelpers.KeyInput(ref _key); + DrawItemInput(); + IpcTesterHelpers.NameInput(ref _gameObjectName); + IpcTesterHelpers.DrawFlagInput(ref _flags); + using var table = ImRaii.Table("##table", 2, ImGuiTableFlags.SizingFixedFit); + + IpcTesterHelpers.DrawIntro("Last Error"); + ImGui.TextUnformatted(_lastError.ToString()); + + IpcTesterHelpers.DrawIntro(SetItem.Label); + if (ImGui.Button("Set##Idx")) + _lastError = new SetItem(pluginInterface).Invoke(_gameObjectIndex, (ApiEquipSlot)_slot, _customItemId.Id, [_stainId.Id], _key, + _flags); + + IpcTesterHelpers.DrawIntro(SetItemName.Label); + if (ImGui.Button("Set##Name")) + _lastError = new SetItemName(pluginInterface).Invoke(_gameObjectName, (ApiEquipSlot)_slot, _customItemId.Id, [_stainId.Id], _key, + _flags); + + IpcTesterHelpers.DrawIntro(SetBonusItem.Label); + if (ImGui.Button("Set##BonusIdx")) + _lastError = new SetBonusItem(pluginInterface).Invoke(_gameObjectIndex, ToApi(_bonusSlot), _customItemId.Id, _key, + _flags); + + IpcTesterHelpers.DrawIntro(SetBonusItemName.Label); + if (ImGui.Button("Set##BonusName")) + _lastError = new SetBonusItemName(pluginInterface).Invoke(_gameObjectName, ToApi(_bonusSlot), _customItemId.Id, _key, + _flags); + } + + private void DrawItemInput() + { + var tmp = _customItemId.Id; + var width = ImGui.GetContentRegionAvail().X / 2; + ImGui.SetNextItemWidth(width); + if (ImGuiUtil.InputUlong("Custom Item ID", ref tmp)) + _customItemId = tmp; + EquipSlotCombo.Draw("Equip Slot", string.Empty, ref _slot, width); + BonusSlotCombo.Draw("Bonus Slot", string.Empty, ref _bonusSlot, width); + var value = (int)_stainId.Id; + ImGui.SetNextItemWidth(width); + if (ImGui.InputInt("Stain ID", ref value, 1, 3)) + { + value = Math.Clamp(value, 0, byte.MaxValue); + _stainId = (StainId)value; + } + } + + private static ApiBonusSlot ToApi(BonusItemFlag slot) + => slot switch + { + BonusItemFlag.Glasses => ApiBonusSlot.Glasses, + _ => ApiBonusSlot.Unknown, + }; +} diff --git a/Glamourer/Gui/Tabs/DebugTab/IpcTester/StateIpcTester.cs b/Glamourer/Gui/Tabs/DebugTab/IpcTester/StateIpcTester.cs new file mode 100644 index 0000000..6fb9d68 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/IpcTester/StateIpcTester.cs @@ -0,0 +1,272 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Plugin; +using Glamourer.Api.Enums; +using Glamourer.Api.Helpers; +using Glamourer.Api.IpcSubscribers; +using Glamourer.Designs; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Interop; +using Penumbra.String; + +namespace Glamourer.Gui.Tabs.DebugTab.IpcTester; + +public class StateIpcTester : IUiService, IDisposable +{ + private readonly IDalamudPluginInterface _pluginInterface; + + private int _gameObjectIndex; + private string _gameObjectName = string.Empty; + private uint _key; + private ApplyFlag _flags = ApplyFlagEx.DesignDefault; + private GlamourerApiEc _lastError; + private JObject? _state; + private string? _stateString; + private string _base64State = string.Empty; + private string? _getStateString; + + public readonly EventSubscriber AutoRedrawChanged; + private bool _lastAutoRedrawChangeValue; + private DateTime _lastAutoRedrawChangeTime; + + public readonly EventSubscriber StateChanged; + private nint _lastStateChangeActor; + private ByteString _lastStateChangeName = ByteString.Empty; + private DateTime _lastStateChangeTime; + private StateChangeType _lastStateChangeType; + + public readonly EventSubscriber StateFinalized; + private nint _lastStateFinalizeActor; + private ByteString _lastStateFinalizeName = ByteString.Empty; + private DateTime _lastStateFinalizeTime; + private StateFinalizationType _lastStateFinalizeType; + + public readonly EventSubscriber GPoseChanged; + private bool _lastGPoseChangeValue; + private DateTime _lastGPoseChangeTime; + + private int _numUnlocked; + + public StateIpcTester(IDalamudPluginInterface pluginInterface) + { + _pluginInterface = pluginInterface; + AutoRedrawChanged = AutoReloadGearChanged.Subscriber(_pluginInterface, OnAutoRedrawChanged); + StateChanged = StateChangedWithType.Subscriber(_pluginInterface, OnStateChanged); + StateFinalized = Api.IpcSubscribers.StateFinalized.Subscriber(_pluginInterface, OnStateFinalized); + GPoseChanged = Api.IpcSubscribers.GPoseChanged.Subscriber(_pluginInterface, OnGPoseChange); + AutoRedrawChanged.Disable(); + StateChanged.Disable(); + StateFinalized.Disable(); + GPoseChanged.Disable(); + } + + public void Dispose() + { + AutoRedrawChanged.Dispose(); + StateChanged.Dispose(); + StateFinalized.Dispose(); + GPoseChanged.Dispose(); + } + + public void Draw() + { + using var tree = ImRaii.TreeNode("State"); + if (!tree) + return; + + IpcTesterHelpers.IndexInput(ref _gameObjectIndex); + IpcTesterHelpers.KeyInput(ref _key); + IpcTesterHelpers.NameInput(ref _gameObjectName); + IpcTesterHelpers.DrawFlagInput(ref _flags); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + ImGui.InputTextWithHint("##base64", "Base 64 State...", ref _base64State, 2000); + using var table = ImRaii.Table("##table", 2, ImGuiTableFlags.SizingFixedFit); + + IpcTesterHelpers.DrawIntro("Last Error"); + ImGui.TextUnformatted(_lastError.ToString()); + IpcTesterHelpers.DrawIntro("Last Auto Redraw Change"); + ImGui.TextUnformatted($"{_lastAutoRedrawChangeValue} at {_lastAutoRedrawChangeTime.ToLocalTime().TimeOfDay}"); + IpcTesterHelpers.DrawIntro("Last State Change"); + PrintChangeName(); + IpcTesterHelpers.DrawIntro("Last State Finalization"); + PrintFinalizeName(); + IpcTesterHelpers.DrawIntro("Last GPose Change"); + ImGui.TextUnformatted($"{_lastGPoseChangeValue} at {_lastGPoseChangeTime.ToLocalTime().TimeOfDay}"); + + + IpcTesterHelpers.DrawIntro(GetState.Label); + DrawStatePopup(); + if (ImUtf8.Button("Get##Idx"u8)) + { + (_lastError, _state) = new GetState(_pluginInterface).Invoke(_gameObjectIndex, _key); + _stateString = _state?.ToString(Formatting.Indented) ?? "No State Available"; + ImUtf8.OpenPopup("State"u8); + } + + IpcTesterHelpers.DrawIntro(GetStateName.Label); + if (ImUtf8.Button("Get##Name"u8)) + { + (_lastError, _state) = new GetStateName(_pluginInterface).Invoke(_gameObjectName, _key); + _stateString = _state?.ToString(Formatting.Indented) ?? "No State Available"; + ImUtf8.OpenPopup("State"u8); + } + + IpcTesterHelpers.DrawIntro(GetStateBase64.Label); + if (ImUtf8.Button("Get##Base64Idx"u8)) + { + (_lastError, _getStateString) = new GetStateBase64(_pluginInterface).Invoke(_gameObjectIndex, _key); + _stateString = _getStateString ?? "No State Available"; + ImUtf8.OpenPopup("State"u8); + } + + IpcTesterHelpers.DrawIntro(GetStateBase64Name.Label); + if (ImUtf8.Button("Get##Base64Idx"u8)) + { + (_lastError, _getStateString) = new GetStateBase64Name(_pluginInterface).Invoke(_gameObjectName, _key); + _stateString = _getStateString ?? "No State Available"; + ImUtf8.OpenPopup("State"u8); + } + + IpcTesterHelpers.DrawIntro(ApplyState.Label); + if (ImGuiUtil.DrawDisabledButton("Apply Last##Idx", Vector2.Zero, string.Empty, _state == null)) + _lastError = new ApplyState(_pluginInterface).Invoke(_state!, _gameObjectIndex, _key, _flags); + ImGui.SameLine(); + if (ImUtf8.Button("Apply Base64##Idx"u8)) + _lastError = new ApplyState(_pluginInterface).Invoke(_base64State, _gameObjectIndex, _key, _flags); + + IpcTesterHelpers.DrawIntro(ApplyStateName.Label); + if (ImGuiUtil.DrawDisabledButton("Apply Last##Name", Vector2.Zero, string.Empty, _state == null)) + _lastError = new ApplyStateName(_pluginInterface).Invoke(_state!, _gameObjectName, _key, _flags); + ImGui.SameLine(); + if (ImUtf8.Button("Apply Base64##Name"u8)) + _lastError = new ApplyStateName(_pluginInterface).Invoke(_base64State, _gameObjectName, _key, _flags); + + IpcTesterHelpers.DrawIntro(ReapplyState.Label); + if (ImUtf8.Button("Reapply##Idx"u8)) + _lastError = new ReapplyState(_pluginInterface).Invoke(_gameObjectIndex, _key, _flags); + + IpcTesterHelpers.DrawIntro(ReapplyStateName.Label); + if (ImUtf8.Button("Reapply##Name"u8)) + _lastError = new ReapplyStateName(_pluginInterface).Invoke(_gameObjectName, _key, _flags); + + IpcTesterHelpers.DrawIntro(RevertState.Label); + if (ImUtf8.Button("Revert##Idx"u8)) + _lastError = new RevertState(_pluginInterface).Invoke(_gameObjectIndex, _key, _flags); + + IpcTesterHelpers.DrawIntro(RevertStateName.Label); + if (ImUtf8.Button("Revert##Name"u8)) + _lastError = new RevertStateName(_pluginInterface).Invoke(_gameObjectName, _key, _flags); + + IpcTesterHelpers.DrawIntro(UnlockState.Label); + if (ImUtf8.Button("Unlock##Idx"u8)) + _lastError = new UnlockState(_pluginInterface).Invoke(_gameObjectIndex, _key); + + IpcTesterHelpers.DrawIntro(UnlockStateName.Label); + if (ImUtf8.Button("Unlock##Name"u8)) + _lastError = new UnlockStateName(_pluginInterface).Invoke(_gameObjectName, _key); + + IpcTesterHelpers.DrawIntro(UnlockAll.Label); + if (ImUtf8.Button("Unlock##All"u8)) + _numUnlocked = new UnlockAll(_pluginInterface).Invoke(_key); + ImGui.SameLine(); + ImGui.TextUnformatted($"Unlocked {_numUnlocked}"); + + IpcTesterHelpers.DrawIntro(RevertToAutomation.Label); + if (ImUtf8.Button("Revert##AutomationIdx"u8)) + _lastError = new RevertToAutomation(_pluginInterface).Invoke(_gameObjectIndex, _key, _flags); + + IpcTesterHelpers.DrawIntro(RevertToAutomationName.Label); + if (ImUtf8.Button("Revert##AutomationName"u8)) + _lastError = new RevertToAutomationName(_pluginInterface).Invoke(_gameObjectName, _key, _flags); + } + + private void DrawStatePopup() + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + if (_stateString == null) + return; + + using var p = ImUtf8.Popup("State"u8); + if (!p) + return; + + if (ImUtf8.Button("Copy to Clipboard"u8)) + ImUtf8.SetClipboardText(_stateString); + if (_stateString[0] is '{') + { + ImGui.SameLine(); + if (ImUtf8.Button("Copy as Base64"u8) && _state != null) + ImUtf8.SetClipboardText(DesignConverter.ToBase64(_state)); + } + + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + ImUtf8.TextWrapped(_stateString ?? string.Empty); + + if (ImUtf8.Button("Close"u8, -Vector2.UnitX) || !ImGui.IsWindowFocused()) + ImGui.CloseCurrentPopup(); + } + + private unsafe void PrintChangeName() + { + ImUtf8.Text(_lastStateChangeName.Span); + ImGui.SameLine(0, 0); + ImUtf8.Text($" ({_lastStateChangeType})"); + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImUtf8.CopyOnClickSelectable($"0x{_lastStateChangeActor:X}"); + } + + ImGui.SameLine(); + ImUtf8.Text($"at {_lastStateChangeTime.ToLocalTime().TimeOfDay}"); + } + + private unsafe void PrintFinalizeName() + { + ImUtf8.Text(_lastStateFinalizeName.Span); + ImGui.SameLine(0, 0); + ImUtf8.Text($" ({_lastStateFinalizeType})"); + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImUtf8.CopyOnClickSelectable($"0x{_lastStateFinalizeActor:X}"); + } + + ImGui.SameLine(); + ImUtf8.Text($"at {_lastStateFinalizeTime.ToLocalTime().TimeOfDay}"); + } + + private void OnAutoRedrawChanged(bool value) + { + _lastAutoRedrawChangeValue = value; + _lastAutoRedrawChangeTime = DateTime.UtcNow; + } + + private void OnStateChanged(nint actor, StateChangeType type) + { + _lastStateChangeActor = actor; + _lastStateChangeTime = DateTime.UtcNow; + _lastStateChangeName = actor != nint.Zero ? ((Actor)actor).Utf8Name.Clone() : ByteString.Empty; + _lastStateChangeType = type; + } + + private void OnStateFinalized(nint actor, StateFinalizationType type) + { + _lastStateFinalizeActor = actor; + _lastStateFinalizeTime = DateTime.UtcNow; + _lastStateFinalizeName = actor != nint.Zero ? ((Actor)actor).Utf8Name.Clone() : ByteString.Empty; + _lastStateFinalizeType = type; + } + + private void OnGPoseChange(bool value) + { + _lastGPoseChangeValue = value; + _lastGPoseChangeTime = DateTime.UtcNow; + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/ItemUnlockPanel.cs b/Glamourer/Gui/Tabs/DebugTab/ItemUnlockPanel.cs new file mode 100644 index 0000000..f82bfb3 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/ItemUnlockPanel.cs @@ -0,0 +1,62 @@ +using Dalamud.Interface.Utility; +using Glamourer.Services; +using Glamourer.Unlocks; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; +using ImGuiClip = OtterGui.ImGuiClip; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class ItemUnlockPanel(ItemUnlockManager _itemUnlocks, ItemManager _items) : IGameDataDrawer +{ + public string Label + => "Unlocked Items"; + + public bool Disabled + => false; + + public void Draw() + { + using var table = ImRaii.Table("itemUnlocks", 5, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter, + new Vector2(ImGui.GetContentRegionAvail().X, 12 * ImGui.GetTextLineHeight())); + if (!table) + return; + + ImGui.TableSetupColumn("ItemId", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, 400 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Slot", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Model", ImGuiTableColumnFlags.WidthFixed, 80 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Unlock", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale); + + ImGui.TableNextColumn(); + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); + ImGui.TableNextRow(); + var remainder = ImGuiClip.ClippedDraw(_itemUnlocks, skips, t => + { + ImGuiUtil.DrawTableColumn(t.Key.ToString()); + if (_items.ItemData.TryGetValue(t.Key, EquipSlot.MainHand, out var equip)) + { + ImGuiUtil.DrawTableColumn(equip.Name); + ImGuiUtil.DrawTableColumn(equip.Type.ToName()); + ImGuiUtil.DrawTableColumn(equip.Weapon().ToString()); + } + else + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + } + + ImGuiUtil.DrawTableColumn(_itemUnlocks.IsUnlocked(t.Key, out var time) + ? time == DateTimeOffset.MinValue + ? "Always" + : time.LocalDateTime.ToString("g") + : "Never"); + }, _itemUnlocks.Count); + ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight()); + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs b/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs new file mode 100644 index 0000000..185e19b --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs @@ -0,0 +1,362 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Glamourer.GameData; +using Glamourer.Interop; +using Glamourer.Interop.Structs; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.DataContainers; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public unsafe class ModelEvaluationPanel( + ActorObjectManager _objectManager, + VisorService _visorService, + VieraEarService _vieraEarService, + UpdateSlotService _updateSlotService, + ChangeCustomizeService _changeCustomizeService, + CrestService _crestService, + DictBonusItems bonusItems) : IGameDataDrawer +{ + public string Label + => "Model Evaluation"; + + public bool Disabled + => false; + + private int _gameObjectIndex; + + public void Draw() + { + ImGui.InputInt("Game Object Index", ref _gameObjectIndex, 0, 0); + var actor = _objectManager.Objects[_gameObjectIndex]; + var model = actor.Model; + using var table = ImRaii.Table("##evaluationTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableHeader("Actor"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Model"); + ImGui.TableNextColumn(); + + ImGuiUtil.DrawTableColumn("Address"); + ImGui.TableNextColumn(); + + Glamourer.Dynamis.DrawPointer(actor); + ImGui.TableNextColumn(); + Glamourer.Dynamis.DrawPointer(model); + ImGui.TableNextColumn(); + if (actor.IsCharacter) + { + ImGui.TextUnformatted(actor.AsCharacter->ModelContainer.ModelCharaId.ToString()); + if (actor.AsCharacter->CharacterData.TransformationId != 0) + ImGui.TextUnformatted($"Transformation Id: {actor.AsCharacter->CharacterData.TransformationId}"); + if (actor.AsCharacter->ModelContainer.ModelCharaId_2 != -1) + ImGui.TextUnformatted($"ModelChara2 {actor.AsCharacter->ModelContainer.ModelCharaId_2}"); + + ImGuiUtil.DrawTableColumn("Character Mode"); + ImGuiUtil.DrawTableColumn($"{actor.AsCharacter->Mode}"); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + + ImGuiUtil.DrawTableColumn("Animation"); + ImGuiUtil.DrawTableColumn($"{((ushort*)&actor.AsCharacter->Timeline)[0x78]}"); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + } + + ImGuiUtil.DrawTableColumn("Mainhand"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetMainhand().ToString() : "No Character"); + + var (mainhand, offhand, mainModel, offModel) = model.GetWeapons(actor); + ImGuiUtil.DrawTableColumn(mainModel.ToString()); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(mainhand.ToString()); + + ImGuiUtil.DrawTableColumn("Offhand"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetOffhand().ToString() : "No Character"); + ImGuiUtil.DrawTableColumn(offModel.ToString()); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(offhand.ToString()); + + DrawVisor(actor, model); + DrawVieraEars(actor, model); + DrawHatState(actor, model); + DrawWeaponState(actor, model); + DrawWetness(actor, model); + DrawEquip(actor, model); + DrawCustomize(actor, model); + DrawCrests(actor, model); + DrawParameters(actor, model); + + ImGuiUtil.DrawTableColumn("Scale"); + ImGuiUtil.DrawTableColumn(actor.Valid ? actor.AsObject->Scale.ToString(CultureInfo.InvariantCulture) : "No Character"); + ImGuiUtil.DrawTableColumn(model.Valid ? model.AsDrawObject->Object.Scale.ToString() : "No Model"); + ImGuiUtil.DrawTableColumn(model.IsCharacterBase + ? $"{*(float*)(model.Address + 0x270)} {*(float*)(model.Address + 0x274)}" + : "No CharacterBase"); + } + + private static void DrawParameters(Actor actor, Model model) + { + if (!model.IsHuman) + return; + + var convert = model.GetParameterData(); + foreach (var flag in CustomizeParameterExtensions.AllFlags) + { + ImGuiUtil.DrawTableColumn(flag.ToString()); + ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(convert[flag].InternalQuadruple.ToString()); + ImGui.TableNextColumn(); + } + } + + private void DrawVisor(Actor actor, Model model) + { + using var id = ImRaii.PushId("Visor"); + ImGuiUtil.DrawTableColumn("Visor State"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->DrawData.IsVisorToggled.ToString() : "No Character"); + ImGuiUtil.DrawTableColumn(model.IsHuman ? VisorService.GetVisorState(model).ToString() : "No Human"); + ImGui.TableNextColumn(); + if (!model.IsHuman) + return; + + if (ImGui.SmallButton("Set True")) + _visorService.SetVisorState(model, true); + ImGui.SameLine(); + if (ImGui.SmallButton("Set False")) + _visorService.SetVisorState(model, false); + ImGui.SameLine(); + if (ImGui.SmallButton("Toggle")) + _visorService.SetVisorState(model, !VisorService.GetVisorState(model)); + } + + private void DrawVieraEars(Actor actor, Model model) + { + using var id = ImRaii.PushId("Viera Ears"); + ImGuiUtil.DrawTableColumn("Viera Ears"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.ShowVieraEars.ToString() : "No Character"); + ImGuiUtil.DrawTableColumn(model.IsHuman ? model.VieraEarsVisible.ToString() : "No Human"); + ImGui.TableNextColumn(); + if (!model.IsHuman) + return; + + if (ImGui.SmallButton("Set True")) + _vieraEarService.SetVieraEarState(model, true); + ImGui.SameLine(); + if (ImGui.SmallButton("Set False")) + _vieraEarService.SetVieraEarState(model, false); + ImGui.SameLine(); + if (ImGui.SmallButton("Toggle")) + _vieraEarService.SetVieraEarState(model, !model.VieraEarsVisible); + } + + private void DrawHatState(Actor actor, Model model) + { + using var id = ImRaii.PushId("HatState"); + ImGuiUtil.DrawTableColumn("Hat State"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter + ? actor.AsCharacter->DrawData.IsHatHidden ? "Hidden" : actor.GetArmor(EquipSlot.Head).ToString() + : "No Character"); + ImGuiUtil.DrawTableColumn(model.IsHuman + ? model.AsHuman->Head.Value == 0 ? "No Hat" : model.GetArmor(EquipSlot.Head).ToString() + : "No Human"); + ImGui.TableNextColumn(); + if (!model.IsHuman) + return; + + if (ImGui.SmallButton("Hide")) + _updateSlotService.UpdateEquipSlot(model, EquipSlot.Head, CharacterArmor.Empty); + ImGui.SameLine(); + if (ImGui.SmallButton("Show")) + _updateSlotService.UpdateEquipSlot(model, EquipSlot.Head, actor.GetArmor(EquipSlot.Head)); + ImGui.SameLine(); + if (ImGui.SmallButton("Toggle")) + _updateSlotService.UpdateEquipSlot(model, EquipSlot.Head, + model.AsHuman->Head.Value == 0 ? actor.GetArmor(EquipSlot.Head) : CharacterArmor.Empty); + } + + private static void DrawWeaponState(Actor actor, Model model) + { + using var id = ImRaii.PushId("WeaponState"); + ImGuiUtil.DrawTableColumn("Weapon State"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter + ? actor.AsCharacter->DrawData.IsWeaponHidden ? "Hidden" : "Visible" + : "No Character"); + string text; + if (!model.IsHuman) + { + text = "No Model"; + } + else if (model.AsDrawObject->Object.ChildObject == null) + { + text = "No Weapon"; + } + else + { + var weapon = (DrawObject*)model.AsDrawObject->Object.ChildObject; + text = (weapon->Flags & 0x09) == 0x09 ? "Visible" : "Hidden"; + } + + ImGuiUtil.DrawTableColumn(text); + ImGui.TableNextColumn(); + } + + private static void DrawWetness(Actor actor, Model model) + { + using var id = ImRaii.PushId("Wetness"); + ImGuiUtil.DrawTableColumn("Wetness"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.IsGPoseWet ? "GPose" : "None" : "No Character"); + var modelString = model.IsCharacterBase + ? $"{model.AsCharacterBase->SwimmingWetness:F4} Swimming\n" + + $"{model.AsCharacterBase->WeatherWetness:F4} Weather\n" + + $"{model.AsCharacterBase->ForcedWetness:F4} Forced\n" + + $"{model.AsCharacterBase->WetnessDepth:F4} Depth\n" + : "No CharacterBase"; + ImGuiUtil.DrawTableColumn(modelString); + ImGui.TableNextColumn(); + if (!actor.IsCharacter) + return; + + if (ImGui.SmallButton("GPose On")) + actor.IsGPoseWet = true; + ImGui.SameLine(); + if (ImGui.SmallButton("GPose Off")) + actor.IsGPoseWet = false; + ImGui.SameLine(); + if (ImGui.SmallButton("GPose Toggle")) + actor.IsGPoseWet = !actor.IsGPoseWet; + } + + private void DrawEquip(Actor actor, Model model) + { + using var id = ImRaii.PushId("Equipment"); + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + using var id2 = ImRaii.PushId((int)slot); + ImGuiUtil.DrawTableColumn(slot.ToName()); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetArmor(slot).ToString() : "No Character"); + ImGuiUtil.DrawTableColumn(model.IsHuman ? model.GetArmor(slot).ToString() : "No Human"); + ImGui.TableNextColumn(); + if (!model.IsHuman) + continue; + + if (ImGui.SmallButton("Change Piece")) + _updateSlotService.UpdateArmor(model, slot, + new CharacterArmor((PrimaryId)(slot == EquipSlot.Hands ? 6064 : slot == EquipSlot.Head ? 6072 : 1), 1, StainIds.None)); + ImGui.SameLine(); + if (ImGui.SmallButton("Change Stain")) + _updateSlotService.UpdateStain(model, slot, new StainIds(5, 7)); + ImGui.SameLine(); + if (ImGui.SmallButton("Reset")) + _updateSlotService.UpdateEquipSlot(model, slot, actor.GetArmor(slot)); + } + + foreach (var slot in BonusExtensions.AllFlags) + { + using var id2 = ImRaii.PushId((int)slot.ToModelIndex()); + ImGuiUtil.DrawTableColumn(slot.ToName()); + if (!actor.IsCharacter) + { + ImGuiUtil.DrawTableColumn("No Character"); + } + else + { + var glassesId = actor.GetBonusItem(slot); + if (bonusItems.TryGetValue(glassesId, out var glasses)) + ImGuiUtil.DrawTableColumn($"{glasses.PrimaryId.Id},{glasses.Variant.Id} ({glassesId})"); + else + ImGuiUtil.DrawTableColumn($"{glassesId}"); + } + + ImGuiUtil.DrawTableColumn(model.IsHuman ? model.GetBonus(slot).ToString() : "No Human"); + ImGui.TableNextColumn(); + if (ImUtf8.SmallButton("Change Piece"u8)) + { + var data = model.GetBonus(slot); + _updateSlotService.UpdateBonusSlot(model, slot, data with { Variant = (Variant)((data.Variant.Id + 1) % 12) }); + } + } + } + + private void DrawCustomize(Actor actor, Model model) + { + using var id = ImRaii.PushId("Customize"); + var actorCustomize = actor.IsCharacter + ? *(CustomizeArray*)&actor.AsCharacter->DrawData.CustomizeData + : new CustomizeArray(); + var modelCustomize = model.IsHuman + ? *(CustomizeArray*)&model.AsHuman->Customize + : new CustomizeArray(); + foreach (var type in Enum.GetValues()) + { + using var id2 = ImRaii.PushId((int)type); + ImGuiUtil.DrawTableColumn(type.ToDefaultName()); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actorCustomize[type].Value.ToString("X2") : "No Character"); + ImGuiUtil.DrawTableColumn(model.IsHuman ? modelCustomize[type].Value.ToString("X2") : "No Human"); + ImGui.TableNextColumn(); + if (!model.IsHuman || type.ToFlag().RequiresRedraw()) + continue; + + if (ImGui.SmallButton("++")) + { + var value = modelCustomize[type].Value; + var (_, mask) = type.ToByteAndMask(); + var shift = BitOperations.TrailingZeroCount(mask); + var newValue = value + (1 << shift); + modelCustomize.Set(type, (CustomizeValue)newValue); + _changeCustomizeService.UpdateCustomize(model, modelCustomize); + } + + ImGui.SameLine(); + if (ImGui.SmallButton("--")) + { + var value = modelCustomize[type].Value; + var (_, mask) = type.ToByteAndMask(); + var shift = BitOperations.TrailingZeroCount(mask); + var newValue = value - (1 << shift); + modelCustomize.Set(type, (CustomizeValue)newValue); + _changeCustomizeService.UpdateCustomize(model, modelCustomize); + } + + ImGui.SameLine(); + if (ImGui.SmallButton("Reset")) + { + modelCustomize.Set(type, actorCustomize[type]); + _changeCustomizeService.UpdateCustomize(model, modelCustomize); + } + } + } + + private void DrawCrests(Actor actor, Model model) + { + using var id = ImRaii.PushId("Crests"); + CrestFlag whichToggle = 0; + CrestFlag totalModelFlags = 0; + foreach (var crestFlag in CrestExtensions.AllRelevantSet) + { + id.Push((int)crestFlag); + var modelCrest = CrestService.GetModelCrest(actor, crestFlag); + if (modelCrest) + totalModelFlags |= crestFlag; + ImGuiUtil.DrawTableColumn($"{crestFlag.ToLabel()} Crest"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetCrest(crestFlag).ToString() : "No Character"); + ImGuiUtil.DrawTableColumn(modelCrest.ToString()); + + ImGui.TableNextColumn(); + if (model.IsHuman && ImGui.SmallButton("Toggle")) + whichToggle = crestFlag; + + id.Pop(); + } + + if (whichToggle != 0) + _crestService.UpdateCrests(actor, totalModelFlags ^ whichToggle); + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs b/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs new file mode 100644 index 0000000..0d93bb8 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs @@ -0,0 +1,91 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Glamourer.Designs; +using Glamourer.GameData; +using Glamourer.State; +using Dalamud.Bindings.ImGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Interop; +using ImGuiClip = OtterGui.ImGuiClip; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class NpcAppearancePanel(NpcCombo npcCombo, StateManager stateManager, ActorObjectManager objectManager, DesignConverter designConverter) + : IGameDataDrawer +{ + public string Label + => "NPC Appearance"; + + public bool Disabled + => false; + + private string _npcFilter = string.Empty; + private bool _customizeOrGear; + + public void Draw() + { + ImUtf8.Checkbox("Compare Customize (or Gear)"u8, ref _customizeOrGear); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + var resetScroll = ImUtf8.InputText("##npcFilter"u8, ref _npcFilter, "Filter..."u8); + + using var table = ImRaii.Table("npcs", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, + new Vector2(-1, 400 * ImGuiHelpers.GlobalScale)); + if (!table) + return; + + if (resetScroll) + ImGui.SetScrollY(0); + + ImUtf8.TableSetupColumn("Button"u8, ImGuiTableColumnFlags.WidthFixed); + ImUtf8.TableSetupColumn("Name"u8, ImGuiTableColumnFlags.WidthFixed, ImGuiHelpers.GlobalScale * 300); + ImUtf8.TableSetupColumn("Kind"u8, ImGuiTableColumnFlags.WidthFixed); + ImUtf8.TableSetupColumn("Id"u8, ImGuiTableColumnFlags.WidthFixed); + ImUtf8.TableSetupColumn("Model"u8, ImGuiTableColumnFlags.WidthFixed); + ImUtf8.TableSetupColumn("Visor"u8, ImGuiTableColumnFlags.WidthFixed); + ImUtf8.TableSetupColumn("Compare"u8, ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetFrameHeightWithSpacing()); + ImGui.TableNextRow(); + var idx = 0; + var remainder = ImGuiClip.FilteredClippedDraw(npcCombo.Items, skips, + d => d.Name.Contains(_npcFilter, StringComparison.OrdinalIgnoreCase), DrawData); + ImGui.TableNextColumn(); + ImGuiClip.DrawEndDummy(remainder, ImGui.GetFrameHeightWithSpacing()); + return; + + void DrawData(NpcData data) + { + using var id = ImRaii.PushId(idx++); + var disabled = !stateManager.GetOrCreate(objectManager.Player, out var state); + ImGui.TableNextColumn(); + if (ImUtf8.ButtonEx("Apply"u8, ""u8, Vector2.Zero, disabled)) + { + foreach (var (slot, item, stain) in designConverter.FromDrawData(data.Equip.ToArray(), data.Mainhand, data.Offhand, true)) + stateManager.ChangeEquip(state!, slot, item, stain, ApplySettings.Manual); + stateManager.ChangeMetaState(state!, MetaIndex.VisorState, data.VisorToggled, ApplySettings.Manual); + stateManager.ChangeEntireCustomize(state!, data.Customize, CustomizeFlagExtensions.All, ApplySettings.Manual); + } + + ImUtf8.DrawFrameColumn(data.Name); + + ImUtf8.DrawFrameColumn(data.Kind is ObjectKind.BattleNpc ? "B" : "E"); + + ImUtf8.DrawFrameColumn(data.Id.Id.ToString()); + + ImUtf8.DrawFrameColumn(data.ModelId.ToString()); + + using (_ = ImRaii.PushFont(UiBuilder.IconFont)) + { + ImUtf8.DrawFrameColumn(data.VisorToggled ? FontAwesomeIcon.Check.ToIconString() : FontAwesomeIcon.Times.ToIconString()); + } + + using var mono = ImRaii.PushFont(UiBuilder.MonoFont); + ImUtf8.DrawFrameColumn(_customizeOrGear ? data.Customize.ToString() : data.WriteGear()); + } + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/ObjectManagerPanel.cs b/Glamourer/Gui/Tabs/DebugTab/ObjectManagerPanel.cs new file mode 100644 index 0000000..97847ae --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/ObjectManagerPanel.cs @@ -0,0 +1,83 @@ +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Text; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Interop; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class ObjectManagerPanel(ActorObjectManager _objectManager, ActorManager _actors) : IGameDataDrawer +{ + public string Label + => "Object Manager"; + + public bool Disabled + => false; + + private string _objectFilter = string.Empty; + + public void Draw() + { + _objectManager.Objects.DrawDebug(); + + using (var table = ImUtf8.Table("##data"u8, 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) + { + if (!table) + return; + + ImUtf8.DrawTableColumn("World"u8); + ImUtf8.DrawTableColumn(_actors.Finished ? _actors.Data.ToWorldName(_objectManager.World) : "Service Missing"); + ImUtf8.DrawTableColumn(_objectManager.World.ToString()); + + ImUtf8.DrawTableColumn("Player Character"u8); + ImUtf8.DrawTableColumn($"{_objectManager.Player.Utf8Name} ({_objectManager.Player.Index})"); + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable(_objectManager.Player.ToString()); + + ImUtf8.DrawTableColumn("In GPose"u8); + ImUtf8.DrawTableColumn(_objectManager.IsInGPose.ToString()); + ImGui.TableNextColumn(); + + ImUtf8.DrawTableColumn("In Lobby"u8); + ImUtf8.DrawTableColumn(_objectManager.IsInLobby.ToString()); + ImGui.TableNextColumn(); + + if (_objectManager.IsInGPose) + { + ImUtf8.DrawTableColumn("GPose Player"u8); + ImUtf8.DrawTableColumn($"{_objectManager.GPosePlayer.Utf8Name} ({_objectManager.GPosePlayer.Index})"); + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable(_objectManager.GPosePlayer.ToString()); + } + + ImUtf8.DrawTableColumn("Number of Players"u8); + ImUtf8.DrawTableColumn(_objectManager.Count.ToString()); + ImGui.TableNextColumn(); + } + + var filterChanged = ImUtf8.InputText("##Filter"u8, ref _objectFilter, "Filter..."u8); + using var table2 = ImUtf8.Table("##data2"u8, 3, + ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.ScrollY, + new Vector2(-1, 20 * ImGui.GetTextLineHeightWithSpacing())); + if (!table2) + return; + + if (filterChanged) + ImGui.SetScrollY(0); + + ImGui.TableNextColumn(); + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); + ImGui.TableNextRow(); + + var remainder = ImGuiClip.FilteredClippedDraw(_objectManager, skips, + p => p.Value.Label.Contains(_objectFilter, StringComparison.OrdinalIgnoreCase), p + => + { + ImUtf8.DrawTableColumn(p.Key.ToString()); + ImUtf8.DrawTableColumn(p.Value.Label); + ImUtf8.DrawTableColumn(string.Join(", ", p.Value.Objects.OrderBy(a => a.Index).Select(a => a.Index.ToString()))); + }); + ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeightWithSpacing()); + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/PenumbraPanel.cs b/Glamourer/Gui/Tabs/DebugTab/PenumbraPanel.cs new file mode 100644 index 0000000..833ebe4 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/PenumbraPanel.cs @@ -0,0 +1,103 @@ +using Dalamud.Interface.Utility; +using Glamourer.Interop.Penumbra; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItemTooltip _penumbraTooltip) : IGameDataDrawer +{ + public string Label + => "Penumbra Interop"; + + public bool Disabled + => false; + + private int _gameObjectIndex; + private Model _drawObject = Model.Null; + + public void Draw() + { + using var table = ImRaii.Table("##PenumbraTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + ImGuiUtil.DrawTableColumn("Available"); + ImGuiUtil.DrawTableColumn(_penumbra.Available.ToString()); + ImGui.TableNextColumn(); + if (ImGui.SmallButton("Unattach")) + _penumbra.Unattach(); + ImGui.SameLine(); + if (ImGui.SmallButton("Reattach")) + _penumbra.Reattach(); + + ImGuiUtil.DrawTableColumn("Version"); + ImGuiUtil.DrawTableColumn($"{_penumbra.CurrentMajor}.{_penumbra.CurrentMinor}"); + ImGui.TableNextColumn(); + + ImGuiUtil.DrawTableColumn("Attached When"); + ImGuiUtil.DrawTableColumn(_penumbra.AttachTime.ToLocalTime().ToLongTimeString()); + ImGui.TableNextColumn(); + + ImGuiUtil.DrawTableColumn("Draw Object"); + ImGui.TableNextColumn(); + var address = _drawObject.Address; + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + if (ImGui.InputScalar("##drawObjectPtr", ImGuiDataType.U64, ref address, nint.Zero, nint.Zero, "%llx", + ImGuiInputTextFlags.CharsHexadecimal)) + _drawObject = address; + ImGuiUtil.DrawTableColumn(_penumbra.Available + ? $"0x{_penumbra.GameObjectFromDrawObject(_drawObject).Address:X}" + : "Penumbra Unavailable"); + + ImGuiUtil.DrawTableColumn("Cutscene Object"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + ImGui.InputInt("##CutsceneIndex", ref _gameObjectIndex, 0, 0); + ImGuiUtil.DrawTableColumn(_penumbra.Available + ? _penumbra.CutsceneParent((ushort)_gameObjectIndex).ToString() + : "Penumbra Unavailable"); + + ImGuiUtil.DrawTableColumn("Redraw Object"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + ImGui.InputInt("##redrawObject", ref _gameObjectIndex, 0, 0); + ImGui.TableNextColumn(); + using (_ = ImRaii.Disabled(!_penumbra.Available)) + { + if (ImGui.SmallButton("Redraw")) + _penumbra.RedrawObject((ObjectIndex)_gameObjectIndex, RedrawType.Redraw); + } + + ImGuiUtil.DrawTableColumn("Last Tooltip Date"); + ImGuiUtil.DrawTableColumn(_penumbraTooltip.LastTooltip > DateTime.MinValue + ? $"{_penumbraTooltip.LastTooltip.ToLongTimeString()} ({_penumbraTooltip.LastType} {_penumbraTooltip.LastId})" + : "Never"); + ImGui.TableNextColumn(); + + ImGuiUtil.DrawTableColumn("Last Click Date"); + ImGuiUtil.DrawTableColumn(_penumbraTooltip.LastClick > DateTime.MinValue ? _penumbraTooltip.LastClick.ToLongTimeString() : "Never"); + ImGui.TableNextColumn(); + + ImGui.Separator(); + ImGui.Separator(); + foreach (var (slot, item) in _penumbraTooltip.LastItems) + { + switch (slot) + { + case EquipSlot e: ImGuiUtil.DrawTableColumn($"{e.ToName()} Revert-Item"); break; + case BonusItemFlag f: ImGuiUtil.DrawTableColumn($"{f.ToName()} Revert-Item"); break; + default: ImGuiUtil.DrawTableColumn("Unk Revert-Item"); break; + } + + ImGuiUtil.DrawTableColumn(item.Valid ? item.Name : "None"); + ImGui.TableNextColumn(); + } + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/RetainedStatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/RetainedStatePanel.cs new file mode 100644 index 0000000..21f0c50 --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/RetainedStatePanel.cs @@ -0,0 +1,27 @@ +using Glamourer.Interop; +using Glamourer.Interop.Structs; +using Glamourer.State; +using OtterGui.Raii; +using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Interop; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class RetainedStatePanel(StateManager _stateManager, ActorObjectManager _objectManager) : IGameDataDrawer +{ + public string Label + => "Retained States (Inactive Actors)"; + + public bool Disabled + => false; + + public void Draw() + { + foreach (var (identifier, state) in _stateManager.Where(kvp => !_objectManager.ContainsKey(kvp.Key))) + { + using var t = ImRaii.TreeNode(identifier.ToString()); + if (t) + ActiveStatePanel.DrawState(_stateManager, ActorData.Invalid, state); + } + } +} diff --git a/Glamourer/Gui/Tabs/DebugTab/UnlockableItemsPanel.cs b/Glamourer/Gui/Tabs/DebugTab/UnlockableItemsPanel.cs new file mode 100644 index 0000000..b22008d --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab/UnlockableItemsPanel.cs @@ -0,0 +1,64 @@ +using Dalamud.Interface.Utility; +using Glamourer.Services; +using Glamourer.Unlocks; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; +using ImGuiClip = OtterGui.ImGuiClip; + +namespace Glamourer.Gui.Tabs.DebugTab; + +public class UnlockableItemsPanel(ItemUnlockManager _itemUnlocks, ItemManager _items) : IGameDataDrawer +{ + public string Label + => "Unlockable Items"; + + public bool Disabled + => false; + + public void Draw() + { + using var table = ImRaii.Table("unlockableItem", 6, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter, + new Vector2(ImGui.GetContentRegionAvail().X, 12 * ImGui.GetTextLineHeight())); + if (!table) + return; + + ImGui.TableSetupColumn("ItemId", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, 400 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Slot", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Model", ImGuiTableColumnFlags.WidthFixed, 80 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Unlock", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Criteria", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); + ImGui.TableNextRow(); + var remainder = ImGuiClip.ClippedDraw(_itemUnlocks.Unlockable, skips, t => + { + ImGuiUtil.DrawTableColumn(t.Key.ToString()); + if (_items.ItemData.TryGetValue(t.Key, EquipSlot.MainHand, out var equip)) + { + ImGuiUtil.DrawTableColumn(equip.Name); + ImGuiUtil.DrawTableColumn(equip.Type.ToName()); + ImGuiUtil.DrawTableColumn(equip.Weapon().ToString()); + } + else + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + } + + ImGuiUtil.DrawTableColumn(_itemUnlocks.IsUnlocked(t.Key, out var time) + ? time == DateTimeOffset.MinValue + ? "Always" + : time.LocalDateTime.ToString("g") + : "Never"); + ImGuiUtil.DrawTableColumn(t.Value.ToString()); + }, _itemUnlocks.Unlockable.Count); + ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight()); + } +} diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs b/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs new file mode 100644 index 0000000..e9fe775 --- /dev/null +++ b/Glamourer/Gui/Tabs/DesignTab/DesignColorCombo.cs @@ -0,0 +1,27 @@ +using Glamourer.Designs; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; + +namespace Glamourer.Gui.Tabs.DesignTab; + +public sealed class DesignColorCombo(DesignColors _designColors, bool _skipAutomatic) : + FilterComboCache(_skipAutomatic + ? _designColors.Keys.OrderBy(k => k) + : _designColors.Keys.OrderBy(k => k).Prepend(DesignColors.AutomaticName), + MouseWheelType.Control, Glamourer.Log) +{ + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var isAutomatic = !_skipAutomatic && globalIdx == 0; + var key = Items[globalIdx]; + var color = isAutomatic ? 0 : _designColors[key]; + using var c = ImRaii.PushColor(ImGuiCol.Text, color, color != 0); + var ret = base.DrawSelectable(globalIdx, selected); + if (isAutomatic) + ImGuiUtil.HoverTooltip( + "The automatic color uses the colors dependent on the design state, as defined in the regular color definitions."); + return ret; + } +} diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs b/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs index 38fd5f8..8a3dd06 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignDetailTab.cs @@ -1,14 +1,12 @@ -using System; -using System.Diagnostics; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; using Glamourer.Designs; using Glamourer.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; namespace Glamourer.Gui.Tabs.DesignTab; @@ -16,9 +14,12 @@ namespace Glamourer.Gui.Tabs.DesignTab; public class DesignDetailTab { private readonly SaveService _saveService; + private readonly Configuration _config; private readonly DesignFileSystemSelector _selector; private readonly DesignFileSystem _fileSystem; private readonly DesignManager _manager; + private readonly DesignColors _colors; + private readonly DesignColorCombo _colorCombo; private readonly TagButtons _tagButtons = new(); private string? _newPath; @@ -29,17 +30,22 @@ public class DesignDetailTab private Design? _changeDesign; private DesignFileSystem.Leaf? _changeLeaf; - public DesignDetailTab(SaveService saveService, DesignFileSystemSelector selector, DesignManager manager, DesignFileSystem fileSystem) + public DesignDetailTab(SaveService saveService, DesignFileSystemSelector selector, DesignManager manager, DesignFileSystem fileSystem, + DesignColors colors, Configuration config) { _saveService = saveService; _selector = selector; _manager = manager; _fileSystem = fileSystem; + _colors = colors; + _config = config; + _colorCombo = new DesignColorCombo(_colors, false); } public void Draw() { - if (!ImGui.CollapsingHeader("Design Details")) + using var h = DesignPanelFlag.DesignDetails.Header(_config); + if (!h) return; DrawDesignInfoTable(); @@ -51,19 +57,19 @@ public class DesignDetailTab private void DrawDesignInfoTable() { using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); - using var table = ImRaii.Table("Details", 2); + using var table = ImUtf8.Table("Details"u8, 2); if (!table) return; - ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Last Update Datem").X); - ImGui.TableSetupColumn("Data", ImGuiTableColumnFlags.WidthStretch); + ImUtf8.TableSetupColumn("Type"u8, ImGuiTableColumnFlags.WidthFixed, ImUtf8.CalcTextSize("Reset Temporary Settings"u8).X); + ImUtf8.TableSetupColumn("Data"u8, ImGuiTableColumnFlags.WidthStretch); - ImGuiUtil.DrawFrameColumn("Design Name"); + ImUtf8.DrawFrameColumn("Design Name"u8); ImGui.TableNextColumn(); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); var name = _newName ?? _selector.Selected!.Name; ImGui.SetNextItemWidth(width.X); - if (ImGui.InputText("##Name", ref name, 128)) + if (ImUtf8.InputText("##Name"u8, ref name)) { _newName = name; _changeDesign = _selector.Selected; @@ -77,10 +83,10 @@ public class DesignDetailTab } var identifier = _selector.Selected!.Identifier.ToString(); - ImGuiUtil.DrawFrameColumn("Unique Identifier"); + ImUtf8.DrawFrameColumn("Unique Identifier"u8); ImGui.TableNextColumn(); var fileName = _saveService.FileNames.DesignFile(_selector.Selected!); - using (var mono = ImRaii.PushFont(UiBuilder.MonoFont)) + using (ImRaii.PushFont(UiBuilder.MonoFont)) { if (ImGui.Button(identifier, width)) try @@ -89,17 +95,22 @@ public class DesignDetailTab } catch (Exception ex) { - Glamourer.Messager.NotificationMessage(ex, $"Could not open file {fileName}.", $"Could not open file {fileName}", NotificationType.Warning); + Glamourer.Messager.NotificationMessage(ex, $"Could not open file {fileName}.", $"Could not open file {fileName}", + NotificationType.Warning); } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImGui.SetClipboardText(identifier); } - ImGuiUtil.HoverTooltip($"Open the file\n\t{fileName}\ncontaining this design in the .json-editor of your choice."); + ImUtf8.HoverTooltip( + $"Open the file\n\t{fileName}\ncontaining this design in the .json-editor of your choice.\n\nRight-Click to copy identifier to clipboard."); - ImGuiUtil.DrawFrameColumn("Full Selector Path"); + ImUtf8.DrawFrameColumn("Full Selector Path"u8); ImGui.TableNextColumn(); var path = _newPath ?? _selector.SelectedLeaf!.FullName(); ImGui.SetNextItemWidth(width.X); - if (ImGui.InputText("##Path", ref path, 1024)) + if (ImUtf8.InputText("##Path"u8, ref path)) { _newPath = path; _changeLeaf = _selector.SelectedLeaf!; @@ -117,15 +128,79 @@ public class DesignDetailTab Glamourer.Messager.NotificationMessage(ex, ex.Message, "Could not rename or move design", NotificationType.Error); } - ImGuiUtil.DrawFrameColumn("Creation Date"); + ImUtf8.DrawFrameColumn("Quick Design Bar"u8); + ImGui.TableNextColumn(); + if (ImUtf8.RadioButton("Display##qdb"u8, _selector.Selected.QuickDesign)) + _manager.SetQuickDesign(_selector.Selected!, true); + var hovered = ImGui.IsItemHovered(); + ImGui.SameLine(); + if (ImUtf8.RadioButton("Hide##qdb"u8, !_selector.Selected.QuickDesign)) + _manager.SetQuickDesign(_selector.Selected!, false); + if (hovered || ImGui.IsItemHovered()) + { + using var tt = ImUtf8.Tooltip(); + ImUtf8.Text("Display or hide this design in your quick design bar."u8); + } + + var forceRedraw = _selector.Selected!.ForcedRedraw; + ImUtf8.DrawFrameColumn("Force Redrawing"u8); + ImGui.TableNextColumn(); + if (ImUtf8.Checkbox("##ForceRedraw"u8, ref forceRedraw)) + _manager.ChangeForcedRedraw(_selector.Selected!, forceRedraw); + ImUtf8.HoverTooltip("Set this design to always force a redraw when it is applied through any means."u8); + + var resetAdvancedDyes = _selector.Selected!.ResetAdvancedDyes; + ImUtf8.DrawFrameColumn("Reset Advanced Dyes"u8); + ImGui.TableNextColumn(); + if (ImUtf8.Checkbox("##ResetAdvancedDyes"u8, ref resetAdvancedDyes)) + _manager.ChangeResetAdvancedDyes(_selector.Selected!, resetAdvancedDyes); + ImUtf8.HoverTooltip("Set this design to reset any previously applied advanced dyes when it is applied through any means."u8); + + var resetTemporarySettings = _selector.Selected!.ResetTemporarySettings; + ImUtf8.DrawFrameColumn("Reset Temporary Settings"u8); + ImGui.TableNextColumn(); + if (ImUtf8.Checkbox("##ResetTemporarySettings"u8, ref resetTemporarySettings)) + _manager.ChangeResetTemporarySettings(_selector.Selected!, resetTemporarySettings); + ImUtf8.HoverTooltip( + "Set this design to reset any temporary settings previously applied to the associated collection when it is applied through any means."u8); + + ImUtf8.DrawFrameColumn("Color"u8); + var colorName = _selector.Selected!.Color.Length == 0 ? DesignColors.AutomaticName : _selector.Selected!.Color; + ImGui.TableNextColumn(); + if (_colorCombo.Draw("##colorCombo", colorName, "Associate a color with this design.\n" + + "Right-Click to revert to automatic coloring.\n" + + "Hold Control and scroll the mousewheel to scroll.", + width.X - ImGui.GetStyle().ItemSpacing.X - ImGui.GetFrameHeight(), ImGui.GetTextLineHeight()) + && _colorCombo.CurrentSelection != null) + { + colorName = _colorCombo.CurrentSelection is DesignColors.AutomaticName ? string.Empty : _colorCombo.CurrentSelection; + _manager.ChangeColor(_selector.Selected!, colorName); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + _manager.ChangeColor(_selector.Selected!, string.Empty); + + if (_colors.TryGetValue(_selector.Selected!.Color, out var currentColor)) + { + ImGui.SameLine(); + if (DesignColorUi.DrawColorButton($"Color associated with {_selector.Selected!.Color}", currentColor, out var newColor)) + _colors.SetColor(_selector.Selected!.Color, newColor); + } + else if (_selector.Selected!.Color.Length != 0) + { + ImGui.SameLine(); + ImUtf8.Icon(FontAwesomeIcon.ExclamationCircle, "The color associated with this design does not exist."u8, _colors.MissingColor); + } + + ImUtf8.DrawFrameColumn("Creation Date"u8); ImGui.TableNextColumn(); ImGuiUtil.DrawTextButton(_selector.Selected!.CreationDate.LocalDateTime.ToString("F"), width, 0); - ImGuiUtil.DrawFrameColumn("Last Update Date"); + ImUtf8.DrawFrameColumn("Last Update Date"u8); ImGui.TableNextColumn(); ImGuiUtil.DrawTextButton(_selector.Selected!.LastEdit.LocalDateTime.ToString("F"), width, 0); - ImGuiUtil.DrawFrameColumn("Tags"); + ImUtf8.DrawFrameColumn("Tags"u8); ImGui.TableNextColumn(); DrawTags(); } @@ -155,18 +230,18 @@ public class DesignDetailTab var size = new Vector2(ImGui.GetContentRegionAvail().X, 12 * ImGui.GetTextLineHeightWithSpacing()); if (!_editDescriptionMode) { - using (var textBox = ImRaii.ListBox("##desc", size)) + using (var textBox = ImUtf8.ListBox("##desc"u8, size)) { - ImGuiUtil.TextWrapped(desc); + ImUtf8.TextWrapped(desc); } - if (ImGui.Button("Edit Description")) + if (ImUtf8.Button("Edit Description"u8)) _editDescriptionMode = true; } else { var edit = _newDescription ?? desc; - if (ImGui.InputTextMultiline("##desc", ref edit, (uint)Math.Max(2000, 4 * edit.Length), size)) + if (ImUtf8.InputMultiLine("##desc"u8, ref edit, size)) _newDescription = edit; if (ImGui.IsItemDeactivatedAfterEdit()) @@ -175,7 +250,7 @@ public class DesignDetailTab _newDescription = null; } - if (ImGui.Button("Stop Editing")) + if (ImUtf8.Button("Stop Editing"u8)) _editDescriptionMode = false; } } diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs index 114a527..e0e4543 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignFileSystemSelector.cs @@ -1,19 +1,18 @@ -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; -using Glamourer.Customization; using Glamourer.Designs; +using Glamourer.Designs.History; using Glamourer.Events; -using ImGuiNET; +using Glamourer.Services; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.FileSystem.Selector; using OtterGui.Log; using OtterGui.Raii; +using OtterGui.Text; namespace Glamourer.Gui.Tabs.DesignTab; @@ -24,6 +23,8 @@ public sealed class DesignFileSystemSelector : FileSystemSelector _config.IncognitoMode; + get => _config.Ephemeral.IncognitoMode; set { - _config.IncognitoMode = value; - _config.Save(); + _config.Ephemeral.IncognitoMode = value; + _config.Ephemeral.Save(); } } public new DesignFileSystem.Leaf? SelectedLeaf => base.SelectedLeaf; - public struct DesignState + public record struct DesignState(uint Color) + { } + + protected override float CurrentWidth + => _config.Ephemeral.CurrentDesignSelectorWidth * ImUtf8.GlobalScale; + + protected override float MinimumAbsoluteRemainder + => 470 * ImUtf8.GlobalScale; + + protected override float MinimumScaling + => _config.Ephemeral.DesignSelectorMinimumScale; + + protected override float MaximumScaling + => _config.Ephemeral.DesignSelectorMaximumScale; + + protected override void SetSize(Vector2 size) { - public ColorId Color; + base.SetSize(size); + var adaptedSize = MathF.Round(size.X / ImUtf8.GlobalScale); + if (adaptedSize == _config.Ephemeral.CurrentDesignSelectorWidth) + return; + + _config.Ephemeral.CurrentDesignSelectorWidth = adaptedSize; + _config.Ephemeral.Save(); } public DesignFileSystemSelector(DesignManager designManager, DesignFileSystem fileSystem, IKeyState keyState, DesignChanged @event, - Configuration config, DesignConverter converter, TabSelected selectionEvent, Logger log) + Configuration config, DesignConverter converter, TabSelected selectionEvent, Logger log, DesignColors designColors, + DesignApplier designApplier) : base(fileSystem, keyState, log, allowMultipleSelection: true) { _designManager = designManager; @@ -56,14 +79,90 @@ public sealed class DesignFileSystemSelector : FileSystemSelector.Leaf? leaf, bool clear, in DesignState storage = default) + { + base.Select(leaf, clear, storage); + var id = SelectedLeaf?.Value.Identifier ?? Guid.Empty; + if (id != _config.Ephemeral.SelectedDesign) + { + _config.Ephemeral.SelectedDesign = id; + _config.Ephemeral.Save(); + } } protected override void DrawPopups() @@ -75,8 +174,10 @@ public sealed class DesignFileSystemSelector : FileSystemSelector SortMode @@ -101,7 +203,7 @@ public sealed class DesignFileSystemSelector : FileSystemSelector _config.OpenFoldersByDefault; - private void OnDesignChange(DesignChanged.Type type, Design design, object? oldData) + private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? _) { switch (type) { @@ -116,6 +218,11 @@ public sealed class DesignFileSystemSelector : FileSystemSelector Appropriately identify and set the string filter and its type. @@ -220,10 +337,14 @@ public sealed class DesignFileSystemSelector : FileSystemSelector filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1), 'N' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1), - 'm' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 2), - 'M' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 2), - 't' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 3), - 'T' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 3), + 'm' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 2), + 'M' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 2), + 't' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 3), + 'T' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 3), + 'i' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 4), + 'I' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 4), + 'c' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 5), + 'C' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 5), _ => (new LowerString(filterValue), 0), }, _ => (new LowerString(filterValue), 0), @@ -232,6 +353,15 @@ public sealed class DesignFileSystemSelector : FileSystemSelector /// The overwritten filter method also computes the state. /// Folders have default state and are filtered out on the direct string instead of the other options. @@ -254,29 +384,23 @@ public sealed class DesignFileSystemSelector : FileSystemSelector false, - 0 => !(_designFilter.IsContained(leaf.FullName()) || design.Name.Contains(_designFilter)), - 1 => !design.Name.Contains(_designFilter), - 2 => !design.AssociatedMods.Any(kvp => _designFilter.IsContained(kvp.Key.Name)), - 3 => !design.Tags.Any(_designFilter.IsContained), - _ => false, // Should never happen + -1 => false, + 0 => !(_designFilter.IsContained(leaf.FullName()) || design.Name.Contains(_designFilter)), + 1 => !design.Name.Contains(_designFilter), + 2 => !design.AssociatedMods.Any(kvp => _designFilter.IsContained(kvp.Key.Name)), + 3 => !design.Tags.Any(_designFilter.IsContained), + 4 => !design.DesignData.ContainsName(_designFilter), + 5 => !_designFilter.IsContained(design.Color.Length == 0 ? DesignColors.AutomaticName : design.Color), + 2 + EmptyOffset => design.AssociatedMods.Count > 0, + 3 + EmptyOffset => design.Tags.Length > 0, + _ => false, // Should never happen }; } /// Combined wrapper for handling all filters and setting state. private bool ApplyFiltersAndState(DesignFileSystem.Leaf leaf, out DesignState state) { - var applyEquip = leaf.Value.ApplyEquip != 0; - var applyCustomize = (leaf.Value.ApplyCustomize & ~(CustomizeFlag.BodyType | CustomizeFlag.Race)) != 0; - - state.Color = (applyEquip, applyCustomize) switch - { - (false, false) => ColorId.StateDesign, - (false, true) => ColorId.CustomizationDesign, - (true, false) => ColorId.EquipmentDesign, - (true, true) => ColorId.NormalDesign, - }; - + state = new DesignState(_designColors.GetColor(leaf.Value)); return ApplyStringFilters(leaf, leaf.Value); } diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs b/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs new file mode 100644 index 0000000..d9517c8 --- /dev/null +++ b/Glamourer/Gui/Tabs/DesignTab/DesignLinkDrawer.cs @@ -0,0 +1,253 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Glamourer.Automation; +using Glamourer.Designs; +using Glamourer.Designs.Links; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; + +namespace Glamourer.Gui.Tabs.DesignTab; + +public class DesignLinkDrawer( + DesignLinkManager _linkManager, + DesignFileSystemSelector _selector, + LinkDesignCombo _combo, + DesignColors _colorManager, + Configuration config) : IUiService +{ + private int _dragDropIndex = -1; + private LinkOrder _dragDropOrder = LinkOrder.None; + private int _dragDropTargetIndex = -1; + private LinkOrder _dragDropTargetOrder = LinkOrder.None; + + public void Draw() + { + using var h = DesignPanelFlag.DesignLinks.Header(config); + if (h.Disposed) + return; + + ImGuiUtil.HoverTooltip( + "Design links are links to other designs that will be applied to characters or during automation according to the rules set.\n" + + "They apply from top to bottom just like automated design sets, so anything set by an earlier design will not not be set again by later designs, and order is important.\n" + + "If a linked design links to other designs, they will also be applied, so circular links are prohibited. "); + if (!h) + return; + + DrawList(); + } + + private void MoveLink() + { + if (_dragDropTargetIndex < 0 || _dragDropIndex < 0) + return; + + if (_dragDropOrder is LinkOrder.Self) + switch (_dragDropTargetOrder) + { + case LinkOrder.Before: + for (var i = _selector.Selected!.Links.Before.Count - 1; i >= _dragDropTargetIndex; --i) + _linkManager.MoveDesignLink(_selector.Selected!, i, LinkOrder.Before, 0, LinkOrder.After); + break; + case LinkOrder.After: + for (var i = 0; i <= _dragDropTargetIndex; ++i) + { + _linkManager.MoveDesignLink(_selector.Selected!, 0, LinkOrder.After, _selector.Selected!.Links.Before.Count, + LinkOrder.Before); + } + + break; + } + else if (_dragDropTargetOrder is LinkOrder.Self) + _linkManager.MoveDesignLink(_selector.Selected!, _dragDropIndex, _dragDropOrder, _selector.Selected!.Links.Before.Count, + LinkOrder.Before); + else + _linkManager.MoveDesignLink(_selector.Selected!, _dragDropIndex, _dragDropOrder, _dragDropTargetIndex, _dragDropTargetOrder); + + _dragDropIndex = -1; + _dragDropTargetIndex = -1; + _dragDropOrder = LinkOrder.None; + _dragDropTargetOrder = LinkOrder.None; + } + + private void DrawList() + { + using var table = ImRaii.Table("table", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter); + if (!table) + return; + + ImGui.TableSetupColumn("Del", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Detail", ImGuiTableColumnFlags.WidthFixed, + 6 * ImGui.GetFrameHeight() + 5 * ImGui.GetStyle().ItemInnerSpacing.X); + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); + DrawSubList(_selector.Selected!.Links.Before, LinkOrder.Before); + DrawSelf(); + DrawSubList(_selector.Selected!.Links.After, LinkOrder.After); + DrawNew(); + MoveLink(); + } + + private void DrawSelf() + { + using var id = ImRaii.PushId((int)LinkOrder.Self); + ImGui.TableNextColumn(); + var color = _colorManager.GetColor(_selector.Selected!); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + using var c = ImRaii.PushColor(ImGuiCol.Text, color); + ImGui.AlignTextToFramePadding(); + ImGuiUtil.RightAlign(FontAwesomeIcon.ArrowRightLong.ToIconString()); + } + + ImGui.TableNextColumn(); + using (ImRaii.PushColor(ImGuiCol.Text, color)) + { + ImGui.AlignTextToFramePadding(); + ImGui.Selectable(_selector.IncognitoMode ? _selector.Selected!.Incognito : _selector.Selected!.Name.Text); + } + + ImGuiUtil.HoverTooltip("Current Design"); + DrawDragDrop(_selector.Selected!, LinkOrder.Self, 0); + ImGui.TableNextColumn(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + using var c = ImRaii.PushColor(ImGuiCol.Text, color); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(FontAwesomeIcon.ArrowLeftLong.ToIconString()); + } + } + + private void DrawSubList(IReadOnlyList list, LinkOrder order) + { + using var id = ImRaii.PushId((int)order); + + var buttonSize = new Vector2(ImGui.GetFrameHeight()); + for (var i = 0; i < list.Count; ++i) + { + id.Push(i); + + ImGui.TableNextColumn(); + var delete = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), buttonSize, "Delete this link.", false, true); + var (design, flags) = list[i]; + ImGui.TableNextColumn(); + + using (ImRaii.PushColor(ImGuiCol.Text, _colorManager.GetColor(design))) + { + ImGui.AlignTextToFramePadding(); + ImGui.Selectable(_selector.IncognitoMode ? design.Incognito : design.Name.Text); + } + + DrawDragDrop(design, order, i); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + DrawApplicationBoxes(i, order, flags); + + if (delete) + _linkManager.RemoveDesignLink(_selector.Selected!, i--, order); + } + } + + private void DrawNew() + { + var buttonSize = new Vector2(ImGui.GetFrameHeight()); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + _combo.Draw(ImGui.GetContentRegionAvail().X); + ImGui.TableNextColumn(); + string ttBefore, ttAfter; + bool canAddBefore, canAddAfter; + var design = _combo.Design as Design; + if (design == null) + { + ttAfter = ttBefore = "Select a design first."; + canAddBefore = canAddAfter = false; + } + else + { + canAddBefore = LinkContainer.CanAddLink(_selector.Selected!, design, LinkOrder.Before, out var error); + ttBefore = canAddBefore + ? $"Add a link at the top of the list to {design.Name}." + : $"Can not add a link to {design.Name}:\n{error}"; + canAddAfter = LinkContainer.CanAddLink(_selector.Selected!, design, LinkOrder.After, out error); + ttAfter = canAddAfter + ? $"Add a link at the bottom of the list to {design.Name}." + : $"Can not add a link to {design.Name}:\n{error}"; + } + + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowCircleUp.ToIconString(), buttonSize, ttBefore, !canAddBefore, true)) + { + _linkManager.AddDesignLink(_selector.Selected!, design!, LinkOrder.Before); + _linkManager.MoveDesignLink(_selector.Selected!, _selector.Selected!.Links.Before.Count - 1, LinkOrder.Before, 0, LinkOrder.Before); + } + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowCircleDown.ToIconString(), buttonSize, ttAfter, !canAddAfter, true)) + _linkManager.AddDesignLink(_selector.Selected!, design!, LinkOrder.After); + } + + private void DrawDragDrop(Design design, LinkOrder order, int index) + { + using (var source = ImRaii.DragDropSource()) + { + if (source) + { + ImGui.SetDragDropPayload("DraggingLink", null, 0); + ImGui.TextUnformatted($"Reordering {design.Name}..."); + _dragDropIndex = index; + _dragDropOrder = order; + } + } + + using var target = ImRaii.DragDropTarget(); + if (!target) + return; + + if (!ImGuiUtil.IsDropping("DraggingLink")) + return; + + _dragDropTargetIndex = index; + _dragDropTargetOrder = order; + } + + private void DrawApplicationBoxes(int idx, LinkOrder order, ApplicationType current) + { + var newType = current; + var newTypeInt = (uint)newType; + using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale)) + { + using var _ = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()); + if (ImGui.CheckboxFlags("##all", ref newTypeInt, (uint)ApplicationType.All)) + newType = (ApplicationType)newTypeInt; + } + + ImGuiUtil.HoverTooltip("Toggle all application modes at once."); + + ImGui.SameLine(); + Box(0); + ImGui.SameLine(); + Box(1); + ImGui.SameLine(); + + Box(2); + ImGui.SameLine(); + Box(3); + ImGui.SameLine(); + Box(4); + if (newType != current) + _linkManager.ChangeApplicationType(_selector.Selected!, idx, order, newType); + return; + + void Box(int i) + { + var (applicationType, description) = ApplicationTypeExtensions.Types[i]; + var value = current.HasFlag(applicationType); + if (ImGui.Checkbox($"##{(byte)applicationType}", ref value)) + newType = value ? newType | applicationType : newType & ~applicationType; + ImGuiUtil.HoverTooltip(description); + } + } +} diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index 2cb843b..e3c965c 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -1,109 +1,109 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; +using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using FFXIVClientStructs.FFXIV.Client.System.Framework; +using Glamourer.Api.Enums; using Glamourer.Automation; -using Glamourer.Customization; using Glamourer.Designs; -using Glamourer.Events; +using Glamourer.Designs.History; +using Glamourer.GameData; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; +using Glamourer.Gui.Materials; using Glamourer.Interop; -using Glamourer.Services; using Glamourer.State; -using Glamourer.Structs; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using static Glamourer.Gui.Tabs.HeaderDrawer; namespace Glamourer.Gui.Tabs.DesignTab; public class DesignPanel { - private readonly ObjectManager _objects; + private readonly FileDialogManager _fileDialog = new(); private readonly DesignFileSystemSelector _selector; - private readonly DesignManager _manager; private readonly CustomizationDrawer _customizationDrawer; + private readonly DesignManager _manager; + private readonly ActorObjectManager _objects; private readonly StateManager _state; private readonly EquipmentDrawer _equipmentDrawer; - private readonly CustomizationService _customizationService; private readonly ModAssociationsTab _modAssociations; + private readonly Configuration _config; private readonly DesignDetailTab _designDetails; + private readonly ImportService _importService; private readonly DesignConverter _converter; - private readonly DatFileService _datFileService; - private readonly FileDialogManager _fileDialog = new(); + private readonly MultiDesignPanel _multiDesignPanel; + private readonly CustomizeParameterDrawer _parameterDrawer; + private readonly DesignLinkDrawer _designLinkDrawer; + private readonly MaterialDrawer _materials; + private readonly EditorHistory _history; + private readonly Button[] _leftButtons; + private readonly Button[] _rightButtons; - public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager, ObjectManager objects, - StateManager state, EquipmentDrawer equipmentDrawer, CustomizationService customizationService, ModAssociationsTab modAssociations, - DesignDetailTab designDetails, DesignConverter converter, DatFileService datFileService) + + public DesignPanel(DesignFileSystemSelector selector, + CustomizationDrawer customizationDrawer, + DesignManager manager, + ActorObjectManager objects, + StateManager state, + EquipmentDrawer equipmentDrawer, + ModAssociationsTab modAssociations, + Configuration config, + DesignDetailTab designDetails, + DesignConverter converter, + ImportService importService, + MultiDesignPanel multiDesignPanel, + CustomizeParameterDrawer parameterDrawer, + DesignLinkDrawer designLinkDrawer, + MaterialDrawer materials, + EditorHistory history) { - _selector = selector; - _customizationDrawer = customizationDrawer; - _manager = manager; - _objects = objects; - _state = state; - _equipmentDrawer = equipmentDrawer; - _customizationService = customizationService; - _modAssociations = modAssociations; - _designDetails = designDetails; - _converter = converter; - _datFileService = datFileService; + _selector = selector; + _customizationDrawer = customizationDrawer; + _manager = manager; + _objects = objects; + _state = state; + _equipmentDrawer = equipmentDrawer; + _modAssociations = modAssociations; + _config = config; + _designDetails = designDetails; + _importService = importService; + _converter = converter; + _multiDesignPanel = multiDesignPanel; + _parameterDrawer = parameterDrawer; + _designLinkDrawer = designLinkDrawer; + _materials = materials; + _history = history; + _leftButtons = + [ + new SetFromClipboardButton(this), + new DesignUndoButton(this), + new ExportToClipboardButton(this), + new ApplyCharacterButton(this), + new UndoButton(this), + ]; + _rightButtons = + [ + new LockButton(this), + new IncognitoButton(_config), + ]; } - private HeaderDrawer.Button LockButton() - => _selector.Selected == null - ? HeaderDrawer.Button.Invisible - : _selector.Selected.WriteProtected() - ? new HeaderDrawer.Button - { - Description = "Make this design editable.", - Icon = FontAwesomeIcon.Lock, - OnClick = () => _manager.SetWriteProtection(_selector.Selected!, false), - } - : new HeaderDrawer.Button - { - Description = "Write-protect this design.", - Icon = FontAwesomeIcon.LockOpen, - OnClick = () => _manager.SetWriteProtection(_selector.Selected!, true), - }; - - private HeaderDrawer.Button SetFromClipboardButton() - => new() - { - Description = - "Try to apply a design from your clipboard over this design.\nHold Control to only apply gear.\nHold Shift to only apply customizations.", - Icon = FontAwesomeIcon.Clipboard, - OnClick = SetFromClipboard, - Visible = _selector.Selected != null, - Disabled = _selector.Selected?.WriteProtected() ?? true, - }; - - private HeaderDrawer.Button ExportToClipboardButton() - => new() - { - Description = "Copy the current design to your clipboard.", - Icon = FontAwesomeIcon.Copy, - OnClick = ExportToClipboard, - Visible = _selector.Selected != null, - }; - private void DrawHeader() - => HeaderDrawer.Draw(SelectionName, 0, ImGui.GetColorU32(ImGuiCol.FrameBg), - 2, SetFromClipboardButton(), ExportToClipboardButton(), LockButton(), - HeaderDrawer.Button.IncognitoButton(_selector.IncognitoMode, v => _selector.IncognitoMode = v)); + => HeaderDrawer.Draw(SelectionName, 0, ImGui.GetColorU32(ImGuiCol.FrameBg), _leftButtons, _rightButtons); private string SelectionName => _selector.Selected == null ? "No Selection" : _selector.IncognitoMode ? _selector.Selected.Incognito : _selector.Selected.Name.Text; private void DrawEquipment() { - if (!ImGui.CollapsingHeader("Equipment")) + using var h = DesignPanelFlag.Equipment.Header(_config); + if (!h) return; _equipmentDrawer.Prepare(); @@ -111,79 +111,71 @@ public class DesignPanel var usedAllStain = _equipmentDrawer.DrawAllStain(out var newAllStain, _selector.Selected!.WriteProtected()); foreach (var slot in EquipSlotExtensions.EqdpSlots) { - var changes = _equipmentDrawer.DrawEquip(slot, _selector.Selected!.DesignData, out var newArmor, out var newStain, - _selector.Selected.ApplyEquip, out var newApply, out var newApplyStain, _selector.Selected!.WriteProtected()); - if (changes.HasFlag(DataChange.Item)) - _manager.ChangeEquip(_selector.Selected, slot, newArmor); - if (changes.HasFlag(DataChange.Stain)) - _manager.ChangeStain(_selector.Selected, slot, newStain); - else if (usedAllStain) - _manager.ChangeStain(_selector.Selected, slot, newAllStain); - if (changes.HasFlag(DataChange.ApplyItem)) - _manager.ChangeApplyEquip(_selector.Selected, slot, newApply); - if (changes.HasFlag(DataChange.ApplyStain)) - _manager.ChangeApplyStain(_selector.Selected, slot, newApplyStain); + var data = EquipDrawData.FromDesign(_manager, _selector.Selected!, slot); + _equipmentDrawer.DrawEquip(data); + if (usedAllStain) + _manager.ChangeStains(_selector.Selected, slot, newAllStain); } - var weaponChanges = _equipmentDrawer.DrawWeapons(_selector.Selected!.DesignData, out var newMainhand, out var newOffhand, - out var newMainhandStain, out var newOffhandStain, _selector.Selected.ApplyEquip, true, out var applyMain, out var applyMainStain, - out var applyOff, out var applyOffStain, _selector.Selected!.WriteProtected()); + var mainhand = EquipDrawData.FromDesign(_manager, _selector.Selected!, EquipSlot.MainHand); + var offhand = EquipDrawData.FromDesign(_manager, _selector.Selected!, EquipSlot.OffHand); + _equipmentDrawer.DrawWeapons(mainhand, offhand, true); - if (weaponChanges.HasFlag(DataChange.Item)) - _manager.ChangeWeapon(_selector.Selected, EquipSlot.MainHand, newMainhand); - if (weaponChanges.HasFlag(DataChange.Stain)) - _manager.ChangeStain(_selector.Selected, EquipSlot.MainHand, newMainhandStain); - else if (usedAllStain) - _manager.ChangeStain(_selector.Selected, EquipSlot.MainHand, newAllStain); - if (weaponChanges.HasFlag(DataChange.ApplyItem)) - _manager.ChangeApplyEquip(_selector.Selected, EquipSlot.MainHand, applyMain); - if (weaponChanges.HasFlag(DataChange.ApplyStain)) - _manager.ChangeApplyStain(_selector.Selected, EquipSlot.MainHand, applyMainStain); - if (weaponChanges.HasFlag(DataChange.Item2)) - _manager.ChangeWeapon(_selector.Selected, EquipSlot.OffHand, newOffhand); - if (weaponChanges.HasFlag(DataChange.Stain2)) - _manager.ChangeStain(_selector.Selected, EquipSlot.OffHand, newOffhandStain); - else if (usedAllStain) - _manager.ChangeStain(_selector.Selected, EquipSlot.OffHand, newAllStain); - if (weaponChanges.HasFlag(DataChange.ApplyItem2)) - _manager.ChangeApplyEquip(_selector.Selected, EquipSlot.OffHand, applyOff); - if (weaponChanges.HasFlag(DataChange.ApplyStain2)) - _manager.ChangeApplyStain(_selector.Selected, EquipSlot.OffHand, applyOffStain); + foreach (var slot in BonusExtensions.AllFlags) + { + var data = BonusDrawData.FromDesign(_manager, _selector.Selected!, slot); + _equipmentDrawer.DrawBonusItem(data); + } ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); DrawEquipmentMetaToggles(); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + _equipmentDrawer.DrawDragDropTooltip(); } private void DrawEquipmentMetaToggles() { - var hatChanges = EquipmentDrawer.DrawHatState(_selector.Selected!.DesignData.IsHatVisible(), - _selector.Selected.DoApplyHatVisible(), - out var newHatState, out var newHatApply, _selector.Selected.WriteProtected()); - ApplyChanges(ActorState.MetaIndex.HatState, hatChanges, newHatState, newHatApply); + using (var _ = ImRaii.Group()) + { + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(MetaIndex.HatState, _manager, _selector.Selected!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromDesign(CrestFlag.Head, _manager, _selector.Selected!)); + } ImGui.SameLine(); - var visorChanges = EquipmentDrawer.DrawVisorState(_selector.Selected!.DesignData.IsVisorToggled(), - _selector.Selected.DoApplyVisorToggle(), - out var newVisorState, out var newVisorApply, _selector.Selected.WriteProtected()); - ApplyChanges(ActorState.MetaIndex.VisorState, visorChanges, newVisorState, newVisorApply); + using (var _ = ImRaii.Group()) + { + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(MetaIndex.VisorState, _manager, _selector.Selected!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromDesign(CrestFlag.Body, _manager, _selector.Selected!)); + } ImGui.SameLine(); - var weaponChanges = EquipmentDrawer.DrawWeaponState(_selector.Selected!.DesignData.IsWeaponVisible(), - _selector.Selected.DoApplyWeaponVisible(), - out var newWeaponState, out var newWeaponApply, _selector.Selected.WriteProtected()); - ApplyChanges(ActorState.MetaIndex.WeaponState, weaponChanges, newWeaponState, newWeaponApply); + using (var _ = ImRaii.Group()) + { + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(MetaIndex.WeaponState, _manager, _selector.Selected!)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromDesign(CrestFlag.OffHand, _manager, _selector.Selected!)); + } + + ImGui.SameLine(); + using (var _ = ImRaii.Group()) + { + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(MetaIndex.EarState, _manager, _selector.Selected!)); + } } private void DrawCustomize() { + if (_config.HideDesignPanel.HasFlag(DesignPanelFlag.Customization)) + return; + var header = _selector.Selected!.DesignData.ModelId == 0 ? "Customization" : $"Customization (Model Id #{_selector.Selected!.DesignData.ModelId})###Customization"; - if (!ImGui.CollapsingHeader(header)) + var expand = _config.AutoExpandDesignPanel.HasFlag(DesignPanelFlag.Customization); + using var h = ImUtf8.CollapsingHeaderId(header, expand ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None); + if (!h) return; - if (_customizationDrawer.Draw(_selector.Selected!.DesignData.Customize, _selector.Selected.ApplyCustomize, + if (_customizationDrawer.Draw(_selector.Selected!.DesignData.Customize, _selector.Selected.Application.Customize, _selector.Selected!.WriteProtected(), false)) foreach (var idx in Enum.GetValues()) { @@ -194,122 +186,293 @@ public class DesignPanel _manager.ChangeCustomize(_selector.Selected, idx, _customizationDrawer.Customize[idx]); } - var wetnessChanges = _customizationDrawer.DrawWetnessState(_selector.Selected!.DesignData.IsWet(), - _selector.Selected!.DoApplyWetness(), out var newWetnessState, out var newWetnessApply, _selector.Selected!.WriteProtected()); - ApplyChanges(ActorState.MetaIndex.Wetness, wetnessChanges, newWetnessState, newWetnessApply); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(MetaIndex.Wetness, _manager, _selector.Selected!)); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); } + private void DrawCustomizeParameters() + { + using var h = DesignPanelFlag.AdvancedCustomizations.Header(_config); + if (!h) + return; + + _parameterDrawer.Draw(_manager, _selector.Selected!); + } + + private void DrawMaterialValues() + { + using var h = DesignPanelFlag.AdvancedDyes.Header(_config); + if (!h) + return; + + _materials.Draw(_selector.Selected!); + } + + private void DrawCustomizeApplication() + { + using var id = ImUtf8.PushId("Customizations"u8); + var set = _selector.Selected!.CustomizeSet; + var available = set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.BodyType; + var flags = _selector.Selected!.ApplyCustomizeExcludingBodyType == 0 ? 0 : + (_selector.Selected!.ApplyCustomize & available) == available ? 3 : 1; + if (ImGui.CheckboxFlags("Apply All Customizations", ref flags, 3)) + { + var newFlags = flags == 3; + _manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Clan, newFlags); + _manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Gender, newFlags); + foreach (var index in CustomizationExtensions.AllBasic) + _manager.ChangeApplyCustomize(_selector.Selected!, index, newFlags); + } + + var applyClan = _selector.Selected!.DoApplyCustomize(CustomizeIndex.Clan); + if (ImUtf8.Checkbox($"Apply {CustomizeIndex.Clan.ToDefaultName()}", ref applyClan)) + _manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Clan, applyClan); + + var applyGender = _selector.Selected!.DoApplyCustomize(CustomizeIndex.Gender); + if (ImUtf8.Checkbox($"Apply {CustomizeIndex.Gender.ToDefaultName()}", ref applyGender)) + _manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Gender, applyGender); + + + foreach (var index in CustomizationExtensions.All.Where(set.IsAvailable)) + { + var apply = _selector.Selected!.DoApplyCustomize(index); + if (ImUtf8.Checkbox($"Apply {set.Option(index)}", ref apply)) + _manager.ChangeApplyCustomize(_selector.Selected!, index, apply); + } + } + + private void DrawCrestApplication() + { + using var id = ImUtf8.PushId("Crests"u8); + var flags = (uint)_selector.Selected!.Application.Crest; + var bigChange = ImGui.CheckboxFlags("Apply All Crests", ref flags, (uint)CrestExtensions.AllRelevant); + foreach (var flag in CrestExtensions.AllRelevantSet) + { + var apply = bigChange ? ((CrestFlag)flags & flag) == flag : _selector.Selected!.DoApplyCrest(flag); + if (ImUtf8.Checkbox($"Apply {flag.ToLabel()} Crest", ref apply) || bigChange) + _manager.ChangeApplyCrest(_selector.Selected!, flag, apply); + } + } + private void DrawApplicationRules() { - if (!ImGui.CollapsingHeader("Application Rules")) + using var h = DesignPanelFlag.ApplicationRules.Header(_config); + if (!h) return; - using (var group1 = ImRaii.Group()) + using var disabled = ImRaii.Disabled(_selector.Selected!.WriteProtected()); + + DrawAllButtons(); + + using (var _ = ImUtf8.Group()) { - var set = _customizationService.AwaitedService.GetList(_selector.Selected!.DesignData.Customize.Clan, - _selector.Selected!.DesignData.Customize.Gender); - var all = CustomizationExtensions.All.Where(set.IsAvailable).Select(c => c.ToFlag()).Aggregate((a, b) => a | b) - | CustomizeFlag.Clan - | CustomizeFlag.Gender; - var flags = (_selector.Selected!.ApplyCustomize & all) == 0 ? 0 : (_selector.Selected!.ApplyCustomize & all) == all ? 3 : 1; - if (ImGui.CheckboxFlags("Apply All Customizations", ref flags, 3)) - { - var newFlags = flags == 3; - _manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Clan, newFlags); - _manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Gender, newFlags); - foreach (var index in CustomizationExtensions.AllBasic.Where(set.IsAvailable)) - _manager.ChangeApplyCustomize(_selector.Selected!, index, newFlags); - } - - var applyClan = _selector.Selected!.DoApplyCustomize(CustomizeIndex.Clan); - if (ImGui.Checkbox($"Apply {CustomizeIndex.Clan.ToDefaultName()}", ref applyClan)) - _manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Clan, applyClan); - - var applyGender = _selector.Selected!.DoApplyCustomize(CustomizeIndex.Gender); - if (ImGui.Checkbox($"Apply {CustomizeIndex.Gender.ToDefaultName()}", ref applyGender)) - _manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Gender, applyGender); - - - foreach (var index in CustomizationExtensions.All.Where(set.IsAvailable)) - { - var apply = _selector.Selected!.DoApplyCustomize(index); - if (ImGui.Checkbox($"Apply {index.ToDefaultName()}", ref apply)) - _manager.ChangeApplyCustomize(_selector.Selected!, index, apply); - } + DrawCustomizeApplication(); + ImUtf8.IconDummy(); + DrawCrestApplication(); + ImUtf8.IconDummy(); + DrawMetaApplication(); } - ImGui.SameLine(ImGui.GetContentRegionAvail().X / 2); - using (var group2 = ImRaii.Group()) + ImGui.SameLine(210 * ImUtf8.GlobalScale + ImGui.GetStyle().ItemSpacing.X); + using (var _ = ImRaii.Group()) { - void ApplyEquip(string label, EquipFlag all, bool stain, IEnumerable slots) + void ApplyEquip(string label, EquipFlag allFlags, bool stain, IEnumerable slots) { - var flags = (uint)(all & _selector.Selected!.ApplyEquip); - - var bigChange = ImGui.CheckboxFlags($"Apply All {label}", ref flags, (uint)all); + var flags = (uint)(allFlags & _selector.Selected!.Application.Equip); + using var id = ImUtf8.PushId(label); + var bigChange = ImGui.CheckboxFlags($"Apply All {label}", ref flags, (uint)allFlags); if (stain) foreach (var slot in slots) { var apply = bigChange ? ((EquipFlag)flags).HasFlag(slot.ToStainFlag()) : _selector.Selected!.DoApplyStain(slot); - if (ImGui.Checkbox($"Apply {slot.ToName()} Dye", ref apply) || bigChange) - _manager.ChangeApplyStain(_selector.Selected!, slot, apply); + if (ImUtf8.Checkbox($"Apply {slot.ToName()} Dye", ref apply) || bigChange) + _manager.ChangeApplyStains(_selector.Selected!, slot, apply); } else foreach (var slot in slots) { var apply = bigChange ? ((EquipFlag)flags).HasFlag(slot.ToFlag()) : _selector.Selected!.DoApplyEquip(slot); - if (ImGui.Checkbox($"Apply {slot.ToName()}", ref apply) || bigChange) - _manager.ChangeApplyEquip(_selector.Selected!, slot, apply); + if (ImUtf8.Checkbox($"Apply {slot.ToName()}", ref apply) || bigChange) + _manager.ChangeApplyItem(_selector.Selected!, slot, apply); } } - ApplyEquip("Weapons", AutoDesign.WeaponFlags, false, new[] + ApplyEquip("Weapons", ApplicationTypeExtensions.WeaponFlags, false, new[] { EquipSlot.MainHand, EquipSlot.OffHand, }); - ImGui.NewLine(); - ApplyEquip("Armor", AutoDesign.ArmorFlags, false, EquipSlotExtensions.EquipmentSlots); + ImUtf8.IconDummy(); + ApplyEquip("Armor", ApplicationTypeExtensions.ArmorFlags, false, EquipSlotExtensions.EquipmentSlots); - ImGui.NewLine(); - ApplyEquip("Accessories", AutoDesign.AccessoryFlags, false, EquipSlotExtensions.AccessorySlots); + ImUtf8.IconDummy(); + ApplyEquip("Accessories", ApplicationTypeExtensions.AccessoryFlags, false, EquipSlotExtensions.AccessorySlots); - ImGui.NewLine(); - ApplyEquip("Dyes", AutoDesign.StainFlags, true, + ImUtf8.IconDummy(); + ApplyEquip("Dyes", ApplicationTypeExtensions.StainFlags, true, EquipSlotExtensions.FullSlots); - ImGui.NewLine(); - const uint all = 0x0Fu; - var flags = (_selector.Selected!.DoApplyHatVisible() ? 0x01u : 0x00) - | (_selector.Selected!.DoApplyVisorToggle() ? 0x02u : 0x00) - | (_selector.Selected!.DoApplyWeaponVisible() ? 0x04u : 0x00) - | (_selector.Selected!.DoApplyWetness() ? 0x08u : 0x00); - var bigChange = ImGui.CheckboxFlags("Apply All Meta Changes", ref flags, all); - var apply = bigChange ? (flags & 0x01) == 0x01 : _selector.Selected!.DoApplyHatVisible(); - if (ImGui.Checkbox("Apply Hat Visibility", ref apply) || bigChange) - _manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.HatState, apply); + ImUtf8.IconDummy(); + DrawParameterApplication(); - apply = bigChange ? (flags & 0x02) == 0x02 : _selector.Selected!.DoApplyVisorToggle(); - if (ImGui.Checkbox("Apply Visor State", ref apply) || bigChange) - _manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.VisorState, apply); + ImUtf8.IconDummy(); + DrawBonusSlotApplication(); + } + } - apply = bigChange ? (flags & 0x04) == 0x04 : _selector.Selected!.DoApplyWeaponVisible(); - if (ImGui.Checkbox("Apply Weapon Visibility", ref apply) || bigChange) - _manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.WeaponState, apply); + private void DrawAllButtons() + { + var enabled = _config.DeleteDesignModifier.IsActive(); + bool? equip = null; + bool? customize = null; + var size = new Vector2(210 * ImUtf8.GlobalScale, 0); + if (ImUtf8.ButtonEx("Disable Everything"u8, + "Disable application of everything, including any existing advanced dyes, advanced customizations, crests and wetness."u8, size, + !enabled)) + { + equip = false; + customize = false; + } - apply = bigChange ? (flags & 0x08) == 0x08 : _selector.Selected!.DoApplyWetness(); - if (ImGui.Checkbox("Apply Wetness", ref apply) || bigChange) - _manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.Wetness, apply); + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteDesignModifier} while clicking."); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Enable Everything"u8, + "Enable application of everything, including any existing advanced dyes, advanced customizations, crests and wetness."u8, size, + !enabled)) + { + equip = true; + customize = true; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteDesignModifier} while clicking."); + + if (ImUtf8.ButtonEx("Equipment Only"u8, + "Enable application of anything related to gear, disable anything that is not related to gear."u8, size, + !enabled)) + { + equip = true; + customize = false; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteDesignModifier} while clicking."); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Customization Only"u8, + "Enable application of anything related to customization, disable anything that is not related to customization."u8, size, + !enabled)) + { + equip = false; + customize = true; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteDesignModifier} while clicking."); + + if (ImUtf8.ButtonEx("Default Application"u8, + "Set the application rules to the default values as if the design was newly created, without any advanced features or wetness."u8, + size, + !enabled)) + { + _manager.ChangeApplyMulti(_selector.Selected!, true, true, true, false, true, true, false, true); + _manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.Wetness, false); + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteDesignModifier} while clicking."); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Disable Advanced"u8, "Disable all advanced dyes and customizations but keep everything else as is."u8, + size, + !enabled)) + _manager.ChangeApplyMulti(_selector.Selected!, null, null, null, false, null, null, false, null); + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteDesignModifier} while clicking."); + + if (equip is null && customize is null) + return; + + _manager.ChangeApplyMulti(_selector.Selected!, equip, customize, equip, customize.HasValue && !customize.Value ? false : null, null, + equip, equip, equip); + if (equip.HasValue) + { + _manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.HatState, equip.Value); + _manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.VisorState, equip.Value); + _manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.WeaponState, equip.Value); + _manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.EarState, equip.Value); + } + + if (customize.HasValue) + _manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.Wetness, customize.Value); + } + + private static readonly IReadOnlyList MetaLabels = + [ + "Apply Wetness", + "Apply Hat Visibility", + "Apply Visor State", + "Apply Weapon Visibility", + "Apply Viera Ear Visibility", + ]; + + private void DrawMetaApplication() + { + using var id = ImUtf8.PushId("Meta"); + const uint all = (uint)MetaExtensions.All; + var flags = (uint)_selector.Selected!.Application.Meta; + var bigChange = ImGui.CheckboxFlags("Apply All Meta Changes", ref flags, all); + + foreach (var (index, label) in MetaExtensions.AllRelevant.Zip(MetaLabels)) + { + var apply = bigChange ? ((MetaFlag)flags).HasFlag(index.ToFlag()) : _selector.Selected!.DoApplyMeta(index); + if (ImUtf8.Checkbox(label, ref apply) || bigChange) + _manager.ChangeApplyMeta(_selector.Selected!, index, apply); + } + } + + private static readonly IReadOnlyList BonusSlotLabels = + [ + "Apply Facewear", + ]; + + private void DrawBonusSlotApplication() + { + using var id = ImUtf8.PushId("Bonus"u8); + var flags = _selector.Selected!.Application.BonusItem; + var bigChange = BonusExtensions.AllFlags.Count > 1 && ImUtf8.Checkbox("Apply All Bonus Slots"u8, ref flags, BonusExtensions.All); + foreach (var (index, label) in BonusExtensions.AllFlags.Zip(BonusSlotLabels)) + { + var apply = bigChange ? flags.HasFlag(index) : _selector.Selected!.DoApplyBonusItem(index); + if (ImUtf8.Checkbox(label, ref apply) || bigChange) + _manager.ChangeApplyBonusItem(_selector.Selected!, index, apply); + } + } + + + private void DrawParameterApplication() + { + using var id = ImUtf8.PushId("Parameter"); + var flags = (uint)_selector.Selected!.Application.Parameters; + var bigChange = ImGui.CheckboxFlags("Apply All Customize Parameters", ref flags, (uint)CustomizeParameterExtensions.All); + foreach (var flag in CustomizeParameterExtensions.AllFlags) + { + var apply = bigChange ? ((CustomizeParameterFlag)flags).HasFlag(flag) : _selector.Selected!.DoApplyParameter(flag); + if (ImUtf8.Checkbox($"Apply {flag.ToName()}", ref apply) || bigChange) + _manager.ChangeApplyParameter(_selector.Selected!, flag, apply); } } public void Draw() { - using var group = ImRaii.Group(); + using var group = ImUtf8.Group(); if (_selector.SelectedPaths.Count > 1) { - DrawMultiSelection(); + _multiDesignPanel.Draw(); } else { @@ -319,68 +482,49 @@ public class DesignPanel if (_selector.Selected == null || _selector.Selected.WriteProtected()) return; - if (_datFileService.CreateImGuiTarget(out var dat)) + if (_importService.CreateDatTarget(out var dat)) { _manager.ChangeCustomize(_selector.Selected!, CustomizeIndex.Clan, dat.Customize[CustomizeIndex.Clan]); _manager.ChangeCustomize(_selector.Selected!, CustomizeIndex.Gender, dat.Customize[CustomizeIndex.Gender]); foreach (var idx in CustomizationExtensions.AllBasic) _manager.ChangeCustomize(_selector.Selected!, idx, dat.Customize[idx]); + Glamourer.Messager.NotificationMessage( + $"Applied games .dat file {dat.Description} customizations to {_selector.Selected.Name}.", NotificationType.Success, false); + } + else if (_importService.CreateCharaTarget(out var designBase, out var name)) + { + _manager.ApplyDesign(_selector.Selected!, designBase); + Glamourer.Messager.NotificationMessage($"Applied Anamnesis .chara file {name} to {_selector.Selected.Name}.", + NotificationType.Success, false); } } - _datFileService.CreateSource(); - } - - private void DrawMultiSelection() - { - if (_selector.SelectedPaths.Count == 0) - return; - - var sizeType = ImGui.GetFrameHeight(); - var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType - 4 * ImGui.GetStyle().CellPadding.X) / 100; - var sizeMods = availableSizePercent * 35; - var sizeFolders = availableSizePercent * 65; - - ImGui.NewLine(); - ImGui.TextUnformatted("Currently Selected Objects"); - ImGui.Separator(); - using var table = ImRaii.Table("mods", 3, ImGuiTableFlags.RowBg); - ImGui.TableSetupColumn("type", ImGuiTableColumnFlags.WidthFixed, sizeType); - ImGui.TableSetupColumn("mod", ImGuiTableColumnFlags.WidthFixed, sizeMods); - ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthFixed, sizeFolders); - - var i = 0; - foreach (var (fullName, path) in _selector.SelectedPaths.Select(p => (p.FullName(), p)) - .OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase)) - { - using var id = ImRaii.PushId(i++); - ImGui.TableNextColumn(); - var icon = (path is DesignFileSystem.Leaf ? FontAwesomeIcon.FileCircleMinus : FontAwesomeIcon.FolderMinus).ToIconString(); - if (ImGuiUtil.DrawDisabledButton(icon, new Vector2(sizeType), "Remove from selection.", false, true)) - _selector.RemovePathFromMultiselection(path); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(path is DesignFileSystem.Leaf l ? l.Value.Name : string.Empty); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(fullName); - } + _importService.CreateDatSource(); } private void DrawPanel() { - using var child = ImRaii.Child("##Panel", -Vector2.One, true); - if (!child || _selector.Selected == null) + using var table = ImUtf8.Table("##Panel", 1, ImGuiTableFlags.BordersOuter | ImGuiTableFlags.ScrollY, ImGui.GetContentRegionAvail()); + if (!table || _selector.Selected == null) return; + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableNextColumn(); + if (_selector.Selected == null) + return; + + ImGui.Dummy(Vector2.Zero); DrawButtonRow(); + ImGui.TableNextColumn(); + DrawCustomize(); DrawEquipment(); + DrawCustomizeParameters(); + DrawMaterialValues(); _designDetails.Draw(); DrawApplicationRules(); _modAssociations.Draw(); + _designLinkDrawer.Draw(); } private void DrawButtonRow() @@ -394,36 +538,6 @@ public class DesignPanel DrawSaveToDat(); } - private void SetFromClipboard() - { - try - { - var text = ImGui.GetClipboardText(); - var (applyEquip, applyCustomize) = UiHelpers.ConvertKeysToBool(); - var design = _converter.FromBase64(text, applyCustomize, applyEquip, out _) - ?? throw new Exception("The clipboard did not contain valid data."); - _manager.ApplyDesign(_selector.Selected!, design); - } - catch (Exception ex) - { - Glamourer.Messager.NotificationMessage(ex, $"Could not apply clipboard to {_selector.Selected!.Name}.", - $"Could not apply clipboard to design {_selector.Selected!.Identifier}", NotificationType.Error, false); - } - } - - private void ExportToClipboard() - { - try - { - var text = _converter.ShareBase64(_selector.Selected!); - ImGui.SetClipboardText(text); - } - catch (Exception ex) - { - Glamourer.Messager.NotificationMessage(ex, $"Could not copy {_selector.Selected!.Name} data to clipboard.", - $"Could not copy data from design {_selector.Selected!.Identifier} to clipboard", NotificationType.Error, false); - } - } private void DrawApplyToSelf() { @@ -435,9 +549,8 @@ public class DesignPanel if (_state.GetOrCreate(id, data.Objects[0], out var state)) { - var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags(); - using var _ = _selector.Selected!.TemporarilyRestrictApplication(applyGear, applyCustomize); - _state.ApplyDesign(_selector.Selected!, state, StateChanged.Source.Manual); + using var _ = _selector.Selected!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); + _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks with { IsFinal = true }); } } @@ -449,20 +562,19 @@ public class DesignPanel ? "Apply the current design with its settings to your current target.\nHold Control to only apply gear.\nHold Shift to only apply customizations." : "The current target can not be manipulated." : "No valid target selected."; - if (!ImGuiUtil.DrawDisabledButton("Apply to Target", Vector2.Zero, tt, !data.Valid || _objects.IsInGPose)) + if (!ImGuiUtil.DrawDisabledButton("Apply to Target", Vector2.Zero, tt, !data.Valid)) return; if (_state.GetOrCreate(id, data.Objects[0], out var state)) { - var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags(); - using var _ = _selector.Selected!.TemporarilyRestrictApplication(applyGear, applyCustomize); - _state.ApplyDesign(_selector.Selected!, state, StateChanged.Source.Manual); + using var _ = _selector.Selected!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); + _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks with { IsFinal = true }); } } private void DrawSaveToDat() { - var verified = _datFileService.Verify(_selector.Selected!.DesignData.Customize, out var voice); + var verified = _importService.Verify(_selector.Selected!.DesignData.Customize, out _); var tt = verified ? "Export the currently configured customizations of this design to a character creation data file." : "The current design contains customizations that can not be applied during character creation."; @@ -473,29 +585,172 @@ public class DesignPanel _fileDialog.SaveFileDialog("Save File...", ".dat", "FFXIV_CHARA_01.dat", ".dat", (v, path) => { if (v && _selector.Selected != null) - _datFileService.SaveDesign(path, _selector.Selected!.DesignData.Customize, _selector.Selected!.Name); + _importService.SaveDesignAsDat(path, _selector.Selected!.DesignData.Customize, _selector.Selected!.Name); }, startPath); _fileDialog.Draw(); } - private void ApplyChanges(ActorState.MetaIndex index, DataChange change, bool value, bool apply) + private static unsafe string GetUserPath() + => Framework.Instance()->UserPathString; + + + private sealed class LockButton(DesignPanel panel) : Button { - switch (change) + public override bool Visible + => panel._selector.Selected != null; + + protected override string Description + => panel._selector.Selected!.WriteProtected() + ? "Make this design editable." + : "Write-protect this design."; + + protected override FontAwesomeIcon Icon + => panel._selector.Selected!.WriteProtected() + ? FontAwesomeIcon.Lock + : FontAwesomeIcon.LockOpen; + + protected override void OnClick() + => panel._manager.SetWriteProtection(panel._selector.Selected!, !panel._selector.Selected!.WriteProtected()); + } + + private sealed class SetFromClipboardButton(DesignPanel panel) : Button + { + public override bool Visible + => panel._selector.Selected != null; + + protected override bool Disabled + => panel._selector.Selected?.WriteProtected() ?? true; + + protected override string Description + => "Try to apply a design from your clipboard over this design.\nHold Control to only apply gear.\nHold Shift to only apply customizations."; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.Clipboard; + + protected override void OnClick() { - case DataChange.Item: - _manager.ChangeMeta(_selector.Selected!, index, value); - break; - case DataChange.ApplyItem: - _manager.ChangeApplyMeta(_selector.Selected!, index, apply); - break; - case DataChange.Item | DataChange.ApplyItem: - _manager.ChangeApplyMeta(_selector.Selected!, index, apply); - _manager.ChangeMeta(_selector.Selected!, index, value); - break; + try + { + var text = ImGui.GetClipboardText(); + var (applyEquip, applyCustomize) = UiHelpers.ConvertKeysToBool(); + var design = panel._converter.FromBase64(text, applyCustomize, applyEquip, out _) + ?? throw new Exception("The clipboard did not contain valid data."); + panel._manager.ApplyDesign(panel._selector.Selected!, design); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Could not apply clipboard to {panel._selector.Selected!.Name}.", + $"Could not apply clipboard to design {panel._selector.Selected!.Identifier}", NotificationType.Error, false); + } } } - private static unsafe string GetUserPath() - => Framework.Instance()->UserPath; + private sealed class DesignUndoButton(DesignPanel panel) : Button + { + public override bool Visible + => panel._selector.Selected != null; + + protected override bool Disabled + => !panel._manager.CanUndo(panel._selector.Selected) || (panel._selector.Selected?.WriteProtected() ?? true); + + protected override string Description + => "Undo the last time you applied an entire design onto this design, if you accidentally overwrote your design with a different one."; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.SyncAlt; + + protected override void OnClick() + { + try + { + panel._manager.UndoDesignChange(panel._selector.Selected!); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Could not undo last changes to {panel._selector.Selected!.Name}.", + NotificationType.Error, + false); + } + } + } + + private sealed class ExportToClipboardButton(DesignPanel panel) : Button + { + public override bool Visible + => panel._selector.Selected != null; + + protected override string Description + => "Copy the current design to your clipboard."; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.Copy; + + protected override void OnClick() + { + try + { + var text = panel._converter.ShareBase64(panel._selector.Selected!); + ImGui.SetClipboardText(text); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Could not copy {panel._selector.Selected!.Name} data to clipboard.", + $"Could not copy data from design {panel._selector.Selected!.Identifier} to clipboard", NotificationType.Error, false); + } + } + } + + private sealed class ApplyCharacterButton(DesignPanel panel) : Button + { + public override bool Visible + => panel._selector.Selected != null && panel._objects.Player.Valid; + + protected override string Description + => "Overwrite this design with your character's current state."; + + protected override bool Disabled + => panel._selector.Selected?.WriteProtected() ?? true; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.UserEdit; + + protected override void OnClick() + { + try + { + var (player, actor) = panel._objects.PlayerData; + if (!player.IsValid || !actor.Valid || !panel._state.GetOrCreate(player, actor.Objects[0], out var state)) + throw new Exception("No player state available."); + + var design = panel._converter.Convert(state, ApplicationRules.FromModifiers(state)) + ?? throw new Exception("The clipboard did not contain valid data."); + panel._selector.Selected!.GetMaterialDataRef().Clear(); + panel._manager.ApplyDesign(panel._selector.Selected!, design); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Could not apply player state to {panel._selector.Selected!.Name}.", + $"Could not apply player state to design {panel._selector.Selected!.Identifier}", NotificationType.Error, false); + } + } + } + + private sealed class UndoButton(DesignPanel panel) : Button + { + protected override string Description + => "Undo the last change."; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.Undo; + + public override bool Visible + => panel._selector.Selected != null; + + protected override bool Disabled + => (panel._selector.Selected?.WriteProtected() ?? true) || !panel._history.CanUndo(panel._selector.Selected); + + protected override void OnClick() + => panel._history.Undo(panel._selector.Selected!); + } } diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignTab.cs b/Glamourer/Gui/Tabs/DesignTab/DesignTab.cs index 243fc9c..1b92291 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignTab.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignTab.cs @@ -1,31 +1,30 @@ -using System; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility; -using ImGuiNET; +using Glamourer.Designs; +using Glamourer.Interop; +using Dalamud.Bindings.ImGui; +using OtterGui.Classes; using OtterGui.Widgets; namespace Glamourer.Gui.Tabs.DesignTab; -public class DesignTab : ITab +public class DesignTab(DesignFileSystemSelector _selector, DesignPanel _panel, ImportService _importService, DesignManager _manager) + : ITab { - public readonly DesignFileSystemSelector Selector; - private readonly DesignPanel _panel; - - public DesignTab(DesignFileSystemSelector selector, DesignPanel panel) - { - Selector = selector; - _panel = panel; - } - public ReadOnlySpan Label => "Designs"u8; public void DrawContent() { - Selector.Draw(GetDesignSelectorSize()); + _selector.Draw(); + if (_importService.CreateCharaTarget(out var designBase, out var name)) + { + var newDesign = _manager.CreateClone(designBase, name, true); + Glamourer.Messager.NotificationMessage($"Imported Anamnesis .chara file {name} as new design {newDesign.Name}", NotificationType.Success, false); + } + ImGui.SameLine(); _panel.Draw(); + _importService.CreateCharaSource(); } - - public float GetDesignSelectorSize() - => 200f * ImGuiHelpers.GlobalScale; } diff --git a/Glamourer/Gui/Tabs/DesignTab/ModAssociationsTab.cs b/Glamourer/Gui/Tabs/DesignTab/ModAssociationsTab.cs index 29c8ab7..587fe65 100644 --- a/Glamourer/Gui/Tabs/DesignTab/ModAssociationsTab.cs +++ b/Glamourer/Gui/Tabs/DesignTab/ModAssociationsTab.cs @@ -1,130 +1,231 @@ -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility; using Dalamud.Utility; using Glamourer.Designs; using Glamourer.Interop.Penumbra; -using ImGuiNET; +using Glamourer.State; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Text.Widget; namespace Glamourer.Gui.Tabs.DesignTab; -public class ModAssociationsTab +public class ModAssociationsTab(PenumbraService penumbra, DesignFileSystemSelector selector, DesignManager manager, Configuration config) { - private readonly PenumbraService _penumbra; - private readonly DesignFileSystemSelector _selector; - private readonly DesignManager _manager; - private readonly ModCombo _modCombo; - - public ModAssociationsTab(PenumbraService penumbra, DesignFileSystemSelector selector, DesignManager manager) - { - _penumbra = penumbra; - _selector = selector; - _manager = manager; - _modCombo = new ModCombo(penumbra, Glamourer.Log); - } + private readonly ModCombo _modCombo = new(penumbra, Glamourer.Log, selector); + private (Mod, ModSettings)[]? _copy; public void Draw() { - var headerOpen = ImGui.CollapsingHeader("Mod Associations"); + using var h = DesignPanelFlag.ModAssociations.Header(config); + if (h.Disposed) + return; + ImGuiUtil.HoverTooltip( "This tab can store information about specific mods associated with this design.\n\n" + "It does NOT change any mod settings automatically, though there is functionality to apply desired mod settings manually.\n" + "You can also use it to quickly open the associated mod page in Penumbra.\n\n" + "It is not feasible to apply those changes automatically in general cases, since there would be no way to revert those changes, handle multiple designs applying at once, etc."); - if (!headerOpen) + if (!h) return; DrawApplyAllButton(); DrawTable(); + DrawCopyButtons(); + } + + private void DrawCopyButtons() + { + var size = new Vector2((ImGui.GetContentRegionAvail().X - 2 * ImGui.GetStyle().ItemSpacing.X) / 3, 0); + if (ImGui.Button("Copy All to Clipboard", size)) + _copy = selector.Selected!.AssociatedMods.Select(kvp => (kvp.Key, kvp.Value)).ToArray(); + + ImGui.SameLine(); + + if (ImGuiUtil.DrawDisabledButton("Add from Clipboard", size, + _copy != null + ? $"Add {_copy.Length} mod association(s) from clipboard." + : "Copy some mod associations to the clipboard, first.", _copy == null)) + foreach (var (mod, setting) in _copy!) + manager.UpdateMod(selector.Selected!, mod, setting); + + ImGui.SameLine(); + + if (ImGuiUtil.DrawDisabledButton("Set from Clipboard", size, + _copy != null + ? $"Set {_copy.Length} mod association(s) from clipboard and discard existing." + : "Copy some mod associations to the clipboard, first.", _copy == null)) + { + while (selector.Selected!.AssociatedMods.Count > 0) + manager.RemoveMod(selector.Selected!, selector.Selected!.AssociatedMods.Keys[0]); + foreach (var (mod, setting) in _copy!) + manager.AddMod(selector.Selected!, mod, setting); + } } private void DrawApplyAllButton() { - var current = _penumbra.CurrentCollection; - if (ImGuiUtil.DrawDisabledButton($"Try Applying All Associated Mods to {current}##applyAll", - new Vector2(ImGui.GetContentRegionAvail().X, 0), string.Empty, current is "")) + var (id, name) = penumbra.CurrentCollection; + if (config.Ephemeral.IncognitoMode) + name = id.ShortGuid(); + if (ImGuiUtil.DrawDisabledButton($"Try Applying All Associated Mods to {name}##applyAll", + new Vector2(ImGui.GetContentRegionAvail().X, 0), string.Empty, id == Guid.Empty)) ApplyAll(); } public void DrawApplyButton() { - var current = _penumbra.CurrentCollection; + var (id, name) = penumbra.CurrentCollection; if (ImGuiUtil.DrawDisabledButton("Apply Mod Associations", Vector2.Zero, - $"Try to apply all associated mod settings to Penumbras current collection {current}", - _selector.Selected!.AssociatedMods.Count == 0 || current is "")) + $"Try to apply all associated mod settings to Penumbras current collection {name}", + selector.Selected!.AssociatedMods.Count == 0 || id == Guid.Empty)) ApplyAll(); } public void ApplyAll() { - foreach (var (mod, settings) in _selector.Selected!.AssociatedMods) - _penumbra.SetMod(mod, settings); + foreach (var (mod, settings) in selector.Selected!.AssociatedMods) + penumbra.SetMod(mod, settings, StateSource.Manual, false); } private void DrawTable() { - using var table = ImRaii.Table("Mods", 6, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("Mods"u8, config.UseTemporarySettings ? 7 : 6, ImGuiTableFlags.RowBg); if (!table) return; - ImGui.TableSetupColumn("##Delete", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); - ImGui.TableSetupColumn("Mod Name", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Directory Name", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("State").X); - ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Priority").X); - ImGui.TableSetupColumn("##Options", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Try Applyingm").X); + ImUtf8.TableSetupColumn("##Buttons"u8, ImGuiTableColumnFlags.WidthFixed, + ImGui.GetFrameHeight() * 3 + ImGui.GetStyle().ItemInnerSpacing.X * 2); + ImUtf8.TableSetupColumn("Mod Name"u8, ImGuiTableColumnFlags.WidthStretch); + if (config.UseTemporarySettings) + ImUtf8.TableSetupColumn("Remove"u8, ImGuiTableColumnFlags.WidthFixed, ImUtf8.CalcTextSize("Remove"u8).X); + ImUtf8.TableSetupColumn("Inherit"u8, ImGuiTableColumnFlags.WidthFixed, ImUtf8.CalcTextSize("Inherit"u8).X); + ImUtf8.TableSetupColumn("State"u8, ImGuiTableColumnFlags.WidthFixed, ImUtf8.CalcTextSize("State"u8).X); + ImUtf8.TableSetupColumn("Priority"u8, ImGuiTableColumnFlags.WidthFixed, ImUtf8.CalcTextSize("Priority"u8).X); + ImUtf8.TableSetupColumn("##Options"u8, ImGuiTableColumnFlags.WidthFixed, ImUtf8.CalcTextSize("Applym"u8).X); ImGui.TableHeadersRow(); - Mod? removedMod = null; - foreach (var ((mod, settings), idx) in _selector.Selected!.AssociatedMods.WithIndex()) + Mod? removedMod = null; + (Mod mod, ModSettings settings)? updatedMod = null; + foreach (var ((mod, settings), idx) in selector.Selected!.AssociatedMods.WithIndex()) { using var id = ImRaii.PushId(idx); - DrawAssociatedModRow(mod, settings, out var removedModTmp); + DrawAssociatedModRow(mod, settings, out var removedModTmp, out var updatedModTmp); if (removedModTmp.HasValue) removedMod = removedModTmp; + if (updatedModTmp.HasValue) + updatedMod = updatedModTmp; } DrawNewModRow(); if (removedMod.HasValue) - _manager.RemoveMod(_selector.Selected!, removedMod.Value); + manager.RemoveMod(selector.Selected!, removedMod.Value); + + if (updatedMod.HasValue) + manager.UpdateMod(selector.Selected!, updatedMod.Value.mod, updatedMod.Value.settings); } - private void DrawAssociatedModRow(Mod mod, ModSettings settings, out Mod? removedMod) + private void DrawAssociatedModRow(Mod mod, ModSettings settings, out Mod? removedMod, out (Mod, ModSettings)? updatedMod) { removedMod = null; + updatedMod = null; ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), new Vector2(ImGui.GetFrameHeight()), - "Delete this mod from associations", false, true)) - removedMod = mod; - - ImGui.TableNextColumn(); - var selected = ImGui.Selectable($"{mod.Name}##name"); - var hovered = ImGui.IsItemHovered(); - ImGui.TableNextColumn(); - selected |= ImGui.Selectable($"{mod.DirectoryName}##directory"); - hovered |= ImGui.IsItemHovered(); - if (selected) - _penumbra.OpenModPage(mod); - if (hovered) - ImGui.SetTooltip("Click to open mod page in Penumbra."); - ImGui.TableNextColumn(); - using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + var canDelete = config.DeleteDesignModifier.IsActive(); + if (canDelete) { - ImGuiUtil.Center((settings.Enabled ? FontAwesomeIcon.Check : FontAwesomeIcon.Times).ToIconString()); + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, "Delete this mod from associations."u8)) + removedMod = mod; + } + else + { + ImUtf8.IconButton(FontAwesomeIcon.Trash, $"Delete this mod from associations.\nHold {config.DeleteDesignModifier} to delete.", + disabled: true); + } + + ImUtf8.SameLineInner(); + if (ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Copy this mod setting to clipboard."u8)) + _copy = [(mod, settings)]; + + ImUtf8.SameLineInner(); + ImUtf8.IconButton(FontAwesomeIcon.RedoAlt, "Update the settings of this mod association."u8); + if (ImGui.IsItemHovered()) + { + var newSettings = penumbra.GetModSettings(mod, out var source); + if (ImGui.IsItemClicked()) + updatedMod = (mod, newSettings); + + using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 2 * ImGuiHelpers.GlobalScale); + using var tt = ImUtf8.Tooltip(); + if (source.Length > 0) + ImUtf8.Text($"Using temporary settings made by {source}."); + ImGui.Separator(); + var namesDifferent = mod.Name != mod.DirectoryName; + ImGui.Dummy(new Vector2(300 * ImGuiHelpers.GlobalScale, 0)); + using (ImRaii.Group()) + { + if (namesDifferent) + ImUtf8.Text("Directory Name"u8); + ImUtf8.Text("Force Inherit"u8); + ImUtf8.Text("Enabled"u8); + ImUtf8.Text("Priority"u8); + ModCombo.DrawSettingsLeft(newSettings); + } + + ImGui.SameLine(Math.Max(ImGui.GetItemRectSize().X + 3 * ImGui.GetStyle().ItemSpacing.X, 150 * ImGuiHelpers.GlobalScale)); + using (ImRaii.Group()) + { + if (namesDifferent) + ImUtf8.Text(mod.DirectoryName); + + ImUtf8.Text(newSettings.ForceInherit.ToString()); + ImUtf8.Text(newSettings.Enabled.ToString()); + ImUtf8.Text(newSettings.Priority.ToString()); + ModCombo.DrawSettingsRight(newSettings); + } } ImGui.TableNextColumn(); - ImGuiUtil.RightAlign(settings.Priority.ToString()); - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton("Try Applying", new Vector2(ImGui.GetContentRegionAvail().X, 0), string.Empty, - !_penumbra.Available)) + + if (ImUtf8.Selectable($"{mod.Name}##name")) + penumbra.OpenModPage(mod); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Mod Directory: {mod.DirectoryName}\n\nClick to open mod page in Penumbra."); + if (config.UseTemporarySettings) { - var text = _penumbra.SetMod(mod, settings); + ImGui.TableNextColumn(); + var remove = settings.Remove; + if (TwoStateCheckbox.Instance.Draw("##Remove"u8, ref remove)) + updatedMod = (mod, settings with { Remove = remove }); + ImUtf8.HoverTooltip( + "Remove any temporary settings applied by Glamourer instead of applying the configured settings. Only works when using temporary settings, ignored otherwise."u8); + } + + ImGui.TableNextColumn(); + var inherit = settings.ForceInherit; + if (TwoStateCheckbox.Instance.Draw("##ForceInherit"u8, ref inherit)) + updatedMod = (mod, settings with { ForceInherit = inherit }); + ImUtf8.HoverTooltip("Force the mod to inherit its settings from inherited collections."u8); + ImGui.TableNextColumn(); + var enabled = settings.Enabled; + if (TwoStateCheckbox.Instance.Draw("##Enabled"u8, ref enabled)) + updatedMod = (mod, settings with { Enabled = enabled }); + + ImGui.TableNextColumn(); + var priority = settings.Priority; + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImUtf8.InputScalarOnDeactivated("##Priority"u8, ref priority)) + updatedMod = (mod, settings with { Priority = priority }); + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton("Apply", new Vector2(ImGui.GetContentRegionAvail().X, 0), string.Empty, + !penumbra.Available)) + { + var text = penumbra.SetMod(mod, settings, StateSource.Manual, false); if (text.Length > 0) Glamourer.Messager.NotificationMessage(text, NotificationType.Warning, false); } @@ -159,15 +260,15 @@ public class ModAssociationsTab ImGui.TableNextColumn(); var tt = currentName.IsNullOrEmpty() ? "Please select a mod first." - : _selector.Selected!.AssociatedMods.ContainsKey(_modCombo.CurrentSelection.Mod) + : selector.Selected!.AssociatedMods.ContainsKey(_modCombo.CurrentSelection.Mod) ? "The design already contains an association with the selected mod." : string.Empty; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), new Vector2(ImGui.GetFrameHeight()), tt, tt.Length > 0, true)) - _manager.AddMod(_selector.Selected!, _modCombo.CurrentSelection.Mod, _modCombo.CurrentSelection.Settings); + manager.AddMod(selector.Selected!, _modCombo.CurrentSelection.Mod, _modCombo.CurrentSelection.Settings); ImGui.TableNextColumn(); _modCombo.Draw("##new", currentName.IsNullOrEmpty() ? "Select new Mod..." : currentName, string.Empty, - 200 * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeight()); + ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight()); } } diff --git a/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs b/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs index 0b2aa4d..76579c2 100644 --- a/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs +++ b/Glamourer/Gui/Tabs/DesignTab/ModCombo.cs @@ -1,24 +1,21 @@ -using System; -using System.Numerics; -using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility; using Glamourer.Interop.Penumbra; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Classes; using OtterGui.Log; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; namespace Glamourer.Gui.Tabs.DesignTab; -public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)> +public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings, int Count)> { - public ModCombo(PenumbraService penumbra, Logger log) - : base(penumbra.GetMods, log) - { - SearchByParts = false; - } + public ModCombo(PenumbraService penumbra, Logger log, DesignFileSystemSelector selector) + : base(() => penumbra.GetMods(selector.Selected?.FilteredItemNames.ToArray() ?? []), MouseWheelType.None, log) + => SearchByParts = false; - protected override string ToString((Mod Mod, ModSettings Settings) obj) + protected override string ToString((Mod Mod, ModSettings Settings, int Count) obj) => obj.Mod.Name; protected override bool IsVisible(int globalIndex, LowerString filter) @@ -26,36 +23,45 @@ public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)> protected override bool DrawSelectable(int globalIdx, bool selected) { - using var id = ImRaii.PushId(globalIdx); - var (mod, settings) = Items[globalIdx]; + using var id = ImUtf8.PushId(globalIdx); + var (mod, settings, count) = Items[globalIdx]; bool ret; - using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !settings.Enabled)) + var color = settings.Enabled + ? count > 0 + ? ColorId.ContainsItemsEnabled.Value() + : ImGui.GetColorU32(ImGuiCol.Text) + : count > 0 + ? ColorId.ContainsItemsDisabled.Value() + : ImGui.GetColorU32(ImGuiCol.TextDisabled); + using (ImRaii.PushColor(ImGuiCol.Text, color)) { - ret = ImGui.Selectable(mod.Name, selected); + ret = ImUtf8.Selectable(mod.Name, selected); } if (ImGui.IsItemHovered()) { using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 2 * ImGuiHelpers.GlobalScale); - using var tt = ImRaii.Tooltip(); + using var tt = ImUtf8.Tooltip(); var namesDifferent = mod.Name != mod.DirectoryName; ImGui.Dummy(new Vector2(300 * ImGuiHelpers.GlobalScale, 0)); - using (var group = ImRaii.Group()) + using (ImUtf8.Group()) { if (namesDifferent) - ImGui.TextUnformatted("Directory Name"); - ImGui.TextUnformatted("Enabled"); - ImGui.TextUnformatted("Priority"); + ImUtf8.Text("Directory Name"u8); + ImUtf8.Text("Enabled"u8); + ImUtf8.Text("Priority"u8); + ImUtf8.Text("Affected Design Items"u8); DrawSettingsLeft(settings); } ImGui.SameLine(Math.Max(ImGui.GetItemRectSize().X + 3 * ImGui.GetStyle().ItemSpacing.X, 150 * ImGuiHelpers.GlobalScale)); - using (var group = ImRaii.Group()) + using (ImUtf8.Group()) { if (namesDifferent) - ImGui.TextUnformatted(mod.DirectoryName); - ImGui.TextUnformatted(settings.Enabled.ToString()); - ImGui.TextUnformatted(settings.Priority.ToString()); + ImUtf8.Text(mod.DirectoryName); + ImUtf8.Text($"{settings.Enabled}"); + ImUtf8.Text($"{settings.Priority}"); + ImUtf8.Text($"{count}"); DrawSettingsRight(settings); } } @@ -67,7 +73,7 @@ public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)> { foreach (var setting in settings.Settings) { - ImGui.TextUnformatted(setting.Key); + ImUtf8.Text(setting.Key); for (var i = 1; i < setting.Value.Count; ++i) ImGui.NewLine(); } @@ -78,10 +84,10 @@ public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)> foreach (var setting in settings.Settings) { if (setting.Value.Count == 0) - ImGui.TextUnformatted(""); + ImUtf8.Text(""u8); else foreach (var option in setting.Value) - ImGui.TextUnformatted(option); + ImUtf8.Text(option); } } } diff --git a/Glamourer/Gui/Tabs/DesignTab/MultiDesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/MultiDesignPanel.cs new file mode 100644 index 0000000..a68c191 --- /dev/null +++ b/Glamourer/Gui/Tabs/DesignTab/MultiDesignPanel.cs @@ -0,0 +1,514 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Glamourer.Designs; +using Glamourer.Interop.Material; +using Dalamud.Bindings.ImGui; +using OtterGui.Extensions; +using OtterGui.Raii; +using OtterGui.Text; +using static Glamourer.Gui.Tabs.HeaderDrawer; + +namespace Glamourer.Gui.Tabs.DesignTab; + +public class MultiDesignPanel( + DesignFileSystemSelector selector, + DesignManager editor, + DesignColors colors, + Configuration config) +{ + private readonly Button[] _leftButtons = []; + private readonly Button[] _rightButtons = [new IncognitoButton(config)]; + + private readonly DesignColorCombo _colorCombo = new(colors, true); + + public void Draw() + { + if (selector.SelectedPaths.Count == 0) + return; + + HeaderDrawer.Draw(string.Empty, 0, ImGui.GetColorU32(ImGuiCol.FrameBg), _leftButtons, _rightButtons); + using var child = ImUtf8.Child("##MultiPanel"u8, default, true); + if (!child) + return; + + var width = ImGuiHelpers.ScaledVector2(145, 0); + var treeNodePos = ImGui.GetCursorPos(); + _numDesigns = DrawDesignList(); + DrawCounts(treeNodePos); + var offset = DrawMultiTagger(width); + DrawMultiColor(width, offset); + DrawMultiQuickDesignBar(offset); + DrawMultiLock(offset); + DrawMultiResetSettings(offset); + DrawMultiResetDyes(offset); + DrawMultiForceRedraw(offset); + DrawAdvancedButtons(offset); + DrawApplicationButtons(offset); + } + + private void DrawCounts(Vector2 treeNodePos) + { + var startPos = ImGui.GetCursorPos(); + var numFolders = selector.SelectedPaths.Count - _numDesigns; + var text = (_numDesigns, numFolders) switch + { + (0, 0) => string.Empty, // should not happen + (> 0, 0) => $"{_numDesigns} Designs", + (0, > 0) => $"{numFolders} Folders", + _ => $"{_numDesigns} Designs, {numFolders} Folders", + }; + ImGui.SetCursorPos(treeNodePos); + ImUtf8.TextRightAligned(text); + ImGui.SetCursorPos(startPos); + } + + private void ResetCounts() + { + _numQuickDesignEnabled = 0; + _numDesignsLocked = 0; + _numDesignsForcedRedraw = 0; + _numDesignsResetSettings = 0; + _numDesignsResetDyes = 0; + _numDesignsWithAdvancedDyes = 0; + _numAdvancedDyes = 0; + } + + private bool CountLeaves(DesignFileSystem.IPath path) + { + if (path is not DesignFileSystem.Leaf l) + return false; + + if (l.Value.QuickDesign) + ++_numQuickDesignEnabled; + if (l.Value.WriteProtected()) + ++_numDesignsLocked; + if (l.Value.ResetTemporarySettings) + ++_numDesignsResetSettings; + if (l.Value.ForcedRedraw) + ++_numDesignsForcedRedraw; + if (l.Value.ResetAdvancedDyes) + ++_numDesignsResetDyes; + if (l.Value.Materials.Count > 0) + { + ++_numDesignsWithAdvancedDyes; + _numAdvancedDyes += l.Value.Materials.Count; + } + + return true; + } + + private int DrawDesignList() + { + ResetCounts(); + using var tree = ImUtf8.TreeNode("Currently Selected Objects"u8, ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.NoTreePushOnOpen); + ImGui.Separator(); + if (!tree) + return selector.SelectedPaths.Count(CountLeaves); + + var sizeType = new Vector2(ImGui.GetFrameHeight()); + var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType.X - 4 * ImGui.GetStyle().CellPadding.X) / 100; + var sizeMods = availableSizePercent * 35; + var sizeFolders = availableSizePercent * 65; + + var numDesigns = 0; + using (var table = ImUtf8.Table("mods"u8, 3, ImGuiTableFlags.RowBg)) + { + if (!table) + return selector.SelectedPaths.Count(l => l is DesignFileSystem.Leaf); + + ImUtf8.TableSetupColumn("type"u8, ImGuiTableColumnFlags.WidthFixed, sizeType.X); + ImUtf8.TableSetupColumn("mod"u8, ImGuiTableColumnFlags.WidthFixed, sizeMods); + ImUtf8.TableSetupColumn("path"u8, ImGuiTableColumnFlags.WidthFixed, sizeFolders); + + var i = 0; + foreach (var (fullName, path) in selector.SelectedPaths.Select(p => (p.FullName(), p)) + .OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase)) + { + using var id = ImRaii.PushId(i++); + var (icon, text) = path is DesignFileSystem.Leaf l + ? (FontAwesomeIcon.FileCircleMinus, l.Value.Name.Text) + : (FontAwesomeIcon.FolderMinus, string.Empty); + ImGui.TableNextColumn(); + if (ImUtf8.IconButton(icon, "Remove from selection."u8, sizeType)) + selector.RemovePathFromMultiSelection(path); + + ImUtf8.DrawFrameColumn(text); + ImUtf8.DrawFrameColumn(fullName); + + if (CountLeaves(path)) + ++numDesigns; + } + } + + ImGui.Separator(); + return numDesigns; + } + + private string _tag = string.Empty; + private int _numQuickDesignEnabled; + private int _numDesignsLocked; + private int _numDesignsForcedRedraw; + private int _numDesignsResetSettings; + private int _numDesignsResetDyes; + private int _numAdvancedDyes; + private int _numDesignsWithAdvancedDyes; + private int _numDesigns; + private readonly List _addDesigns = []; + private readonly List<(Design, int)> _removeDesigns = []; + + private float DrawMultiTagger(Vector2 width) + { + ImUtf8.TextFrameAligned("Multi Tagger:"u8); + ImGui.SameLine(); + var offset = ImGui.GetItemRectSize().X + ImGui.GetStyle().WindowPadding.X; + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 2 * (width.X + ImGui.GetStyle().ItemSpacing.X)); + ImUtf8.InputText("##tag"u8, ref _tag, "Tag Name..."u8); + + UpdateTagCache(); + var label = _addDesigns.Count > 0 + ? $"Add to {_addDesigns.Count} Designs" + : "Add"; + var tooltip = _addDesigns.Count == 0 + ? _tag.Length == 0 + ? "No tag specified." + : $"All designs selected already contain the tag \"{_tag}\"." + : $"Add the tag \"{_tag}\" to {_addDesigns.Count} designs as a local tag:\n\n\t{string.Join("\n\t", _addDesigns.Select(m => m.Name.Text))}"; + ImGui.SameLine(); + if (ImUtf8.ButtonEx(label, tooltip, width, _addDesigns.Count == 0)) + foreach (var design in _addDesigns) + editor.AddTag(design, _tag); + + label = _removeDesigns.Count > 0 + ? $"Remove from {_removeDesigns.Count} Designs" + : "Remove"; + tooltip = _removeDesigns.Count == 0 + ? _tag.Length == 0 + ? "No tag specified." + : $"No selected design contains the tag \"{_tag}\" locally." + : $"Remove the local tag \"{_tag}\" from {_removeDesigns.Count} designs:\n\n\t{string.Join("\n\t", _removeDesigns.Select(m => m.Item1.Name.Text))}"; + ImGui.SameLine(); + if (ImUtf8.ButtonEx(label, tooltip, width, _removeDesigns.Count == 0)) + foreach (var (design, index) in _removeDesigns) + editor.RemoveTag(design, index); + ImGui.Separator(); + return offset; + } + + private void DrawMultiQuickDesignBar(float offset) + { + ImUtf8.TextFrameAligned("Multi QDB:"u8); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + var buttonWidth = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var diff = _numDesigns - _numQuickDesignEnabled; + var tt = diff == 0 + ? $"All {_numDesigns} selected designs are already displayed in the quick design bar." + : $"Display all {_numDesigns} selected designs in the quick design bar. Changes {diff} designs."; + if (ImUtf8.ButtonEx("Display Selected Designs in QDB"u8, tt, buttonWidth, diff == 0)) + { + foreach (var design in selector.SelectedPaths.OfType()) + editor.SetQuickDesign(design.Value, true); + } + + ImGui.SameLine(); + tt = _numQuickDesignEnabled == 0 + ? $"All {_numDesigns} selected designs are already hidden in the quick design bar." + : $"Hide all {_numDesigns} selected designs in the quick design bar. Changes {_numQuickDesignEnabled} designs."; + if (ImUtf8.ButtonEx("Hide Selected Designs in QDB"u8, tt, buttonWidth, _numQuickDesignEnabled == 0)) + { + foreach (var design in selector.SelectedPaths.OfType()) + editor.SetQuickDesign(design.Value, false); + } + + ImGui.Separator(); + } + + private void DrawMultiLock(float offset) + { + ImUtf8.TextFrameAligned("Multi Lock:"u8); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + var buttonWidth = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var diff = _numDesigns - _numDesignsLocked; + var tt = diff == 0 + ? $"All {_numDesigns} selected designs are already write protected." + : $"Write-protect all {_numDesigns} designs. Changes {diff} designs."; + if (ImUtf8.ButtonEx("Turn Write-Protected"u8, tt, buttonWidth, diff == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.SetWriteProtection(design.Value, true); + + ImGui.SameLine(); + tt = _numDesignsLocked == 0 + ? $"None of the {_numDesigns} selected designs are write-protected." + : $"Remove the write protection of the {_numDesigns} selected designs. Changes {_numDesignsLocked} designs."; + if (ImUtf8.ButtonEx("Remove Write-Protection"u8, tt, buttonWidth, _numDesignsLocked == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.SetWriteProtection(design.Value, false); + ImGui.Separator(); + } + + private void DrawMultiResetSettings(float offset) + { + ImUtf8.TextFrameAligned("Settings:"u8); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + var buttonWidth = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var diff = _numDesigns - _numDesignsResetSettings; + var tt = diff == 0 + ? $"All {_numDesigns} selected designs already reset temporary settings." + : $"Make all {_numDesigns} selected designs reset temporary settings. Changes {diff} designs."; + if (ImUtf8.ButtonEx("Set Reset Temp. Settings"u8, tt, buttonWidth, diff == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.ChangeResetTemporarySettings(design.Value, true); + + ImGui.SameLine(); + tt = _numDesignsResetSettings == 0 + ? $"None of the {_numDesigns} selected designs reset temporary settings." + : $"Stop all {_numDesigns} selected designs from resetting temporary settings. Changes {_numDesignsResetSettings} designs."; + if (ImUtf8.ButtonEx("Remove Reset Temp. Settings"u8, tt, buttonWidth, _numDesignsResetSettings == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.ChangeResetTemporarySettings(design.Value, false); + ImGui.Separator(); + } + + private void DrawMultiResetDyes(float offset) + { + ImUtf8.TextFrameAligned("Adv. Dyes:"u8); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + var buttonWidth = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var diff = _numDesigns - _numDesignsResetDyes; + var tt = diff == 0 + ? $"All {_numDesigns} selected designs already reset advanced dyes." + : $"Make all {_numDesigns} selected designs reset advanced dyes. Changes {diff} designs."; + if (ImUtf8.ButtonEx("Set Reset Dyes"u8, tt, buttonWidth, diff == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.ChangeResetAdvancedDyes(design.Value, true); + + ImGui.SameLine(); + tt = _numDesignsLocked == 0 + ? $"None of the {_numDesigns} selected designs reset advanced dyes." + : $"Stop all {_numDesigns} selected designs from resetting advanced dyes. Changes {_numDesignsResetDyes} designs."; + if (ImUtf8.ButtonEx("Remove Reset Dyes"u8, tt, buttonWidth, _numDesignsResetDyes == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.ChangeResetAdvancedDyes(design.Value, false); + ImGui.Separator(); + } + + private void DrawMultiForceRedraw(float offset) + { + ImUtf8.TextFrameAligned("Redrawing:"u8); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + var buttonWidth = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var diff = _numDesigns - _numDesignsForcedRedraw; + var tt = diff == 0 + ? $"All {_numDesigns} selected designs already force redraws." + : $"Make all {_numDesigns} designs force redraws. Changes {diff} designs."; + if (ImUtf8.ButtonEx("Force Redraws"u8, tt, buttonWidth, diff == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.ChangeForcedRedraw(design.Value, true); + + ImGui.SameLine(); + tt = _numDesignsLocked == 0 + ? $"None of the {_numDesigns} selected designs force redraws." + : $"Stop all {_numDesigns} selected designs from forcing redraws. Changes {_numDesignsForcedRedraw} designs."; + if (ImUtf8.ButtonEx("Remove Forced Redraws"u8, tt, buttonWidth, _numDesignsForcedRedraw == 0)) + foreach (var design in selector.SelectedPaths.OfType()) + editor.ChangeForcedRedraw(design.Value, false); + ImGui.Separator(); + } + + private void DrawMultiColor(Vector2 width, float offset) + { + ImUtf8.TextFrameAligned("Multi Colors:"u8); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + _colorCombo.Draw("##color", _colorCombo.CurrentSelection ?? string.Empty, "Select a design color.", + ImGui.GetContentRegionAvail().X - 2 * (width.X + ImGui.GetStyle().ItemSpacing.X), ImGui.GetTextLineHeight()); + + UpdateColorCache(); + var label = _addDesigns.Count > 0 + ? $"Set for {_addDesigns.Count} Designs" + : "Set"; + var tooltip = _addDesigns.Count == 0 + ? _colorCombo.CurrentSelection switch + { + null => "No color specified.", + DesignColors.AutomaticName => "Use the other button to set to automatic.", + _ => $"All designs selected are already set to the color \"{_colorCombo.CurrentSelection}\".", + } + : $"Set the color of {_addDesigns.Count} designs to \"{_colorCombo.CurrentSelection}\"\n\n\t{string.Join("\n\t", _addDesigns.Select(m => m.Name.Text))}"; + ImGui.SameLine(); + if (ImUtf8.ButtonEx(label, tooltip, width, _addDesigns.Count == 0)) + { + foreach (var design in _addDesigns) + editor.ChangeColor(design, _colorCombo.CurrentSelection!); + } + + label = _removeDesigns.Count > 0 + ? $"Unset {_removeDesigns.Count} Designs" + : "Unset"; + tooltip = _removeDesigns.Count == 0 + ? "No selected design is set to a non-automatic color." + : $"Set {_removeDesigns.Count} designs to use automatic color again:\n\n\t{string.Join("\n\t", _removeDesigns.Select(m => m.Item1.Name.Text))}"; + ImGui.SameLine(); + if (ImUtf8.ButtonEx(label, tooltip, width, _removeDesigns.Count == 0)) + { + foreach (var (design, _) in _removeDesigns) + editor.ChangeColor(design, string.Empty); + } + + ImGui.Separator(); + } + + private void DrawAdvancedButtons(float offset) + { + ImUtf8.TextFrameAligned("Delete Adv."u8); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + var enabled = config.DeleteDesignModifier.IsActive(); + var tt = _numDesignsWithAdvancedDyes is 0 + ? "No selected designs contain any advanced dyes." + : $"Delete {_numAdvancedDyes} advanced dyes from {_numDesignsWithAdvancedDyes} of the selected designs."; + if (ImUtf8.ButtonEx("Delete All Advanced Dyes"u8, tt, new Vector2(ImGui.GetContentRegionAvail().X, 0), + !enabled || _numDesignsWithAdvancedDyes is 0)) + + foreach (var design in selector.SelectedPaths.OfType()) + { + while (design.Value.Materials.Count > 0) + editor.ChangeMaterialValue(design.Value, MaterialValueIndex.FromKey(design.Value.Materials[0].Item1), null); + } + + if (!enabled && _numDesignsWithAdvancedDyes is not 0) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteDesignModifier} while clicking to delete."); + ImGui.Separator(); + } + + private void DrawApplicationButtons(float offset) + { + ImUtf8.TextFrameAligned("Application"u8); + ImGui.SameLine(offset, ImGui.GetStyle().ItemSpacing.X); + var width = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var enabled = config.DeleteDesignModifier.IsActive(); + bool? equip = null; + bool? customize = null; + var group = ImUtf8.Group(); + if (ImUtf8.ButtonEx("Disable Everything"u8, + _numDesigns > 0 + ? $"Disable application of everything, including any existing advanced dyes, advanced customizations, crests and wetness for all {_numDesigns} designs." + : "No designs selected.", width, !enabled)) + { + equip = false; + customize = false; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteDesignModifier} while clicking."); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Enable Everything"u8, + _numDesigns > 0 + ? $"Enable application of everything, including any existing advanced dyes, advanced customizations, crests and wetness for all {_numDesigns} designs." + : "No designs selected.", width, !enabled)) + { + equip = true; + customize = true; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteDesignModifier} while clicking."); + + if (ImUtf8.ButtonEx("Equipment Only"u8, + _numDesigns > 0 + ? $"Enable application of anything related to gear, disable anything that is not related to gear for all {_numDesigns} designs." + : "No designs selected.", width, !enabled)) + { + equip = true; + customize = false; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteDesignModifier} while clicking."); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Customization Only"u8, + _numDesigns > 0 + ? $"Enable application of anything related to customization, disable anything that is not related to customization for all {_numDesigns} designs." + : "No designs selected.", width, !enabled)) + { + equip = false; + customize = true; + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteDesignModifier} while clicking."); + + if (ImUtf8.ButtonEx("Default Application"u8, + _numDesigns > 0 + ? $"Set the application rules to the default values as if the {_numDesigns} were newly created,without any advanced features or wetness." + : "No designs selected.", width, !enabled)) + foreach (var design in selector.SelectedPaths.OfType().Select(l => l.Value)) + { + editor.ChangeApplyMulti(design, true, true, true, false, true, true, false, true); + editor.ChangeApplyMeta(design, MetaIndex.Wetness, false); + } + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteDesignModifier} while clicking."); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Disable Advanced"u8, _numDesigns > 0 + ? $"Disable all advanced dyes and customizations but keep everything else as is for all {_numDesigns} designs." + : "No designs selected.", width, !enabled)) + foreach (var design in selector.SelectedPaths.OfType().Select(l => l.Value)) + editor.ChangeApplyMulti(design, null, null, null, false, null, null, false, null); + + if (!enabled) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteDesignModifier} while clicking."); + + group.Dispose(); + ImGui.Separator(); + if (equip is null && customize is null) + return; + + foreach (var design in selector.SelectedPaths.OfType().Select(l => l.Value)) + { + editor.ChangeApplyMulti(design, equip, customize, equip, customize.HasValue && !customize.Value ? false : null, null, equip, equip, + equip); + if (equip.HasValue) + { + editor.ChangeApplyMeta(design, MetaIndex.HatState, equip.Value); + editor.ChangeApplyMeta(design, MetaIndex.VisorState, equip.Value); + editor.ChangeApplyMeta(design, MetaIndex.WeaponState, equip.Value); + } + + if (customize.HasValue) + editor.ChangeApplyMeta(design, MetaIndex.Wetness, customize.Value); + } + } + + private void UpdateTagCache() + { + _addDesigns.Clear(); + _removeDesigns.Clear(); + if (_tag.Length == 0) + return; + + foreach (var leaf in selector.SelectedPaths.OfType()) + { + var index = leaf.Value.Tags.AsEnumerable().IndexOf(_tag); + if (index >= 0) + _removeDesigns.Add((leaf.Value, index)); + else + _addDesigns.Add(leaf.Value); + } + } + + private void UpdateColorCache() + { + _addDesigns.Clear(); + _removeDesigns.Clear(); + var selection = _colorCombo.CurrentSelection ?? DesignColors.AutomaticName; + foreach (var leaf in selector.SelectedPaths.OfType()) + { + if (leaf.Value.Color.Length > 0) + _removeDesigns.Add((leaf.Value, 0)); + if (selection != DesignColors.AutomaticName && leaf.Value.Color != selection) + _addDesigns.Add(leaf.Value); + } + } +} diff --git a/Glamourer/Gui/Tabs/DesignTab/RenameField.cs b/Glamourer/Gui/Tabs/DesignTab/RenameField.cs new file mode 100644 index 0000000..d79fb2f --- /dev/null +++ b/Glamourer/Gui/Tabs/DesignTab/RenameField.cs @@ -0,0 +1,26 @@ +namespace Glamourer.Gui.Tabs.DesignTab; + +public enum RenameField +{ + None, + RenameSearchPath, + RenameData, + BothSearchPathPrio, + BothDataPrio, +} + +public static class RenameFieldExtensions +{ + public static (string Name, string Desc) GetData(this RenameField value) + => value switch + { + RenameField.None => ("None", "Show no rename fields in the context menu for designs."), + RenameField.RenameSearchPath => ("Search Path", "Show only the search path / move field in the context menu for designs."), + RenameField.RenameData => ("Design Name", "Show only the design name field in the context menu for designs."), + RenameField.BothSearchPathPrio => ("Both (Focus Search Path)", + "Show both rename fields in the context menu for designs, but put the keyboard cursor on the search path field."), + RenameField.BothDataPrio => ("Both (Focus Design Name)", + "Show both rename fields in the context menu for designs, but put the keyboard cursor on the design name field"), + _ => (string.Empty, string.Empty), + }; +} diff --git a/Glamourer/Gui/Tabs/HeaderDrawer.cs b/Glamourer/Gui/Tabs/HeaderDrawer.cs index 6f62e08..cb169ba 100644 --- a/Glamourer/Gui/Tabs/HeaderDrawer.cs +++ b/Glamourer/Gui/Tabs/HeaderDrawer.cs @@ -1,9 +1,6 @@ -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; +using Dalamud.Interface; using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; @@ -11,76 +8,91 @@ namespace Glamourer.Gui.Tabs; public static class HeaderDrawer { - public struct Button + public abstract class Button { - public static readonly Button Invisible = new() - { - Visible = false, - Width = 0, - }; + protected abstract void OnClick(); - public Action? OnClick; - public string Description = string.Empty; - public float Width; - public uint BorderColor; - public uint TextColor; - public FontAwesomeIcon Icon; - public bool Disabled; - public bool Visible; + protected virtual string Description + => string.Empty; - public Button() - { - Visible = true; - Width = ImGui.GetFrameHeightWithSpacing(); - BorderColor = ColorId.HeaderButtons.Value(); - TextColor = ColorId.HeaderButtons.Value(); - Disabled = false; - } + protected virtual uint BorderColor + => ColorId.HeaderButtons.Value(); - public readonly void Draw() + protected virtual uint TextColor + => ColorId.HeaderButtons.Value(); + + protected virtual FontAwesomeIcon Icon + => FontAwesomeIcon.None; + + protected virtual bool Disabled + => false; + + public virtual bool Visible + => true; + + public void Draw(float width) { if (!Visible) return; using var color = ImRaii.PushColor(ImGuiCol.Border, BorderColor) .Push(ImGuiCol.Text, TextColor, TextColor != 0); - if (ImGuiUtil.DrawDisabledButton(Icon.ToIconString(), new Vector2(Width, ImGui.GetFrameHeight()), string.Empty, Disabled, true)) - OnClick?.Invoke(); + if (ImGuiUtil.DrawDisabledButton(Icon.ToIconString(), new Vector2(width, ImGui.GetFrameHeight()), string.Empty, Disabled, true)) + OnClick(); color.Pop(); ImGuiUtil.HoverTooltip(Description); } - - public static Button IncognitoButton(bool current, Action setter) - => current - ? new Button - { - Description = "Toggle incognito mode off.", - Icon = FontAwesomeIcon.EyeSlash, - OnClick = () => setter(false), - } - : new Button - { - Description = "Toggle incognito mode on.", - Icon = FontAwesomeIcon.Eye, - OnClick = () => setter(true), - }; } - public static void Draw(string text, uint textColor, uint frameColor, int leftButtons, params Button[] buttons) + public sealed class IncognitoButton(Configuration config) : Button { + protected override string Description + { + get + { + var hold = config.IncognitoModifier.IsActive(); + return (config.Ephemeral.IncognitoMode, hold) + switch + { + (true, true) => "Toggle incognito mode off.", + (false, true) => "Toggle incognito mode on.", + (true, false) => $"Toggle incognito mode off.\n\nHold {config.IncognitoModifier} while clicking to toggle.", + (false, false) => $"Toggle incognito mode on.\n\nHold {config.IncognitoModifier} while clicking to toggle.", + }; + } + } + + protected override FontAwesomeIcon Icon + => config.Ephemeral.IncognitoMode + ? FontAwesomeIcon.EyeSlash + : FontAwesomeIcon.Eye; + + protected override void OnClick() + { + if (!config.IncognitoModifier.IsActive()) + return; + + config.Ephemeral.IncognitoMode = !config.Ephemeral.IncognitoMode; + config.Ephemeral.Save(); + } + } + + public static void Draw(string text, uint textColor, uint frameColor, Button[] leftButtons, Button[] rightButtons) + { + var width = ImGui.GetFrameHeightWithSpacing(); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) .Push(ImGuiStyleVar.FrameRounding, 0) .Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); var leftButtonSize = 0f; - foreach (var button in buttons.Take(leftButtons).Where(b => b.Visible)) + foreach (var button in leftButtons.Where(b => b.Visible)) { - button.Draw(); + button.Draw(width); ImGui.SameLine(); - leftButtonSize += button.Width; + leftButtonSize += width; } - var rightButtonSize = buttons.Length > leftButtons ? buttons.Skip(leftButtons).Where(b => b.Visible).Select(b => b.Width).Sum() : 0f; + var rightButtonSize = rightButtons.Count(b => b.Visible) * width; var midSize = ImGui.GetContentRegionAvail().X - rightButtonSize - ImGuiHelpers.GlobalScale; style.Pop(); @@ -92,10 +104,10 @@ public static class HeaderDrawer style.Pop(); style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); - foreach (var button in buttons.Skip(leftButtons).Where(b => b.Visible)) + foreach (var button in rightButtons.Where(b => b.Visible)) { ImGui.SameLine(); - button.Draw(); + button.Draw(width); } } } diff --git a/Glamourer/Gui/Tabs/MessageTab.cs b/Glamourer/Gui/Tabs/MessageTab.cs index 69a2a2c..b3ce428 100644 --- a/Glamourer/Gui/Tabs/MessageTab.cs +++ b/Glamourer/Gui/Tabs/MessageTab.cs @@ -1,5 +1,4 @@ -using System; -using OtterGui.Classes; +using OtterGui.Classes; using OtterGui.Widgets; namespace Glamourer.Gui.Tabs; diff --git a/Glamourer/Gui/Tabs/NpcCombo.cs b/Glamourer/Gui/Tabs/NpcCombo.cs new file mode 100644 index 0000000..86eb766 --- /dev/null +++ b/Glamourer/Gui/Tabs/NpcCombo.cs @@ -0,0 +1,11 @@ +using Glamourer.GameData; +using OtterGui.Widgets; + +namespace Glamourer.Gui.Tabs; + +public class NpcCombo(NpcCustomizeSet npcCustomizeSet) + : FilterComboCache(npcCustomizeSet, MouseWheelType.None, Glamourer.Log) +{ + protected override string ToString(NpcData obj) + => obj.Name; +} diff --git a/Glamourer/Gui/Tabs/NpcTab/LocalNpcAppearanceData.cs b/Glamourer/Gui/Tabs/NpcTab/LocalNpcAppearanceData.cs new file mode 100644 index 0000000..1b70e27 --- /dev/null +++ b/Glamourer/Gui/Tabs/NpcTab/LocalNpcAppearanceData.cs @@ -0,0 +1,138 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Glamourer.Designs; +using Glamourer.GameData; +using Glamourer.Services; +using Dalamud.Bindings.ImGui; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Glamourer.Gui.Tabs.NpcTab; + +public class LocalNpcAppearanceData : ISavable +{ + private readonly DesignColors _colors; + + public record struct Data(string Color = "", bool Favorite = false); + + private readonly Dictionary _data = []; + + public LocalNpcAppearanceData(DesignColors colors, SaveService saveService) + { + _colors = colors; + Load(saveService); + DataChanged += () => saveService.QueueSave(this); + } + + public bool IsFavorite(in NpcData data) + => _data.TryGetValue(ToKey(data), out var tuple) && tuple.Favorite; + + public (uint Color, bool Favorite) GetData(in NpcData data) + => _data.TryGetValue(ToKey(data), out var t) + ? (GetColor(t.Color, t.Favorite, data.Kind), t.Favorite) + : (GetColor(string.Empty, false, data.Kind), false); + + public string GetColor(in NpcData data) + => _data.TryGetValue(ToKey(data), out var t) ? t.Color : string.Empty; + + private uint GetColor(string color, bool favorite, ObjectKind kind) + { + if (color.Length == 0) + { + if (favorite) + return ColorId.FavoriteStarOn.Value(); + + return kind is ObjectKind.BattleNpc + ? ColorId.BattleNpc.Value() + : ColorId.EventNpc.Value(); + } + + if (_colors.TryGetValue(color, out var value)) + return value == 0 ? ImGui.GetColorU32(ImGuiCol.Text) : value; + + return _colors.MissingColor; + } + + public void ToggleFavorite(in NpcData data) + { + var key = ToKey(data); + if (_data.TryGetValue(key, out var t)) + { + if (t is { Color: "", Favorite: true }) + _data.Remove(key); + else + _data[key] = t with { Favorite = !t.Favorite }; + } + else + { + _data[key] = new Data(string.Empty, true); + } + + DataChanged.Invoke(); + } + + public void SetColor(in NpcData data, string color) + { + var key = ToKey(data); + if (_data.TryGetValue(key, out var t)) + { + if (!t.Favorite && color.Length == 0) + _data.Remove(key); + else + _data[key] = t with { Color = color }; + } + else if (color.Length != 0) + { + _data[key] = new Data(color); + } + + DataChanged.Invoke(); + } + + private static ulong ToKey(in NpcData data) + => (byte)data.Kind | ((ulong)data.Id.Id << 8); + + public event Action DataChanged = null!; + + public string ToFilename(FilenameService fileNames) + => fileNames.NpcAppearanceFile; + + public void Save(StreamWriter writer) + { + var jObj = new JObject() + { + ["Version"] = 1, + ["Data"] = JToken.FromObject(_data), + }; + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + jObj.WriteTo(j); + } + + private void Load(SaveService save) + { + var file = save.FileNames.NpcAppearanceFile; + if (!File.Exists(file)) + return; + + try + { + var text = File.ReadAllText(file); + var jObj = JObject.Parse(text); + var version = jObj["Version"]?.ToObject() ?? 0; + switch (version) + { + case 1: + var data = jObj["Data"]?.ToObject>() ?? []; + _data.EnsureCapacity(data.Count); + foreach (var kvp in data) + _data.Add(kvp.Key, kvp.Value); + return; + default: throw new Exception("Invalid version {version}."); + } + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not read local NPC appearance data:\n{ex}"); + } + } +} diff --git a/Glamourer/Gui/Tabs/NpcTab/NpcFilter.cs b/Glamourer/Gui/Tabs/NpcTab/NpcFilter.cs new file mode 100644 index 0000000..1b698f9 --- /dev/null +++ b/Glamourer/Gui/Tabs/NpcTab/NpcFilter.cs @@ -0,0 +1,44 @@ +using Glamourer.Designs; +using Glamourer.GameData; +using OtterGui.Classes; + +namespace Glamourer.Gui.Tabs.NpcTab; + +public sealed class NpcFilter(LocalNpcAppearanceData _favorites) : FilterUtility +{ + protected override string Tooltip + => "Filter NPC appearances for those where their names contain the given substring.\n" + + "Enter i:[number] to filter for NPCs of certain IDs.\n" + + "Enter c:[string] to filter for NPC appearances set to specific colors."; + + protected override (LowerString, long, int) FilterChange(string input) + => input.Length switch + { + 0 => (LowerString.Empty, 0, -1), + > 1 when input[1] == ':' => + input[0] switch + { + 'i' or 'I' => input.Length == 2 ? (LowerString.Empty, 0, -1) : + long.TryParse(input.AsSpan(2), out var r) ? (LowerString.Empty, r, 1) : (LowerString.Empty, 0, -1), + 'c' or 'C' => input.Length == 2 ? (LowerString.Empty, 0, -1) : (new LowerString(input[2..]), 0, 2), + _ => (new LowerString(input), 0, 0), + }, + _ => (new LowerString(input), 0, 0), + }; + + public override bool ApplyFilter(in NpcData value) + => FilterMode switch + { + -1 => false, + 0 => Filter.IsContained(value.Name), + 1 => value.Id.Id == NumericalFilter, + 2 => Filter.IsContained(GetColor(value)), + _ => false, // Should never happen + }; + + private string GetColor(in NpcData value) + { + var color = _favorites.GetColor(value); + return color.Length == 0 ? DesignColors.AutomaticName : color; + } +} diff --git a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs new file mode 100644 index 0000000..29fe7ef --- /dev/null +++ b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs @@ -0,0 +1,350 @@ +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Glamourer.Designs; +using Glamourer.Gui.Customization; +using Glamourer.Gui.Equipment; +using Glamourer.Gui.Tabs.DesignTab; +using Glamourer.State; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using static Glamourer.Gui.Tabs.HeaderDrawer; + +namespace Glamourer.Gui.Tabs.NpcTab; + +public class NpcPanel +{ + private readonly Configuration _config; + private readonly DesignColorCombo _colorCombo; + private string _newName = string.Empty; + private DesignBase? _newDesign; + private readonly NpcSelector _selector; + private readonly LocalNpcAppearanceData _favorites; + private readonly CustomizationDrawer _customizeDrawer; + private readonly EquipmentDrawer _equipDrawer; + private readonly DesignConverter _converter; + private readonly DesignManager _designManager; + private readonly StateManager _state; + private readonly ActorObjectManager _objects; + private readonly DesignColors _colors; + private readonly Button[] _leftButtons; + private readonly Button[] _rightButtons; + + public NpcPanel(NpcSelector selector, + LocalNpcAppearanceData favorites, + CustomizationDrawer customizeDrawer, + EquipmentDrawer equipDrawer, + DesignConverter converter, + DesignManager designManager, + StateManager state, + ActorObjectManager objects, + DesignColors colors, + Configuration config) + { + _selector = selector; + _favorites = favorites; + _customizeDrawer = customizeDrawer; + _equipDrawer = equipDrawer; + _converter = converter; + _designManager = designManager; + _state = state; + _objects = objects; + _colors = colors; + _config = config; + _colorCombo = new DesignColorCombo(colors, true); + _leftButtons = + [ + new ExportToClipboardButton(this), + new SaveAsDesignButton(this), + ]; + _rightButtons = + [ + new FavoriteButton(this), + ]; + } + + public void Draw() + { + using var group = ImRaii.Group(); + + DrawHeader(); + DrawPanel(); + } + + private void DrawHeader() + { + HeaderDrawer.Draw(_selector.HasSelection ? _selector.Selection.Name : "No Selection", ColorId.NormalDesign.Value(), + ImGui.GetColorU32(ImGuiCol.FrameBg), _leftButtons, _rightButtons); + SaveDesignDrawPopup(); + } + + private sealed class FavoriteButton(NpcPanel panel) : Button + { + protected override string Description + => panel._favorites.IsFavorite(panel._selector.Selection) + ? "Remove this NPC appearance from your favorites." + : "Add this NPC Appearance to your favorites."; + + protected override uint TextColor + => panel._favorites.IsFavorite(panel._selector.Selection) + ? ColorId.FavoriteStarOn.Value() + : 0x80000000; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.Star; + + public override bool Visible + => panel._selector.HasSelection; + + protected override void OnClick() + => panel._favorites.ToggleFavorite(panel._selector.Selection); + } + + private void SaveDesignDrawPopup() + { + if (!ImGuiUtil.OpenNameField("Save as Design", ref _newName)) + return; + + if (_newDesign != null && _newName.Length > 0) + _designManager.CreateClone(_newDesign, _newName, true); + _newDesign = null; + _newName = string.Empty; + } + + private void DrawPanel() + { + using var table = ImUtf8.Table("##Panel", 1, ImGuiTableFlags.BordersOuter | ImGuiTableFlags.ScrollY, ImGui.GetContentRegionAvail()); + if (!table || !_selector.HasSelection) + return; + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableNextColumn(); + ImGui.Dummy(Vector2.Zero); + DrawButtonRow(); + + ImGui.TableNextColumn(); + DrawCustomization(); + DrawEquipment(); + DrawAppearanceInfo(); + } + + private void DrawButtonRow() + { + DrawApplyToSelf(); + ImGui.SameLine(); + DrawApplyToTarget(); + } + + private void DrawCustomization() + { + if (_config.HideDesignPanel.HasFlag(DesignPanelFlag.Customization)) + return; + + var header = _selector.Selection.ModelId == 0 + ? "Customization" + : $"Customization (Model Id #{_selector.Selection.ModelId})###Customization"; + var expand = _config.AutoExpandDesignPanel.HasFlag(DesignPanelFlag.Customization); + using var h = ImUtf8.CollapsingHeaderId(header, expand ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None); + if (!h) + return; + + _customizeDrawer.Draw(_selector.Selection.Customize, true, true); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + } + + private void DrawEquipment() + { + using var h = DesignPanelFlag.Equipment.Header(_config); + if (!h) + return; + + _equipDrawer.Prepare(); + var designData = ToDesignData(); + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var data = new EquipDrawData(slot, designData) { Locked = true }; + _equipDrawer.DrawEquip(data); + } + + var mainhandData = new EquipDrawData(EquipSlot.MainHand, designData) { Locked = true }; + var offhandData = new EquipDrawData(EquipSlot.OffHand, designData) { Locked = true }; + _equipDrawer.DrawWeapons(mainhandData, offhandData, false); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromValue(MetaIndex.VisorState, _selector.Selection.VisorToggled)); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + } + + private DesignData ToDesignData() + { + var selection = _selector.Selection; + var items = _converter.FromDrawData(selection.Equip.ToArray(), selection.Mainhand, selection.Offhand, true).ToArray(); + var designData = new DesignData { Customize = selection.Customize }; + foreach (var (slot, item, stain) in items) + { + designData.SetItem(slot, item); + designData.SetStain(slot, stain); + } + + return designData; + } + + private void DrawApplyToSelf() + { + var (id, data) = _objects.PlayerData; + if (!ImUtf8.ButtonEx("Apply to Yourself"u8, + "Apply the current NPC appearance to your character.\nHold Control to only apply gear.\nHold Shift to only apply customizations."u8, + Vector2.Zero, !data.Valid)) + return; + + if (_state.GetOrCreate(id, data.Objects[0], out var state)) + { + var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); + _state.ApplyDesign(state, design, ApplySettings.Manual with { IsFinal = true }); + } + } + + private void DrawApplyToTarget() + { + var (id, data) = _objects.TargetData; + var tt = id.IsValid + ? data.Valid + ? "Apply the current NPC appearance to your current target.\nHold Control to only apply gear.\nHold Shift to only apply customizations."u8 + : "The current target can not be manipulated."u8 + : "No valid target selected."u8; + if (!ImUtf8.ButtonEx("Apply to Target"u8, tt, Vector2.Zero, !data.Valid)) + return; + + if (_state.GetOrCreate(id, data.Objects[0], out var state)) + { + var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); + _state.ApplyDesign(state, design, ApplySettings.Manual with { IsFinal = true }); + } + } + + + private void DrawAppearanceInfo() + { + using var h = DesignPanelFlag.AppearanceDetails.Header(_config); + if (!h) + return; + + using var table = ImUtf8.Table("Details"u8, 2); + if (!table) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); + ImUtf8.TableSetupColumn("Type"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Last Update Datem").X); + ImUtf8.TableSetupColumn("Data"u8, ImGuiTableColumnFlags.WidthStretch); + + var selection = _selector.Selection; + CopyButton("NPC Name"u8, selection.Name); + CopyButton("NPC ID"u8, selection.Id.Id.ToString()); + ImGuiUtil.DrawFrameColumn("NPC Type"); + ImGui.TableNextColumn(); + var width = ImGui.GetContentRegionAvail().X; + ImGuiUtil.DrawTextButton(selection.Kind is ObjectKind.BattleNpc ? "Battle NPC" : "Event NPC", new Vector2(width, 0), + ImGui.GetColorU32(ImGuiCol.FrameBg)); + + ImUtf8.DrawFrameColumn("Color"u8); + var color = _favorites.GetColor(selection); + var colorName = color.Length == 0 ? DesignColors.AutomaticName : color; + ImGui.TableNextColumn(); + if (_colorCombo.Draw("##colorCombo", colorName, + "Associate a color with this NPC appearance.\n" + + "Right-Click to revert to automatic coloring.\n" + + "Hold Control and scroll the mousewheel to scroll.", + width - ImGui.GetStyle().ItemSpacing.X - ImGui.GetFrameHeight(), ImGui.GetTextLineHeight()) + && _colorCombo.CurrentSelection != null) + { + color = _colorCombo.CurrentSelection is DesignColors.AutomaticName ? string.Empty : _colorCombo.CurrentSelection; + _favorites.SetColor(selection, color); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _favorites.SetColor(selection, string.Empty); + color = string.Empty; + } + + if (_colors.TryGetValue(color, out var currentColor)) + { + ImGui.SameLine(); + if (DesignColorUi.DrawColorButton($"Color associated with {color}", currentColor, out var newColor)) + _colors.SetColor(color, newColor); + } + else if (color.Length != 0) + { + ImGui.SameLine(); + var size = new Vector2(ImGui.GetFrameHeight()); + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGuiUtil.DrawTextButton(FontAwesomeIcon.ExclamationCircle.ToIconString(), size, 0, _colors.MissingColor); + ImUtf8.HoverTooltip("The color associated with this design does not exist."u8); + } + + return; + + static void CopyButton(ReadOnlySpan label, string text) + { + ImUtf8.DrawFrameColumn(label); + ImGui.TableNextColumn(); + if (ImUtf8.Button(text, new Vector2(ImGui.GetContentRegionAvail().X, 0))) + ImUtf8.SetClipboardText(text); + ImUtf8.HoverTooltip("Click to copy to clipboard."u8); + } + } + + private sealed class ExportToClipboardButton(NpcPanel panel) : Button + { + protected override string Description + => "Copy the current NPCs appearance to your clipboard.\nHold Control to disable applying of customizations for the copied design.\nHold Shift to disable applying of gear for the copied design."; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.Copy; + + public override bool Visible + => panel._selector.HasSelection; + + protected override void OnClick() + { + try + { + var data = panel.ToDesignData(); + var text = panel._converter.ShareBase64(data, new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); + ImGui.SetClipboardText(text); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Could not copy {panel._selector.Selection.Name}'s data to clipboard.", + $"Could not copy data from NPC appearance {panel._selector.Selection.Kind} {panel._selector.Selection.Id.Id} to clipboard", + NotificationType.Error); + } + } + } + + private sealed class SaveAsDesignButton(NpcPanel panel) : Button + { + protected override string Description + => "Save this NPCs appearance as a design.\nHold Control to disable applying of customizations for the saved design.\nHold Shift to disable applying of gear for the saved design."; + + protected override FontAwesomeIcon Icon + => FontAwesomeIcon.Save; + + public override bool Visible + => panel._selector.HasSelection; + + protected override void OnClick() + { + ImGui.OpenPopup("Save as Design"); + panel._newName = panel._selector.Selection.Name; + var data = panel.ToDesignData(); + panel._newDesign = panel._converter.Convert(data, new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); + } + } +} diff --git a/Glamourer/Gui/Tabs/NpcTab/NpcSelector.cs b/Glamourer/Gui/Tabs/NpcTab/NpcSelector.cs new file mode 100644 index 0000000..8497ab4 --- /dev/null +++ b/Glamourer/Gui/Tabs/NpcTab/NpcSelector.cs @@ -0,0 +1,92 @@ +using Glamourer.GameData; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Extensions; +using OtterGui.Raii; +using ImGuiClip = OtterGui.ImGuiClip; + +namespace Glamourer.Gui.Tabs.NpcTab; + +public class NpcSelector : IDisposable +{ + private readonly NpcCustomizeSet _npcs; + private readonly LocalNpcAppearanceData _favorites; + + private NpcFilter _filter; + private readonly List _visibleOrdered = []; + private int _selectedGlobalIndex; + private bool _listDirty = true; + private Vector2 _defaultItemSpacing; + private float _width; + + + public NpcSelector(NpcCustomizeSet npcs, LocalNpcAppearanceData favorites) + { + _npcs = npcs; + _favorites = favorites; + _filter = new NpcFilter(_favorites); + _favorites.DataChanged += OnFavoriteChange; + } + + public void Dispose() + { + _favorites.DataChanged -= OnFavoriteChange; + } + + private void OnFavoriteChange() + => _listDirty = true; + + public void UpdateList() + { + if (!_listDirty) + return; + + _listDirty = false; + _visibleOrdered.Clear(); + var enumerable = _npcs.WithIndex(); + if (!_filter.IsEmpty) + enumerable = enumerable.Where(d => _filter.ApplyFilter(d.Value)); + var range = enumerable.OrderByDescending(d => _favorites.IsFavorite(d.Value)) + .ThenBy(d => d.Index) + .Select(d => d.Index); + _visibleOrdered.AddRange(range); + } + + public bool HasSelection + => _selectedGlobalIndex >= 0 && _selectedGlobalIndex < _npcs.Count; + + public NpcData Selection + => HasSelection ? _npcs[_selectedGlobalIndex] : default; + + public void Draw(float width) + { + _width = width; + using var group = ImRaii.Group(); + _defaultItemSpacing = ImGui.GetStyle().ItemSpacing; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameRounding, 0); + + if (_filter.Draw(width)) + _listDirty = true; + UpdateList(); + DrawSelector(); + } + + private void DrawSelector() + { + using var child = ImRaii.Child("##Selector", new Vector2(_width, ImGui.GetContentRegionAvail().Y), true); + if (!child) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _defaultItemSpacing); + ImGuiClip.ClippedDraw(_visibleOrdered, DrawSelectable, ImGui.GetTextLineHeight()); + } + + private void DrawSelectable(int globalIndex) + { + using var id = ImRaii.PushId(globalIndex); + using var color = ImRaii.PushColor(ImGuiCol.Text, _favorites.GetData(_npcs[globalIndex]).Color); + if (ImGui.Selectable(_npcs[globalIndex].Name, _selectedGlobalIndex == globalIndex, ImGuiSelectableFlags.AllowItemOverlap)) + _selectedGlobalIndex = globalIndex; + } +} diff --git a/Glamourer/Gui/Tabs/NpcTab/NpcTab.cs b/Glamourer/Gui/Tabs/NpcTab/NpcTab.cs new file mode 100644 index 0000000..318e017 --- /dev/null +++ b/Glamourer/Gui/Tabs/NpcTab/NpcTab.cs @@ -0,0 +1,18 @@ +using Dalamud.Interface.Utility; +using Dalamud.Bindings.ImGui; +using OtterGui.Widgets; + +namespace Glamourer.Gui.Tabs.NpcTab; + +public class NpcTab(NpcSelector _selector, NpcPanel _panel) : ITab +{ + public ReadOnlySpan Label + => "NPCs"u8; + + public void DrawContent() + { + _selector.Draw(200 * ImGuiHelpers.GlobalScale); + ImGui.SameLine(); + _panel.Draw(); + } +} diff --git a/Glamourer/Gui/Tabs/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab.cs deleted file mode 100644 index ff55b14..0000000 --- a/Glamourer/Gui/Tabs/SettingsTab.cs +++ /dev/null @@ -1,265 +0,0 @@ -using System; -using System.Numerics; -using System.Runtime.CompilerServices; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using Dalamud.Interface.Utility; -using Glamourer.Gui.Tabs.DesignTab; -using Glamourer.Interop; -using Glamourer.Interop.Penumbra; -using Glamourer.Services; -using Glamourer.State; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Widgets; - -namespace Glamourer.Gui.Tabs; - -public class SettingsTab : ITab -{ - private readonly Configuration _config; - private readonly DesignFileSystemSelector _selector; - private readonly StateListener _stateListener; - private readonly CodeService _codeService; - private readonly PenumbraAutoRedraw _autoRedraw; - private readonly ContextMenuService _contextMenuService; - private readonly UiBuilder _uiBuilder; - private readonly GlamourerChangelog _changelog; - private readonly FunModule _funModule; - - public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener, - CodeService codeService, PenumbraAutoRedraw autoRedraw, ContextMenuService contextMenuService, UiBuilder uiBuilder, - GlamourerChangelog changelog, FunModule funModule) - { - _config = config; - _selector = selector; - _stateListener = stateListener; - _codeService = codeService; - _autoRedraw = autoRedraw; - _contextMenuService = contextMenuService; - _uiBuilder = uiBuilder; - _changelog = changelog; - _funModule = funModule; - } - - public ReadOnlySpan Label - => "Settings"u8; - - private string _currentCode = string.Empty; - - public void DrawContent() - { - using var child = ImRaii.Child("MainWindowChild"); - if (!child) - return; - - Checkbox("Enabled", "Enable main functionality of keeping and applying state.", _stateListener.Enabled, _stateListener.Enable); - Checkbox("Enable Auto Designs", "Enable the application of designs associated to characters to be applied automatically.", - _config.EnableAutoDesigns, v => _config.EnableAutoDesigns = v); - ImGui.NewLine(); - ImGui.NewLine(); - - using (var child2 = ImRaii.Child("SettingsChild")) - { - DrawBehaviorSettings(); - DrawInterfaceSettings(); - DrawColorSettings(); - DrawCodes(); - } - - MainWindow.DrawSupportButtons(_changelog.Changelog); - } - - private void DrawBehaviorSettings() - { - if (!ImGui.CollapsingHeader("Glamourer Behavior")) - return; - - Checkbox("Use Replacement Gear for Gear Unavailable to Your Race or Gender", - "Use different gender- and race-appropriate models as a substitute when detecting certain items not available for a characters current gender and race.", - _config.UseRestrictedGearProtection, v => _config.UseRestrictedGearProtection = v); - Checkbox("Do Not Apply Unobtained Items in Automation", - "Enable this if you want automatically applied designs to only consider items and customizations you have actually unlocked once, and skip those you have not.", - _config.UnlockedItemMode, v => _config.UnlockedItemMode = v); - Checkbox("Enable Festival Easter-Eggs", - "Glamourer may do some fun things on specific dates. Disable this if you do not want your experience disrupted by this.", - _config.DisableFestivals == 0, v => _config.DisableFestivals = v ? (byte)0 : (byte)2); - Checkbox("Auto-Reload Gear", - "Automatically reload equipment pieces on your own character when changing any mod options in Penumbra in their associated collection.", - _config.AutoRedrawEquipOnChanges, _autoRedraw.SetState); - Checkbox("Revert Manual Changes on Zone Change", - "Restores the old behaviour of reverting your character to its game or automation base whenever you change the zone.", - _config.RevertManualChangesOnZoneChange, v => _config.RevertManualChangesOnZoneChange = v); - ImGui.NewLine(); - } - - private void DrawInterfaceSettings() - { - if (!ImGui.CollapsingHeader("Interface")) - return; - - Checkbox("Smaller Equip Display", "Use single-line display without icons and small dye buttons instead of double-line display.", - _config.SmallEquip, v => _config.SmallEquip = v); - Checkbox("Show Application Checkboxes", - "Show the application checkboxes in the Customization and Equipment panels of the design tab, instead of only showing them under Application Rules.", - !_config.HideApplyCheckmarks, v => _config.HideApplyCheckmarks = !v); - Checkbox("Enable Game Context Menus", "Whether to show a Try On via Glamourer button on context menus for equippable items.", - _config.EnableGameContextMenu, v => - { - _config.EnableGameContextMenu = v; - if (v) - _contextMenuService.Enable(); - else - _contextMenuService.Disable(); - }); - Checkbox("Hide Window in Cutscenes", "Whether the main Glamourer window should automatically be hidden when entering cutscenes or not.", - _config.HideWindowInCutscene, - v => - { - _config.HideWindowInCutscene = v; - _uiBuilder.DisableCutsceneUiHide = !v; - }); - if (Widget.DoubleModifierSelector("Design Deletion Modifier", - "A modifier you need to hold while clicking the Delete Design button for it to take effect.", 100 * ImGuiHelpers.GlobalScale, - _config.DeleteDesignModifier, v => _config.DeleteDesignModifier = v)) - _config.Save(); - DrawFolderSortType(); - Checkbox("Auto-Open Design Folders", - "Have design folders open or closed as their default state after launching.", _config.OpenFoldersByDefault, - v => _config.OpenFoldersByDefault = v); - Checkbox("Show all Application Rule Checkboxes for Automation", - "Show multiple separate application rule checkboxes for automated designs, instead of a single box for enabling or disabling.", - _config.ShowAllAutomatedApplicationRules, v => _config.ShowAllAutomatedApplicationRules = v); - Checkbox("Show Unobtained Item Warnings", - "Show information whether you have unlocked all items and customizations in your automated design or not.", - _config.ShowUnlockedItemWarnings, v => _config.ShowUnlockedItemWarnings = v); - Checkbox("Debug Mode", "Show the debug tab. Only useful for debugging or advanced use. Not recommended in general.", _config.DebugMode, - v => _config.DebugMode = v); - ImGui.NewLine(); - } - - /// Draw the entire Color subsection. - private void DrawColorSettings() - { - if (!ImGui.CollapsingHeader("Colors")) - return; - - foreach (var color in Enum.GetValues()) - { - var (defaultColor, name, description) = color.Data(); - var currentColor = _config.Colors.TryGetValue(color, out var current) ? current : defaultColor; - if (Widget.ColorPicker(name, description, currentColor, c => _config.Colors[color] = c, defaultColor)) - _config.Save(); - } - - ImGui.NewLine(); - } - - private void DrawCodes() - { - const string tooltip = - "Cheat Codes are not actually for cheating in the game, but for 'cheating' in Glamourer. They allow for some fun easter-egg modes that usually manipulate the appearance of all players you see (including yourself) in some way.\n\n" - + "Cheat Codes are generally pop culture references, but it is unlikely you will be able to guess any of them based on nothing. Some codes have been published on the discord server, but other than that, we are still undecided on how and when to publish them or add any new ones. Maybe some will be hidden in the change logs or on the help pages. Or maybe I will just add hints in this section later on.\n\n" - + "In any case, you are not losing out on anything important if you never look at this section and there is no real reason to go on a treasure hunt for them. It is mostly something I added because it was fun for me."; - - var show = ImGui.CollapsingHeader("Cheat Codes"); - if (ImGui.IsItemHovered()) - { - ImGui.SetNextWindowSize(new Vector2(400, 0)); - using var tt = ImRaii.Tooltip(); - ImGuiUtil.TextWrapped(tooltip); - } - - if (!show) - return; - - using (var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, _currentCode.Length > 0)) - { - var color = _codeService.CheckCode(_currentCode) != null ? ColorId.ActorAvailable : ColorId.ActorUnavailable; - using var c = ImRaii.PushColor(ImGuiCol.Border, color.Value(), _currentCode.Length > 0); - if (ImGui.InputTextWithHint("##Code", "Enter Cheat Code...", ref _currentCode, 512, ImGuiInputTextFlags.EnterReturnsTrue)) - if (_codeService.AddCode(_currentCode)) - _currentCode = string.Empty; - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker(tooltip); - - DrawCodeHints(); - - if (_config.Codes.Count <= 0) - return; - - for (var i = 0; i < _config.Codes.Count; ++i) - { - var (code, state) = _config.Codes[i]; - var action = _codeService.CheckCode(code); - if (action == null) - continue; - - if (ImGui.Checkbox(code, ref state)) - { - action(state); - _config.Codes[i] = (code, state); - _codeService.VerifyState(); - _config.Save(); - } - } - - if (_codeService.EnabledCaptain) - { - if (ImGui.Button("Who am I?!?")) - _funModule.WhoAmI(); - - ImGui.SameLine(); - - if (ImGui.Button("Who is that!?!")) - _funModule.WhoIsThat(); - } - } - - private void DrawCodeHints() - { - // TODO - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void Checkbox(string label, string tooltip, bool current, Action setter) - { - using var id = ImRaii.PushId(label); - var tmp = current; - if (ImGui.Checkbox(string.Empty, ref tmp) && tmp != current) - { - setter(tmp); - _config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker(label, tooltip); - } - - /// Different supported sort modes as a combo. - private void DrawFolderSortType() - { - var sortMode = _config.SortMode; - ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); - using (var combo = ImRaii.Combo("##sortMode", sortMode.Name)) - { - if (combo) - foreach (var val in Configuration.Constants.ValidSortModes) - { - if (ImGui.Selectable(val.Name, val.GetType() == sortMode.GetType()) && val.GetType() != sortMode.GetType()) - { - _config.SortMode = val; - _selector.SetFilterDirty(); - _config.Save(); - } - - ImGuiUtil.HoverTooltip(val.Description); - } - } - - ImGuiUtil.LabeledHelpMarker("Sort Mode", "Choose the sort mode for the mod selector in the designs tab."); - } -} diff --git a/Glamourer/Gui/Tabs/SettingsTab/CodeDrawer.cs b/Glamourer/Gui/Tabs/SettingsTab/CodeDrawer.cs new file mode 100644 index 0000000..1dc9331 --- /dev/null +++ b/Glamourer/Gui/Tabs/SettingsTab/CodeDrawer.cs @@ -0,0 +1,195 @@ +using Dalamud.Interface; +using Glamourer.Services; +using Glamourer.State; +using Dalamud.Bindings.ImGui; +using OtterGui.Filesystem; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Text.EndObjects; + +namespace Glamourer.Gui.Tabs.SettingsTab; + +public class CodeDrawer(Configuration config, CodeService codeService, FunModule funModule) : IUiService +{ + private static ReadOnlySpan Tooltip + => "Cheat Codes are not actually for cheating in the game, but for 'cheating' in Glamourer. "u8 + + "They allow for some fun easter-egg modes that usually manipulate the appearance of all players you see (including yourself) in some way."u8; + + private static ReadOnlySpan DragDropLabel + => "##CheatDrag"u8; + + private bool _showCodeHints; + private string _currentCode = string.Empty; + private int _dragCodeIdx = -1; + + + public void Draw() + { + var show = ImGui.CollapsingHeader("Cheat Codes"); + DrawTooltip(); + + if (!show) + return; + + DrawCodeInput(); + DrawCopyButtons(); + var knownFlags = DrawCodes(); + DrawCodeHints(knownFlags); + } + + private void DrawCodeInput() + { + var color = codeService.CheckCode(_currentCode).Item2 is not 0 ? ColorId.ActorAvailable : ColorId.ActorUnavailable; + using var border = ImRaii.PushFrameBorder(ImUtf8.GlobalScale, color.Value(), _currentCode.Length > 0); + ImGui.SetNextItemWidth(500 * ImUtf8.GlobalScale + ImUtf8.ItemSpacing.X); + if (ImUtf8.InputText("##Code"u8, ref _currentCode, "Enter Cheat Code..."u8, ImGuiInputTextFlags.EnterReturnsTrue)) + { + codeService.AddCode(_currentCode); + _currentCode = string.Empty; + } + + ImGui.SameLine(); + ImUtf8.Icon(FontAwesomeIcon.ExclamationCircle, ImGui.GetColorU32(ImGuiCol.TextDisabled)); + DrawTooltip(); + } + + private void DrawCopyButtons() + { + var buttonSize = new Vector2(250 * ImUtf8.GlobalScale, 0); + if (ImUtf8.Button("Who am I?!?"u8, buttonSize)) + funModule.WhoAmI(); + ImUtf8.HoverTooltip( + "Copy your characters actual current appearance including cheat codes or holiday events to the clipboard as a design."u8); + + ImGui.SameLine(); + + if (ImUtf8.Button("Who is that!?!"u8, buttonSize)) + funModule.WhoIsThat(); + ImUtf8.HoverTooltip( + "Copy your targets actual current appearance including cheat codes or holiday events to the clipboard as a design."u8); + } + + private CodeService.CodeFlag DrawCodes() + { + var canDelete = config.DeleteDesignModifier.IsActive(); + CodeService.CodeFlag knownFlags = 0; + for (var i = 0; i < config.Codes.Count; ++i) + { + using var id = ImUtf8.PushId(i); + var (code, state) = config.Codes[i]; + var (action, flag) = codeService.CheckCode(code); + if (flag is 0) + continue; + + var data = CodeService.GetData(flag); + + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, + $"Delete this cheat code.{(canDelete ? string.Empty : $"\nHold {config.DeleteDesignModifier} while clicking to delete.")}", + disabled: !canDelete)) + { + action!(false); + config.Codes.RemoveAt(i--); + codeService.SaveState(); + } + + knownFlags |= flag; + ImUtf8.SameLineInner(); + if (ImUtf8.Checkbox("\0"u8, ref state)) + { + action!(state); + codeService.SaveState(); + } + + var hovered = ImGui.IsItemHovered(); + ImGui.SameLine(); + ImUtf8.Selectable(code, false); + hovered |= ImGui.IsItemHovered(); + DrawSource(i, code); + DrawTarget(i); + if (hovered) + { + using var tt = ImUtf8.Tooltip(); + ImUtf8.Text(data.Effect); + } + } + + return knownFlags; + } + + private void DrawSource(int idx, string code) + { + using var source = ImUtf8.DragDropSource(); + if (!source) + return; + + if (!DragDropSource.SetPayload(DragDropLabel)) + _dragCodeIdx = idx; + ImUtf8.Text($"Dragging {code}..."); + } + + private void DrawTarget(int idx) + { + using var target = ImUtf8.DragDropTarget(); + if (!target.IsDropping(DragDropLabel) || _dragCodeIdx == -1) + return; + + if (config.Codes.Move(_dragCodeIdx, idx)) + codeService.SaveState(); + _dragCodeIdx = -1; + } + + private void DrawCodeHints(CodeService.CodeFlag knownFlags) + { + if (knownFlags.HasFlag(CodeService.AllHintCodes)) + return; + + if (ImUtf8.Button(_showCodeHints ? "Hide Hints"u8 : "Show Hints"u8)) + _showCodeHints = !_showCodeHints; + + if (!_showCodeHints) + return; + + foreach (var code in Enum.GetValues()) + { + if (knownFlags.HasFlag(code)) + continue; + + var data = CodeService.GetData(code); + if (!data.Display) + continue; + + ImGui.Dummy(Vector2.Zero); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); + ImUtf8.Text(data.Effect); + using var indent = ImRaii.PushIndent(2); + using (ImUtf8.Group()) + { + ImUtf8.Text("Capitalized letters: "u8); + ImUtf8.Text("Punctuation: "u8); + } + + ImUtf8.SameLineInner(); + using (ImUtf8.Group()) + { + using var mono = ImRaii.PushFont(UiBuilder.MonoFont); + ImUtf8.Text($"{data.CapitalCount}"); + ImUtf8.Text($"{data.Punctuation}"); + } + + ImUtf8.TextWrapped(data.Hint); + } + } + + + private static void DrawTooltip() + { + if (!ImGui.IsItemHovered()) + return; + + ImGui.SetNextWindowSize(new Vector2(400, 0)); + using var tt = ImUtf8.Tooltip(); + ImUtf8.TextWrapped(Tooltip); + } +} diff --git a/Glamourer/Gui/Tabs/SettingsTab/CollectionCombo.cs b/Glamourer/Gui/Tabs/SettingsTab/CollectionCombo.cs new file mode 100644 index 0000000..7080b4d --- /dev/null +++ b/Glamourer/Gui/Tabs/SettingsTab/CollectionCombo.cs @@ -0,0 +1,36 @@ +using Dalamud.Interface; +using Glamourer.Interop.Penumbra; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Log; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Widgets; + +namespace Glamourer.Gui.Tabs.SettingsTab; + +public sealed class CollectionCombo(Configuration config, PenumbraService penumbra, Logger log) + : FilterComboCache<(Guid Id, string IdShort, string Name)>( + () => penumbra.GetCollections().Select(kvp => (kvp.Key, kvp.Key.ToString()[..8], kvp.Value)).ToArray(), + MouseWheelType.Control, log), IUiService +{ + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var (_, idShort, name) = Items[globalIdx]; + if (config.Ephemeral.IncognitoMode) + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + return ImGui.Selectable(idShort); + } + + var ret = ImGui.Selectable(name, selected); + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)); + ImGuiUtil.RightAlign($"({idShort})"); + } + + return ret; + } +} diff --git a/Glamourer/Gui/Tabs/SettingsTab/CollectionOverrideDrawer.cs b/Glamourer/Gui/Tabs/SettingsTab/CollectionOverrideDrawer.cs new file mode 100644 index 0000000..5c4fec3 --- /dev/null +++ b/Glamourer/Gui/Tabs/SettingsTab/CollectionOverrideDrawer.cs @@ -0,0 +1,174 @@ +using Dalamud.Interface; +using Glamourer.Interop.Penumbra; +using Glamourer.Services; +using Dalamud.Bindings.ImGui; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; + +namespace Glamourer.Gui.Tabs.SettingsTab; + +public class CollectionOverrideDrawer( + CollectionOverrideService collectionOverrides, + Configuration config, + ActorObjectManager objects, + ActorManager actors, + PenumbraService penumbra, + CollectionCombo combo) : IService +{ + private string _newIdentifier = string.Empty; + private ActorIdentifier[] _identifiers = []; + private int _dragDropIndex = -1; + private Exception? _exception; + + public void Draw() + { + using var header = ImRaii.CollapsingHeader("Collection Overrides"); + ImGuiUtil.HoverTooltip( + "Here you can set up overrides for Penumbra collections that should have their settings changed when automatically applying mod settings from a design.\n" + + "Instead of the collection associated with the overridden character, the overridden collection will be manipulated."); + if (!header) + return; + + using var table = ImRaii.Table("table", 4, ImGuiTableFlags.RowBg); + if (!table) + return; + + var buttonSize = new Vector2(ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("buttons", ImGuiTableColumnFlags.WidthFixed, buttonSize.X); + ImGui.TableSetupColumn("identifiers", ImGuiTableColumnFlags.WidthStretch, 0.35f); + ImGui.TableSetupColumn("collections", ImGuiTableColumnFlags.WidthStretch, 0.4f); + ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthStretch, 0.25f); + + for (var i = 0; i < collectionOverrides.Overrides.Count; ++i) + DrawCollectionRow(ref i, buttonSize); + + DrawNewOverride(buttonSize); + } + + private void DrawCollectionRow(ref int idx, Vector2 buttonSize) + { + using var id = ImRaii.PushId(idx); + var (exists, actor, collection, name) = collectionOverrides.Fetch(idx); + + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), buttonSize, "Delete this override.", false, true)) + collectionOverrides.DeleteOverride(idx--); + + ImGui.TableNextColumn(); + DrawActorIdentifier(idx, actor); + + ImGui.TableNextColumn(); + if (combo.Draw("##collection", name, $"Select the overriding collection. Current GUID:", ImGui.GetContentRegionAvail().X, + ImGui.GetTextLineHeight())) + { + var (guid, _, newName) = combo.CurrentSelection; + collectionOverrides.ChangeOverride(idx, guid, newName); + } + + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted($" {collection}"); + } + + ImGui.TableNextColumn(); + DrawCollectionName(exists, collection, name); + } + + private void DrawCollectionName(bool exists, Guid collection, string name) + { + if (!exists) + { + ImGui.TextUnformatted(""); + if (!ImGui.IsItemHovered()) + return; + + using var tt1 = ImRaii.Tooltip(); + ImGui.TextUnformatted($"The design {name} with the GUID"); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImGui.TextUnformatted($" {collection}"); + } + + ImGui.TextUnformatted("does not exist in Penumbra."); + return; + } + + ImGui.TextUnformatted(config.Ephemeral.IncognitoMode ? collection.ToString()[..8] : name); + if (!ImGui.IsItemHovered()) + return; + + using var tt2 = ImRaii.Tooltip(); + using var f = ImRaii.PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted(collection.ToString()); + } + + private void DrawActorIdentifier(int idx, ActorIdentifier actor) + { + ImGui.Selectable(config.Ephemeral.IncognitoMode ? actor.Incognito(null) : actor.ToString()); + using (var target = ImRaii.DragDropTarget()) + { + if (target.Success && ImGuiUtil.IsDropping("DraggingOverride")) + { + collectionOverrides.MoveOverride(_dragDropIndex, idx); + _dragDropIndex = -1; + } + } + + using (var source = ImRaii.DragDropSource()) + { + if (source) + { + ImGui.SetDragDropPayload("DraggingOverride", null, 0); + ImGui.TextUnformatted($"Reordering Override #{idx + 1}..."); + _dragDropIndex = idx; + } + } + } + + private void DrawNewOverride(Vector2 buttonSize) + { + var (currentId, currentName) = penumbra.CurrentCollection; + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PersonCirclePlus.ToIconString(), buttonSize, "Add override for current player.", + !objects.Player.Valid && currentId != Guid.Empty, true)) + collectionOverrides.AddOverride([objects.PlayerData.Identifier], currentId, currentName); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputTextWithHint("##newActor", "New Identifier...", ref _newIdentifier, 80)) + try + { + _identifiers = actors.FromUserString(_newIdentifier, false); + } + catch (ActorIdentifierFactory.IdentifierParseError e) + { + _exception = e; + _identifiers = []; + } + + var tt = _identifiers.Any(i => i.IsValid) + ? $"Add a new override for {_identifiers.First(i => i.IsValid)}." + : _newIdentifier.Length == 0 + ? "Please enter an identifier string first." + : $"The identifier string {_newIdentifier} does not result in a valid identifier{(_exception == null ? "." : $":\n\n{_exception?.Message}")}"; + + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), buttonSize, tt, tt[0] is not 'A', true)) + collectionOverrides.AddOverride(_identifiers, currentId, currentName); + + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)); + ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString()); + } + + if (ImGui.IsItemHovered()) + ActorIdentifierFactory.WriteUserStringTooltip(false); + } +} diff --git a/Glamourer/Gui/Tabs/SettingsTab/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab/SettingsTab.cs new file mode 100644 index 0000000..e559841 --- /dev/null +++ b/Glamourer/Gui/Tabs/SettingsTab/SettingsTab.cs @@ -0,0 +1,525 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Game.ClientState.Keys; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.Utility; +using Dalamud.Plugin.Services; +using Glamourer.Automation; +using Glamourer.Designs; +using Glamourer.Events; +using Glamourer.Gui.Tabs.DesignTab; +using Glamourer.Interop; +using Glamourer.Interop.PalettePlus; +using Glamourer.Interop.Penumbra; +using Glamourer.Services; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Widgets; + +namespace Glamourer.Gui.Tabs.SettingsTab; + +public class SettingsTab( + Configuration config, + DesignFileSystemSelector selector, + ContextMenuService contextMenuService, + IUiBuilder uiBuilder, + GlamourerChangelog changelog, + IKeyState keys, + DesignColorUi designColorUi, + PaletteImport paletteImport, + CollectionOverrideDrawer overrides, + CodeDrawer codeDrawer, + Glamourer glamourer, + AutoDesignApplier autoDesignApplier, + AutoRedrawChanged autoRedraw, + PcpService pcpService) + : ITab +{ + private readonly VirtualKey[] _validKeys = keys.GetValidVirtualKeys().Prepend(VirtualKey.NO_KEY).ToArray(); + + public ReadOnlySpan Label + => "Settings"u8; + + public void DrawContent() + { + using var child = ImUtf8.Child("MainWindowChild"u8, default); + if (!child) + return; + + Checkbox("Enable Auto Designs"u8, + "Enable the application of designs associated to characters in the Automation tab to be applied automatically."u8, + config.EnableAutoDesigns, v => + { + config.EnableAutoDesigns = v; + autoDesignApplier.OnEnableAutoDesignsChanged(v); + }); + ImGui.NewLine(); + ImGui.NewLine(); + ImGui.NewLine(); + ImGui.NewLine(); + + using (ImUtf8.Child("SettingsChild"u8, default)) + { + DrawBehaviorSettings(); + DrawDesignDefaultSettings(); + DrawInterfaceSettings(); + DrawColorSettings(); + overrides.Draw(); + codeDrawer.Draw(); + } + + MainWindow.DrawSupportButtons(glamourer, changelog.Changelog); + } + + public void DrawPenumbraIntegrationSettings() + { + DrawPenumbraIntegrationSettings1(); + DrawPenumbraIntegrationSettings2(); + } + + private void DrawBehaviorSettings() + { + if (!ImUtf8.CollapsingHeader("Glamourer Behavior"u8)) + return; + + Checkbox("Always Apply Entire Weapon for Mainhand"u8, + "When manually applying a mainhand item, will also apply a corresponding offhand and potentially gauntlets for certain fist weapons."u8, + config.ChangeEntireItem, v => config.ChangeEntireItem = v); + Checkbox("Use Replacement Gear for Gear Unavailable to Your Race or Gender"u8, + "Use different gender- and race-appropriate models as a substitute when detecting certain items not available for a characters current gender and race."u8, + config.UseRestrictedGearProtection, v => config.UseRestrictedGearProtection = v); + Checkbox("Do Not Apply Unobtained Items in Automation"u8, + "Enable this if you want automatically applied designs to only consider items and customizations you have actually unlocked once, and skip those you have not."u8, + config.UnlockedItemMode, v => config.UnlockedItemMode = v); + Checkbox("Respect Manual Changes When Editing Automation"u8, + "Whether changing any currently active automation group will respect manual changes to the character before re-applying the changed automation or not."u8, + config.RespectManualOnAutomationUpdate, v => config.RespectManualOnAutomationUpdate = v); + Checkbox("Enable Festival Easter-Eggs"u8, + "Glamourer may do some fun things on specific dates. Disable this if you do not want your experience disrupted by this."u8, + config.DisableFestivals == 0, v => config.DisableFestivals = v ? (byte)0 : (byte)2); + DrawPenumbraIntegrationSettings1(); + Checkbox("Revert Manual Changes on Zone Change"u8, + "Restores the old behaviour of reverting your character to its game or automation base whenever you change the zone."u8, + config.RevertManualChangesOnZoneChange, v => config.RevertManualChangesOnZoneChange = v); + PaletteImportButton(); + DrawPenumbraIntegrationSettings2(); + Checkbox("Prevent Random Design Repeats"u8, + "When using random designs, prevent the same design from being chosen twice in a row."u8, + config.PreventRandomRepeats, v => config.PreventRandomRepeats = v); + ImGui.NewLine(); + } + + private void DrawPenumbraIntegrationSettings1() + { + Checkbox("Auto-Reload Gear"u8, + "Automatically reload equipment pieces on your own character when changing any mod options in Penumbra in their associated collection."u8, + config.AutoRedrawEquipOnChanges, v => + { + config.AutoRedrawEquipOnChanges = v; + autoRedraw.Invoke(v); + }); + Checkbox("Attach to PCP Handling"u8, + "Add the actor's glamourer state when a PCP is created by Penumbra, and create a design and apply it if possible when a PCP is installed by Penumbra."u8, + config.AttachToPcp, pcpService.Set); + var active = config.DeleteDesignModifier.IsActive(); + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Delete all PCP Designs"u8, "Deletes all designs tagged with 'PCP' from the design list."u8, disabled: !active)) + pcpService.CleanPcpDesigns(); + if (!active) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"\nHold {config.DeleteDesignModifier} while clicking."); + } + + private void DrawPenumbraIntegrationSettings2() + { + Checkbox("Always Apply Associated Mods"u8, + "Whenever a design is applied to a character (including via automation), Glamourer will try to apply its associated mod settings to the collection currently associated with that character, if it is available.\n\n"u8 + + "Glamourer will NOT revert these applied settings automatically. This may mess up your collection and configuration.\n\n"u8 + + "If you enable this setting, you are aware that any resulting misconfiguration is your own fault."u8, + config.AlwaysApplyAssociatedMods, v => config.AlwaysApplyAssociatedMods = v); + Checkbox("Use Temporary Mod Settings"u8, + "Apply all settings as temporary settings so they will be reset when Glamourer or the game shut down."u8, + config.UseTemporarySettings, + v => config.UseTemporarySettings = v); + } + + private void DrawDesignDefaultSettings() + { + if (!ImUtf8.CollapsingHeader("Design Defaults")) + return; + + Checkbox("Locked Designs"u8, "Newly created designs will be locked to prevent unintended changes."u8, + config.DefaultDesignSettings.Locked, v => config.DefaultDesignSettings.Locked = v); + Checkbox("Show in Quick Design Bar"u8, "Newly created designs will be shown in the quick design bar by default."u8, + config.DefaultDesignSettings.ShowQuickDesignBar, v => config.DefaultDesignSettings.ShowQuickDesignBar = v); + Checkbox("Reset Advanced Dyes"u8, "Newly created designs will be configured to reset advanced dyes on application by default."u8, + config.DefaultDesignSettings.ResetAdvancedDyes, v => config.DefaultDesignSettings.ResetAdvancedDyes = v); + Checkbox("Always Force Redraw"u8, "Newly created designs will be configured to force character redraws on application by default."u8, + config.DefaultDesignSettings.AlwaysForceRedrawing, v => config.DefaultDesignSettings.AlwaysForceRedrawing = v); + Checkbox("Reset Temporary Settings"u8, + "Newly created designs will be configured to clear all advanced settings applied by Glamourer to the collection by default."u8, + config.DefaultDesignSettings.ResetTemporarySettings, v => config.DefaultDesignSettings.ResetTemporarySettings = v); + + var tmp = config.PcpFolder; + ImGui.SetNextItemWidth(0.4f * ImGui.GetContentRegionAvail().X); + if (ImUtf8.InputText("##pcpFolder"u8, ref tmp)) + config.PcpFolder = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + config.Save(); + + ImGuiUtil.LabeledHelpMarker("Default PCP Organizational Folder", + "The folder any designs created due to penumbra character packs are moved to on creation.\nLeave blank to import into Root."); + + tmp = config.PcpColor; + ImGui.SetNextItemWidth(0.4f * ImGui.GetContentRegionAvail().X); + if (ImUtf8.InputText("##pcpColor"u8, ref tmp)) + config.PcpColor = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + config.Save(); + + ImGuiUtil.LabeledHelpMarker("Default PCP Design Color", + "The name of the color group any designs created due to penumbra character packs are assigned.\nLeave blank for no specific color assignment."); + } + + private void DrawInterfaceSettings() + { + if (!ImUtf8.CollapsingHeader("Interface"u8)) + return; + + EphemeralCheckbox("Show Quick Design Bar"u8, + "Show a bar separate from the main window that allows you to quickly apply designs or revert your character and target."u8, + config.Ephemeral.ShowDesignQuickBar, v => config.Ephemeral.ShowDesignQuickBar = v); + EphemeralCheckbox("Lock Quick Design Bar"u8, "Prevent the quick design bar from being moved and lock it in place."u8, + config.Ephemeral.LockDesignQuickBar, + v => config.Ephemeral.LockDesignQuickBar = v); + if (Widget.ModifiableKeySelector("Hotkey to Toggle Quick Design Bar", "Set a hotkey that opens or closes the quick design bar.", + 100 * ImGuiHelpers.GlobalScale, + config.ToggleQuickDesignBar, v => config.ToggleQuickDesignBar = v, _validKeys)) + config.Save(); + + Checkbox("Show Quick Design Bar in Main Window"u8, + "Show the quick design bar in the tab selection part of the main window, too."u8, + config.ShowQuickBarInTabs, v => config.ShowQuickBarInTabs = v); + DrawQuickDesignBoxes(); + + ImGui.Dummy(Vector2.Zero); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); + + Checkbox("Enable Game Context Menus"u8, "Whether to show a Try On via Glamourer button on context menus for equippable items."u8, + config.EnableGameContextMenu, v => + { + config.EnableGameContextMenu = v; + if (v) + contextMenuService.Enable(); + else + contextMenuService.Disable(); + }); + Checkbox("Show Window when UI is Hidden"u8, "Whether to show Glamourer windows even when the games UI is hidden."u8, + config.ShowWindowWhenUiHidden, v => + { + config.ShowWindowWhenUiHidden = v; + uiBuilder.DisableUserUiHide = v; + }); + Checkbox("Hide Window in Cutscenes"u8, + "Whether the main Glamourer window should automatically be hidden when entering cutscenes or not."u8, + config.HideWindowInCutscene, + v => + { + config.HideWindowInCutscene = v; + uiBuilder.DisableCutsceneUiHide = !v; + }); + EphemeralCheckbox("Lock Main Window"u8, "Prevent the main window from being moved and lock it in place."u8, + config.Ephemeral.LockMainWindow, + v => config.Ephemeral.LockMainWindow = v); + Checkbox("Open Main Window at Game Start"u8, "Whether the main Glamourer window should be open or closed after launching the game."u8, + config.OpenWindowAtStart, v => config.OpenWindowAtStart = v); + ImGui.Dummy(Vector2.Zero); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); + + Checkbox("Smaller Equip Display"u8, "Use single-line display without icons and small dye buttons instead of double-line display."u8, + config.SmallEquip, v => config.SmallEquip = v); + DrawHeightUnitSettings(); + Checkbox("Show Application Checkboxes"u8, + "Show the application checkboxes in the Customization and Equipment panels of the design tab, instead of only showing them under Application Rules."u8, + !config.HideApplyCheckmarks, v => config.HideApplyCheckmarks = !v); + if (Widget.DoubleModifierSelector("Design Deletion Modifier", + "A modifier you need to hold while clicking the Delete Design button for it to take effect.", 100 * ImGuiHelpers.GlobalScale, + config.DeleteDesignModifier, v => config.DeleteDesignModifier = v)) + config.Save(); + if (Widget.DoubleModifierSelector("Incognito Modifier", + "A modifier you need to hold while clicking the Incognito button for it to take effect.", 100 * ImGuiHelpers.GlobalScale, + config.IncognitoModifier, v => config.IncognitoModifier = v)) + config.Save(); + DrawRenameSettings(); + Checkbox("Auto-Open Design Folders"u8, + "Have design folders open or closed as their default state after launching."u8, config.OpenFoldersByDefault, + v => config.OpenFoldersByDefault = v); + DrawFolderSortType(); + + ImGui.NewLine(); + ImUtf8.Text("Show the following panels in their respective tabs:"u8); + ImGui.Dummy(Vector2.Zero); + DesignPanelFlagExtensions.DrawTable("##panelTable"u8, config.HideDesignPanel, config.AutoExpandDesignPanel, v => + { + config.HideDesignPanel = v; + config.Save(); + }, v => + { + config.AutoExpandDesignPanel = v; + config.Save(); + }); + + + ImGui.Dummy(Vector2.Zero); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); + + Checkbox("Allow Double-Clicking Designs to Apply"u8, + "Tries to apply a design to the current player character When double-clicking it in the design selector."u8, + config.AllowDoubleClickToApply, v => config.AllowDoubleClickToApply = v); + Checkbox("Show all Application Rule Checkboxes for Automation"u8, + "Show multiple separate application rule checkboxes for automated designs, instead of a single box for enabling or disabling."u8, + config.ShowAllAutomatedApplicationRules, v => config.ShowAllAutomatedApplicationRules = v); + Checkbox("Show Unobtained Item Warnings"u8, + "Show information whether you have unlocked all items and customizations in your automated design or not."u8, + config.ShowUnlockedItemWarnings, v => config.ShowUnlockedItemWarnings = v); + Checkbox("Show Color Display Config"u8, "Show the Color Display configuration options in the Advanced Customization panels."u8, + config.ShowColorConfig, v => config.ShowColorConfig = v); + Checkbox("Show Palette+ Import Button"u8, + "Show the import button that allows you to import Palette+ palettes onto a design in the Advanced Customization options section for designs."u8, + config.ShowPalettePlusImport, v => config.ShowPalettePlusImport = v); + using (ImRaii.PushId(1)) + { + PaletteImportButton(); + } + + Checkbox("Keep Advanced Dye Window Attached"u8, + "Keeps the advanced dye window expansion attached to the main window, or makes it freely movable."u8, + config.KeepAdvancedDyesAttached, v => config.KeepAdvancedDyesAttached = v); + + Checkbox("Debug Mode"u8, "Show the debug tab. Only useful for debugging or advanced use. Not recommended in general."u8, + config.DebugMode, + v => config.DebugMode = v); + ImGui.NewLine(); + } + + private void DrawQuickDesignBoxes() + { + var showAuto = config.EnableAutoDesigns; + var numColumns = 9 - (showAuto ? 0 : 2) - (config.UseTemporarySettings ? 0 : 1); + ImGui.NewLine(); + ImUtf8.Text("Show the Following Buttons in the Quick Design Bar:"u8); + ImGui.Dummy(Vector2.Zero); + using var table = ImUtf8.Table("##tableQdb"u8, numColumns, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Borders | ImGuiTableFlags.NoHostExtendX); + if (!table) + return; + + ReadOnlySpan<(string, bool, QdbButtons)> columns = + [ + ("Apply Design", true, QdbButtons.ApplyDesign), + ("Revert All", true, QdbButtons.RevertAll), + ("Revert to Auto", showAuto, QdbButtons.RevertAutomation), + ("Reapply Auto", showAuto, QdbButtons.ReapplyAutomation), + ("Revert Equip", true, QdbButtons.RevertEquip), + ("Revert Customize", true, QdbButtons.RevertCustomize), + ("Revert Advanced Customization", true, QdbButtons.RevertAdvancedCustomization), + ("Revert Advanced Dyes", true, QdbButtons.RevertAdvancedDyes), + ("Reset Settings", config.UseTemporarySettings, QdbButtons.ResetSettings), + ]; + + for (var i = 0; i < columns.Length; ++i) + { + if (!columns[i].Item2) + continue; + + ImGui.TableNextColumn(); + ImUtf8.TableHeader(columns[i].Item1); + } + + for (var i = 0; i < columns.Length; ++i) + { + if (!columns[i].Item2) + continue; + + var flag = columns[i].Item3; + using var id = ImUtf8.PushId((int)flag); + ImGui.TableNextColumn(); + var offset = (ImGui.GetContentRegionAvail().X - ImGui.GetFrameHeight()) / 2; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offset); + var value = config.QdbButtons.HasFlag(flag); + if (!ImUtf8.Checkbox(""u8, ref value)) + continue; + + var buttons = value ? config.QdbButtons | flag : config.QdbButtons & ~flag; + if (buttons == config.QdbButtons) + continue; + + config.QdbButtons = buttons; + config.Save(); + } + } + + private void PaletteImportButton() + { + if (!config.ShowPalettePlusImport) + return; + + ImGui.SameLine(); + if (ImUtf8.Button("Import Palette+ to Designs"u8)) + paletteImport.ImportDesigns(); + ImUtf8.HoverTooltip( + $"Import all existing Palettes from your Palette+ Config into Designs at PalettePlus/[Name] if these do not exist. Existing Palettes are:\n\n\t - {string.Join("\n\t - ", paletteImport.Data.Keys)}"); + } + + /// Draw the entire Color subsection. + private void DrawColorSettings() + { + if (!ImUtf8.CollapsingHeader("Colors"u8)) + return; + + using (var tree = ImUtf8.TreeNode("Custom Design Colors"u8)) + { + if (tree) + designColorUi.Draw(); + } + + using (var tree = ImUtf8.TreeNode("Color Settings"u8)) + { + if (tree) + foreach (var color in Enum.GetValues()) + { + var (defaultColor, name, description) = color.Data(); + var currentColor = config.Colors.GetValueOrDefault(color, defaultColor); + if (Widget.ColorPicker(name, description, currentColor, c => config.Colors[color] = c, defaultColor)) + config.Save(); + } + } + + ImGui.NewLine(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Checkbox(ReadOnlySpan label, ReadOnlySpan tooltip, bool current, Action setter) + { + using var id = ImUtf8.PushId(label); + var tmp = current; + if (ImUtf8.Checkbox(""u8, ref tmp) && tmp != current) + { + setter(tmp); + config.Save(); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker(label, tooltip); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void EphemeralCheckbox(ReadOnlySpan label, ReadOnlySpan tooltip, bool current, Action setter) + { + using var id = ImUtf8.PushId(label); + var tmp = current; + if (ImUtf8.Checkbox(""u8, ref tmp) && tmp != current) + { + setter(tmp); + config.Ephemeral.Save(); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker(label, tooltip); + } + + /// Different supported sort modes as a combo. + private void DrawFolderSortType() + { + var sortMode = config.SortMode; + ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); + using (var combo = ImUtf8.Combo("##sortMode"u8, sortMode.Name)) + { + if (combo) + foreach (var val in Configuration.Constants.ValidSortModes) + { + if (ImUtf8.Selectable(val.Name, val.GetType() == sortMode.GetType()) && val.GetType() != sortMode.GetType()) + { + config.SortMode = val; + selector.SetFilterDirty(); + config.Save(); + } + + ImUtf8.HoverTooltip(val.Description); + } + } + + ImUtf8.LabeledHelpMarker("Sort Mode"u8, "Choose the sort mode for the mod selector in the designs tab."u8); + } + + private void DrawRenameSettings() + { + ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); + using (var combo = ImUtf8.Combo("##renameSettings"u8, config.ShowRename.GetData().Name)) + { + if (combo) + foreach (var value in Enum.GetValues()) + { + var (name, desc) = value.GetData(); + if (ImGui.Selectable(name, config.ShowRename == value)) + { + config.ShowRename = value; + selector.SetRenameSearchPath(value); + config.Save(); + } + + ImUtf8.HoverTooltip(desc); + } + } + + ImGui.SameLine(); + const string tt = + "Select which of the two renaming input fields are visible when opening the right-click context menu of a design in the design selector."; + ImGuiComponents.HelpMarker(tt); + ImGui.SameLine(); + ImUtf8.Text("Rename Fields in Design Context Menu"u8); + ImUtf8.HoverTooltip(tt); + } + + private void DrawHeightUnitSettings() + { + ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); + using (var combo = ImUtf8.Combo("##heightUnit"u8, HeightDisplayTypeName(config.HeightDisplayType))) + { + if (combo) + foreach (var type in Enum.GetValues()) + { + if (ImUtf8.Selectable(HeightDisplayTypeName(type), type == config.HeightDisplayType) && type != config.HeightDisplayType) + { + config.HeightDisplayType = type; + config.Save(); + } + } + } + + ImGui.SameLine(); + const string tt = "Select how to display the height of characters in real-world units, if at all."; + ImGuiComponents.HelpMarker(tt); + ImGui.SameLine(); + ImUtf8.Text("Character Height Display Type"u8); + ImUtf8.HoverTooltip(tt); + } + + private static ReadOnlySpan HeightDisplayTypeName(HeightDisplayType type) + => type switch + { + HeightDisplayType.None => "Do Not Display"u8, + HeightDisplayType.Centimetre => "Centimetres (000.0 cm)"u8, + HeightDisplayType.Metre => "Metres (0.00 m)"u8, + HeightDisplayType.Wrong => "Inches (00.0 in)"u8, + HeightDisplayType.WrongFoot => "Feet (0'00'')"u8, + HeightDisplayType.Corgi => "Corgis (0.0 Corgis)"u8, + HeightDisplayType.OlympicPool => "Olympic-size swimming Pools (0.000 Pools)"u8, + _ => ""u8, + }; +} diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs index 2bd79d8..8644aeb 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs @@ -1,37 +1,40 @@ -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface.Utility; -using Glamourer.Customization; +using Glamourer.GameData; using Glamourer.Interop; +using Glamourer.Interop.Penumbra; using Glamourer.Services; using Glamourer.Unlocks; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using ImGuiClip = OtterGui.ImGuiClip; namespace Glamourer.Gui.Tabs.UnlocksTab; -public class UnlockOverview +public class UnlockOverview( + ItemManager items, + CustomizeService customizations, + ItemUnlockManager itemUnlocks, + CustomizeUnlockManager customizeUnlocks, + PenumbraChangedItemTooltip tooltip, + TextureService textures, + CodeService codes, + JobService jobs, + FavoriteManager favorites, + PenumbraService penumbra) { - private readonly ItemManager _items; - private readonly ItemUnlockManager _itemUnlocks; - private readonly CustomizationService _customizations; - private readonly CustomizeUnlockManager _customizeUnlocks; - private readonly PenumbraChangedItemTooltip _tooltip; - private readonly TextureService _textures; - private readonly CodeService _codes; - private readonly JobService _jobs; - private readonly FavoriteManager _favorites; - private static readonly Vector4 UnavailableTint = new(0.3f, 0.3f, 0.3f, 1.0f); private FullEquipType _selected1 = FullEquipType.Unknown; private SubRace _selected2 = SubRace.Unknown; private Gender _selected3 = Gender.Unknown; + private BonusItemFlag _selected4 = BonusItemFlag.Unknown; + + private uint _favoriteColor; + private uint _moddedColor; private void DrawSelector() { @@ -41,7 +44,7 @@ public class UnlockOverview foreach (var type in Enum.GetValues()) { - if (type.IsOffhandType() || !_items.ItemService.AwaitedService.TryGetValue(type, out var items) || items.Count == 0) + if (type.IsOffhandType() || type.IsBonus() || !items.ItemData.ByType.TryGetValue(type, out var value) || value.Count == 0) continue; if (ImGui.Selectable(type.ToName(), _selected1 == type)) @@ -49,42 +52,34 @@ public class UnlockOverview _selected1 = type; _selected2 = SubRace.Unknown; _selected3 = Gender.Unknown; + _selected4 = BonusItemFlag.Unknown; } } - foreach (var clan in _customizations.AwaitedService.Clans) + if (ImGui.Selectable("Bonus Items", _selected4 == BonusItemFlag.Glasses)) { - foreach (var gender in _customizations.AwaitedService.Genders) - { - if (_customizations.AwaitedService.GetList(clan, gender).HairStyles.Count == 0) - continue; + _selected1 = FullEquipType.Unknown; + _selected2 = SubRace.Unknown; + _selected3 = Gender.Unknown; + _selected4 = BonusItemFlag.Glasses; + } - if (ImGui.Selectable($"{(gender is Gender.Male ? '♂' : '♀')} {clan.ToShortName()} Hair & Paint", - _selected2 == clan && _selected3 == gender)) - { - _selected1 = FullEquipType.Unknown; - _selected2 = clan; - _selected3 = gender; - } + foreach (var (clan, gender) in CustomizeManager.AllSets()) + { + if (customizations.Manager.GetSet(clan, gender).HairStyles.Count == 0) + continue; + + if (ImGui.Selectable($"{(gender is Gender.Male ? '♂' : '♀')} {clan.ToShortName()} Hair & Paint", + _selected2 == clan && _selected3 == gender)) + { + _selected1 = FullEquipType.Unknown; + _selected2 = clan; + _selected3 = gender; + _selected4 = BonusItemFlag.Unknown; } } } - public UnlockOverview(ItemManager items, CustomizationService customizations, ItemUnlockManager itemUnlocks, - CustomizeUnlockManager customizeUnlocks, PenumbraChangedItemTooltip tooltip, TextureService textures, CodeService codes, - JobService jobs, FavoriteManager favorites) - { - _items = items; - _customizations = customizations; - _itemUnlocks = itemUnlocks; - _customizeUnlocks = customizeUnlocks; - _tooltip = tooltip; - _textures = textures; - _codes = codes; - _jobs = jobs; - _favorites = favorites; - } - public void Draw() { using var color = ImRaii.PushColor(ImGuiCol.Border, ImGui.GetColorU32(ImGuiCol.TableBorderStrong)); @@ -99,15 +94,20 @@ public class UnlockOverview if (!child) return; + _moddedColor = ColorId.ModdedItemMarker.Value(); + _favoriteColor = ColorId.FavoriteStarOn.Value(); + if (_selected1 is not FullEquipType.Unknown) DrawItems(); else if (_selected2 is not SubRace.Unknown && _selected3 is not Gender.Unknown) DrawCustomizations(); + else if (_selected4 is not BonusItemFlag.Unknown) + DrawBonusItems(); } private void DrawCustomizations() { - var set = _customizations.AwaitedService.GetList(_selected2, _selected3); + var set = customizations.Manager.GetSet(_selected2, _selected3); var spacing = IconSpacing; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); @@ -117,20 +117,25 @@ public class UnlockOverview var counter = 0; foreach (var customize in set.HairStyles.Concat(set.FacePaints)) { - if (!_customizeUnlocks.Unlockable.TryGetValue(customize, out var unlockData)) + if (!customizeUnlocks.Unlockable.TryGetValue(customize, out var unlockData)) continue; - var unlocked = _customizeUnlocks.IsUnlocked(customize, out var time); - var icon = _customizations.AwaitedService.GetIcon(customize.IconId); + var unlocked = customizeUnlocks.IsUnlocked(customize, out var time); + var icon = customizations.Manager.GetIcon(customize.IconId); + var hasIcon = icon.TryGetWrap(out var wrap, out _); + ImGui.Image(wrap?.Handle ?? icon.GetWrapOrEmpty().Handle, iconSize, Vector2.Zero, Vector2.One, + unlocked || codes.Enabled(CodeService.CodeFlag.Shirts) ? Vector4.One : UnavailableTint); - ImGui.Image(icon.ImGuiHandle, iconSize, Vector2.Zero, Vector2.One, - unlocked || _codes.EnabledShirts ? Vector4.One : UnavailableTint); - if (ImGui.IsItemHovered()) + if (favorites.Contains(_selected3, _selected2, customize.Index, customize.Value)) + ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), _favoriteColor, + 12 * ImGuiHelpers.GlobalScale, ImDrawFlags.RoundCornersAll, 6 * ImGuiHelpers.GlobalScale); + + if (hasIcon && ImGui.IsItemHovered()) { using var tt = ImRaii.Tooltip(); - var size = new Vector2(icon.Width, icon.Height); + var size = new Vector2(wrap!.Width, wrap.Height); if (size.X >= iconSize.X && size.Y >= iconSize.Y) - ImGui.Image(icon.ImGuiHandle, size); + ImGui.Image(wrap.Handle, size); ImGui.TextUnformatted(unlockData.Name); ImGui.TextUnformatted($"{customize.Index.ToDefaultName()} {customize.Value.Value}"); ImGui.TextUnformatted(unlocked ? $"Unlocked on {time:g}" : "Not unlocked."); @@ -148,84 +153,23 @@ public class UnlockOverview } } - private void DrawItems() + private void DrawBonusItems() { - if (!_items.ItemService.AwaitedService.TryGetValue(_selected1, out var items)) - return; - var spacing = IconSpacing; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); var iconSize = ImGuiHelpers.ScaledVector2(64); var iconsPerRow = IconsPerRow(iconSize.X, spacing.X); - var numRows = (items.Count + iconsPerRow - 1) / iconsPerRow; + var numRows = (items.DictBonusItems.Count + iconsPerRow - 1) / iconsPerRow; var numVisibleRows = (int)(Math.Ceiling(ImGui.GetContentRegionAvail().Y / (iconSize.Y + spacing.Y)) + 0.5f) + 1; - void DrawItem(EquipItem item) - { - var unlocked = _itemUnlocks.IsUnlocked(item.Id, out var time); - if (!_textures.TryLoadIcon(item.IconId.Id, out var iconHandle)) - return; - - var (icon, size) = (iconHandle.ImGuiHandle, new Vector2(iconHandle.Width, iconHandle.Height)); - - ImGui.Image(icon, iconSize, Vector2.Zero, Vector2.One, unlocked || _codes.EnabledShirts ? Vector4.One : UnavailableTint); - if (_favorites.Contains(item)) - ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), ColorId.FavoriteStarOn.Value(), - 2 * ImGuiHelpers.GlobalScale, ImDrawFlags.RoundCornersAll, 4 * ImGuiHelpers.GlobalScale); - - if (ImGui.IsItemClicked()) - Glamourer.Messager.Chat.Print(new SeStringBuilder().AddItemLink(item.ItemId.Id, false).BuiltString); - - if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && _tooltip.Player(out var state)) - _tooltip.ApplyItem(state, item); - - if (ImGui.IsItemHovered()) - { - using var tt = ImRaii.Tooltip(); - if (size.X >= iconSize.X && size.Y >= iconSize.Y) - ImGui.Image(icon, size); - ImGui.TextUnformatted(item.Name); - var slot = item.Type.ToSlot(); - ImGui.TextUnformatted($"{item.Type.ToName()} ({slot.ToName()})"); - if (item.Type.ValidOffhand().IsOffhandType()) - ImGui.TextUnformatted( - $"{item.Weapon()}{(_items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand) ? $" | {offhand.Weapon()}" : string.Empty)}"); - else - ImGui.TextUnformatted(slot is EquipSlot.MainHand ? $"{item.Weapon()}" : $"{item.Armor()}"); - ImGui.TextUnformatted( - unlocked ? time == DateTimeOffset.MinValue ? "Always Unlocked" : $"Unlocked on {time:g}" : "Not Unlocked."); - - if (item.Level.Value <= 1) - { - if (item.JobRestrictions.Id <= 1 || item.JobRestrictions.Id >= _jobs.AllJobGroups.Count) - ImGui.TextUnformatted("For Everyone"); - else - ImGui.TextUnformatted($"For all {_jobs.AllJobGroups[item.JobRestrictions.Id].Name}"); - } - else - { - if (item.JobRestrictions.Id <= 1 || item.JobRestrictions.Id >= _jobs.AllJobGroups.Count) - ImGui.TextUnformatted($"For Everyone of at least Level {item.Level}"); - else - ImGui.TextUnformatted($"For all {_jobs.AllJobGroups[item.JobRestrictions.Id].Name} of at least Level {item.Level}"); - } - - if (item.Flags.HasFlag(ItemFlags.IsDyable)) - ImGui.TextUnformatted("Dyable"); - if (item.Flags.HasFlag(ItemFlags.IsTradable)) - ImGui.TextUnformatted("Tradable"); - if (item.Flags.HasFlag(ItemFlags.IsCrestWorthy)) - ImGui.TextUnformatted("Can apply Crest"); - _tooltip.CreateTooltip(item, string.Empty, false); - } - } - var skips = ImGuiClip.GetNecessarySkips(iconSize.Y + spacing.Y); - var end = Math.Min(numVisibleRows * iconsPerRow + skips * iconsPerRow, items.Count); + var start = skips * iconsPerRow; + var end = Math.Min(numVisibleRows * iconsPerRow + skips * iconsPerRow, items.DictBonusItems.Count); var counter = 0; - for (var idx = skips * iconsPerRow; idx < end; ++idx) + + foreach (var item in items.DictBonusItems.Values.Skip(start).Take(end - start)) { - DrawItem(items[idx]); + DrawItem(item); if (counter != iconsPerRow - 1) { ImGui.SameLine(); @@ -242,6 +186,142 @@ public class UnlockOverview var remainder = numRows - numVisibleRows - skips; if (remainder > 0) ImGuiClip.DrawEndDummy(remainder, iconSize.Y + spacing.Y); + + void DrawItem(EquipItem item) + { + // TODO check unlocks + var unlocked = true; + if (!textures.TryLoadIcon(item.IconId.Id, out var iconHandle)) + return; + + var (icon, size) = (iconHandle.Handle, new Vector2(iconHandle.Width, iconHandle.Height)); + + ImGui.Image(icon, iconSize, Vector2.Zero, Vector2.One, + unlocked || codes.Enabled(CodeService.CodeFlag.Shirts) ? Vector4.One : UnavailableTint); + if (favorites.Contains(item)) + ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), _favoriteColor, + 2 * ImGuiHelpers.GlobalScale, ImDrawFlags.RoundCornersAll, 4 * ImGuiHelpers.GlobalScale); + + var mods = DrawModdedMarker(item, iconSize); + + // TODO handle clicking + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + if (size.X >= iconSize.X && size.Y >= iconSize.Y) + ImGui.Image(icon, size); + ImUtf8.Text(item.Name); + ImUtf8.Text($"{item.Type.ToName()}"); + ImUtf8.Text($"{item.Id.Id}"); + ImUtf8.Text($"{item.PrimaryId.Id}-{item.Variant.Id}"); + // TODO + ImUtf8.Text("Always Unlocked"u8); // : $"Unlocked on {time:g}" : "Not Unlocked."); + // TODO + //tooltip.CreateTooltip(item, string.Empty, false); + DrawModTooltip(mods); + } + } + } + + private void DrawItems() + { + if (!items.ItemData.ByType.TryGetValue(_selected1, out var value)) + return; + + var spacing = IconSpacing; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); + var iconSize = ImGuiHelpers.ScaledVector2(64); + var iconsPerRow = IconsPerRow(iconSize.X, spacing.X); + var numRows = (value.Count + iconsPerRow - 1) / iconsPerRow; + var numVisibleRows = (int)(Math.Ceiling(ImGui.GetContentRegionAvail().Y / (iconSize.Y + spacing.Y)) + 0.5f) + 1; + + var skips = ImGuiClip.GetNecessarySkips(iconSize.Y + spacing.Y); + var end = Math.Min(numVisibleRows * iconsPerRow + skips * iconsPerRow, value.Count); + var counter = 0; + for (var idx = skips * iconsPerRow; idx < end; ++idx) + { + DrawItem(value[idx]); + if (counter != iconsPerRow - 1) + { + ImGui.SameLine(); + ++counter; + } + else + { + counter = 0; + } + } + + if (ImGui.GetCursorPosX() != 0) + ImGui.NewLine(); + var remainder = numRows - numVisibleRows - skips; + if (remainder > 0) + ImGuiClip.DrawEndDummy(remainder, iconSize.Y + spacing.Y); + return; + + void DrawItem(EquipItem item) + { + var unlocked = itemUnlocks.IsUnlocked(item.Id, out var time); + if (!textures.TryLoadIcon(item.IconId.Id, out var iconHandle)) + return; + + var (icon, size) = (iconHandle.Handle, new Vector2(iconHandle.Width, iconHandle.Height)); + + ImGui.Image(icon, iconSize, Vector2.Zero, Vector2.One, + unlocked || codes.Enabled(CodeService.CodeFlag.Shirts) ? Vector4.One : UnavailableTint); + if (favorites.Contains(item)) + ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), ColorId.FavoriteStarOn.Value(), + 2 * ImGuiHelpers.GlobalScale, ImDrawFlags.RoundCornersAll, 4 * ImGuiHelpers.GlobalScale); + + var mods = DrawModdedMarker(item, iconSize); + + if (ImGui.IsItemClicked()) + Glamourer.Messager.Chat.Print(new SeStringBuilder().AddItemLink(item.ItemId.Id, false).BuiltString); + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && tooltip.Player(out var state)) + tooltip.ApplyItem(state, item); + + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + if (size.X >= iconSize.X && size.Y >= iconSize.Y) + ImGui.Image(icon, size); + ImGui.TextUnformatted(item.Name); + var slot = item.Type.ToSlot(); + ImGui.TextUnformatted($"{item.Type.ToName()} ({slot.ToName()})"); + if (item.Type.ValidOffhand().IsOffhandType()) + ImGui.TextUnformatted( + $"{item.Weapon()}{(items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand) ? $" | {offhand.Weapon()}" : string.Empty)}"); + else + ImGui.TextUnformatted(slot is EquipSlot.MainHand ? $"{item.Weapon()}" : $"{item.Armor()}"); + ImGui.TextUnformatted( + unlocked ? time == DateTimeOffset.MinValue ? "Always Unlocked" : $"Unlocked on {time:g}" : "Not Unlocked."); + + if (item.Level.Value <= 1) + { + if (item.JobRestrictions.Id <= 1 || item.JobRestrictions.Id >= jobs.AllJobGroups.Count) + ImGui.TextUnformatted("For Everyone"); + else + ImGui.TextUnformatted($"For all {jobs.AllJobGroups[item.JobRestrictions.Id].Name}"); + } + else + { + if (item.JobRestrictions.Id <= 1 || item.JobRestrictions.Id >= jobs.AllJobGroups.Count) + ImGui.TextUnformatted($"For Everyone of at least Level {item.Level}"); + else + ImGui.TextUnformatted($"For all {jobs.AllJobGroups[item.JobRestrictions.Id].Name} of at least Level {item.Level}"); + } + + if (item.Flags.HasFlag(ItemFlags.IsDyable1)) + ImGui.TextUnformatted(item.Flags.HasFlag(ItemFlags.IsDyable2) ? "Dyable (2 Slots)" : "Dyable"); + if (item.Flags.HasFlag(ItemFlags.IsTradable)) + ImGui.TextUnformatted("Tradable"); + if (item.Flags.HasFlag(ItemFlags.IsCrestWorthy)) + ImGui.TextUnformatted("Can apply Crest"); + DrawModTooltip(mods); + tooltip.CreateTooltip(item, string.Empty, false); + } + } } private static Vector2 IconSpacing @@ -249,4 +329,36 @@ public class UnlockOverview private static int IconsPerRow(float iconWidth, float iconSpacing) => (int)(ImGui.GetContentRegionAvail().X / (iconWidth + iconSpacing)); + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + private (string ModDirectory, string ModName)[] DrawModdedMarker(in EquipItem item, Vector2 iconSize) + { + var mods = penumbra.CheckCurrentChangedItem(item.Name); + if (mods.Length == 0) + return mods; + + var center = ImGui.GetItemRectMin() + new Vector2(iconSize.X * 0.85f, iconSize.Y * 0.15f); + ImGui.GetWindowDrawList().AddCircleFilled(center, iconSize.X * 0.1f, _moddedColor); + ImGui.GetWindowDrawList().AddCircle(center, iconSize.X * 0.1f, 0xFF000000); + return mods; + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + private void DrawModTooltip((string ModDirectory, string ModName)[] mods) + { + switch (mods.Length) + { + case 0: return; + case 1: + ImUtf8.Text("Modded by: "u8, _moddedColor); + ImGui.SameLine(0, 0); + ImUtf8.Text(mods[0].ModName); + return; + default: + ImUtf8.Text("Modded by:"u8, _moddedColor); + foreach (var (_, mod) in mods) + ImUtf8.BulletText(mod); + return; + } + } } diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs index 5ccd037..2323ca2 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs @@ -1,20 +1,16 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface; using Dalamud.Interface.Utility; using Glamourer.Events; using Glamourer.Interop; +using Glamourer.Interop.Penumbra; using Glamourer.Services; -using Glamourer.Structs; using Glamourer.Unlocks; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Table; +using OtterGui.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -22,35 +18,65 @@ namespace Glamourer.Gui.Tabs.UnlocksTab; public class UnlockTable : Table, IDisposable { - private readonly ObjectUnlocked _event; + private readonly ObjectUnlocked _event; + private readonly PenumbraService _penumbra; + + private Guid _lastCurrentCollection = Guid.Empty; public UnlockTable(ItemManager items, TextureService textures, ItemUnlockManager itemUnlocks, - PenumbraChangedItemTooltip tooltip, ObjectUnlocked @event, JobService jobs, FavoriteManager favorites) + PenumbraChangedItemTooltip tooltip, ObjectUnlocked @event, JobService jobs, FavoriteManager favorites, PenumbraService penumbra) : base("ItemUnlockTable", new ItemList(items), new FavoriteColumn(favorites, @event) { Label = "F" }, + new ModdedColumn(penumbra) { Label = "M" }, new NameColumn(textures, tooltip) { Label = "Item Name..." }, - new SlotColumn() { Label = "Equip Slot" }, - new TypeColumn() { Label = "Item Type..." }, + new SlotColumn { Label = "Equip Slot" }, + new TypeColumn { Label = "Item Type..." }, new UnlockDateColumn(itemUnlocks) { Label = "Unlocked" }, - new ItemIdColumn() { Label = "Item Id..." }, + new ItemIdColumn { Label = "Item Id..." }, new ModelDataColumn(items) { Label = "Model Data..." }, new JobColumn(jobs) { Label = "Jobs" }, - new LevelColumn() { Label = "Level..." }, - new DyableColumn() { Label = "Dye" }, - new CrestColumn() { Label = "Crest" }, - new TradableColumn() { Label = "Trade" } + new RequiredLevelColumn { Label = "Level..." }, + new DyableColumn { Label = "Dye" }, + new CrestColumn { Label = "Crest" }, + new TradableColumn { Label = "Trade" } ) { - _event = @event; - Sortable = true; - Flags |= ImGuiTableFlags.Hideable; + _event = @event; + _penumbra = penumbra; + Sortable = true; + Flags |= ImGuiTableFlags.Hideable | ImGuiTableFlags.Reorderable | ImGuiTableFlags.Resizable; _event.Subscribe(OnObjectUnlock, ObjectUnlocked.Priority.UnlockTable); + _penumbra.ModSettingChanged += OnModSettingsChanged; + + } + + private void OnModSettingsChanged(Penumbra.Api.Enums.ModSettingChange type, Guid collection, string mod, bool inherited) + { + if (collection != _lastCurrentCollection) + return; + + FilterDirty = true; + SortDirty = true; + } + + protected override void PreDraw() + { + var lastCurrentCollection = _penumbra.CurrentCollection.Id; + if (_lastCurrentCollection != lastCurrentCollection) + { + _lastCurrentCollection = lastCurrentCollection; + FilterDirty = true; + SortDirty = true; + } } public void Dispose() - => _event.Unsubscribe(OnObjectUnlock); + { + _event.Unsubscribe(OnObjectUnlock); + _penumbra.ModSettingChanged -= OnModSettingsChanged; + } - private sealed class FavoriteColumn : YesNoColumn + private sealed class FavoriteColumn : YesNoColumn { public override float Width => ImGui.GetFrameHeightWithSpacing(); @@ -60,8 +86,9 @@ public class UnlockTable : Table, IDisposable public FavoriteColumn(FavoriteManager favorites, ObjectUnlocked hackEvent) { - _favorites = favorites; - _hackEvent = hackEvent; + _favorites = favorites; + _hackEvent = hackEvent; + Flags |= ImGuiTableColumnFlags.NoResize; } protected override bool GetValue(EquipItem item) @@ -81,6 +108,66 @@ public class UnlockTable : Table, IDisposable => _favorites.Contains(rhs).CompareTo(_favorites.Contains(lhs)); } + private sealed class ModdedColumn : YesNoColumn + { + public override float Width + => ImGui.GetFrameHeightWithSpacing(); + + private readonly PenumbraService _penumbra; + private readonly Dictionary _compareCache = []; + + public ModdedColumn(PenumbraService penumbra) + { + _penumbra = penumbra; + Flags |= ImGuiTableColumnFlags.NoResize; + } + + public override void PostSort() + { + _compareCache.Clear(); + } + + public override void DrawColumn(EquipItem item, int idx) + { + var value = _penumbra.CheckCurrentChangedItem(item.Name); + if (value.Length == 0) + return; + + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ModdedItemMarker.Value()); + ImGuiUtil.Center(FontAwesomeIcon.Circle.ToIconString()); + } + + if (ImGui.IsItemHovered()) + { + using var tt = ImUtf8.Tooltip(); + foreach (var (_, mod) in value) + ImUtf8.BulletText(mod); + } + } + + public override bool FilterFunc(EquipItem item) + => FilterValue.HasFlag(_penumbra.CheckCurrentChangedItem(item.Name).Length > 0 ? YesNoFlag.Yes : YesNoFlag.No); + + public override int Compare(EquipItem lhs, EquipItem rhs) + { + if (!_compareCache.TryGetValue(lhs.Id, out var lhsCount)) + { + lhsCount = _penumbra.CheckCurrentChangedItem(lhs.Name).Length; + _compareCache[lhs.Id] = lhsCount; + } + + if (!_compareCache.TryGetValue(rhs.Id, out var rhsCount)) + { + rhsCount = _penumbra.CheckCurrentChangedItem(rhs.Name).Length; + _compareCache[rhs.Id] = rhsCount; + } + + return lhsCount.CompareTo(rhsCount); + } + } + private sealed class NameColumn : ColumnString { private readonly TextureService _textures; @@ -107,7 +194,7 @@ public class UnlockTable : Table, IDisposable ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); ImGui.SameLine(); ImGui.AlignTextToFramePadding(); - if (ImGui.Selectable(item.Name)) + if (ImGui.Selectable(item.Name) && !item.Id.IsBonusItem) Glamourer.Messager.Chat.Print(new SeStringBuilder().AddItemLink(item.ItemId.Id, false).BuiltString); if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && _tooltip.Player(out var state)) @@ -145,8 +232,9 @@ public class UnlockTable : Table, IDisposable public SlotColumn() { - AllFlags = Values.Aggregate((a, b) => a | b); - _filterValue = AllFlags; + Flags &= ~ImGuiTableColumnFlags.NoResize; + AllFlags = Values.Aggregate((a, b) => a | b); + _filterValue = AllFlags; } public override void DrawColumn(EquipItem item, int idx) @@ -225,7 +313,10 @@ public class UnlockTable : Table, IDisposable => 110 * ImGuiHelpers.GlobalScale; public UnlockDateColumn(ItemUnlockManager unlocks) - => _unlocks = unlocks; + { + _unlocks = unlocks; + Flags &= ~ImGuiTableColumnFlags.NoResize; + } public override void DrawColumn(EquipItem item, int idx) { @@ -245,22 +336,17 @@ public class UnlockTable : Table, IDisposable } } - private sealed class ItemIdColumn : ColumnString + private sealed class ItemIdColumn : ColumnNumber { public override float Width => 70 * ImGuiHelpers.GlobalScale; - public override int Compare(EquipItem lhs, EquipItem rhs) - => lhs.ItemId.Id.CompareTo(rhs.ItemId.Id); + public override int ToValue(EquipItem item) + => (int)item.Id.Id; - public override string ToName(EquipItem item) - => item.ItemId.ToString(); - - public override void DrawColumn(EquipItem item, int _) - { - ImGui.AlignTextToFramePadding(); - ImGuiUtil.RightAlign(item.ItemId.ToString()); - } + public ItemIdColumn() + : base(ComparisonMethod.Equal) + { } } private sealed class ModelDataColumn : ColumnString @@ -279,7 +365,7 @@ public class UnlockTable : Table, IDisposable ImGuiUtil.RightAlign(item.ModelString); if (ImGui.IsItemHovered() && item.Type.ValidOffhand().IsOffhandType() - && _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) + && _items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) { using var tt = ImRaii.Tooltip(); ImGui.TextUnformatted("Offhand: " + offhand.ModelString); @@ -287,7 +373,7 @@ public class UnlockTable : Table, IDisposable } public override int Compare(EquipItem lhs, EquipItem rhs) - => lhs.Weapon().Value.CompareTo(rhs.Weapon().Value); + => lhs.Weapon().CompareTo(rhs.Weapon()); public override bool FilterFunc(EquipItem item) { @@ -298,7 +384,7 @@ public class UnlockTable : Table, IDisposable return true; if (item.Type.ValidOffhand().IsOffhandType() - && _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) + && _items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) return FilterRegex?.IsMatch(offhand.ModelString) ?? offhand.ModelString.Contains(FilterValue, StringComparison.OrdinalIgnoreCase); @@ -306,7 +392,7 @@ public class UnlockTable : Table, IDisposable } } - private sealed class LevelColumn : ColumnString + private sealed class RequiredLevelColumn : ColumnNumber { public override float Width => 70 * ImGuiHelpers.GlobalScale; @@ -314,14 +400,14 @@ public class UnlockTable : Table, IDisposable public override string ToName(EquipItem item) => item.Level.ToString(); - public override void DrawColumn(EquipItem item, int _) - => ImGuiUtil.RightAlign(item.Level.Value.ToString()); + public override int ToValue(EquipItem item) + => item.Level.Value; - public override int Compare(EquipItem lhs, EquipItem rhs) - => lhs.Level.Value.CompareTo(rhs.Level.Value); + public RequiredLevelColumn() + : base(ComparisonMethod.LessEqual) + { } } - private sealed class JobColumn : ColumnFlags { public override float Width @@ -338,11 +424,47 @@ public class UnlockTable : Table, IDisposable public JobColumn(JobService jobs) { - _jobs = jobs; - _values = _jobs.Jobs.Values.Skip(1).Select(j => j.Flag).ToArray(); - _names = _jobs.Jobs.Values.Skip(1).Select(j => j.Abbreviation).ToArray(); - AllFlags = _values.Aggregate((l, r) => l | r); - _filterValue = AllFlags; + _jobs = jobs; + _values = _jobs.Jobs.Ordered.Select(j => j.Flag).ToArray(); + _names = _jobs.Jobs.Ordered.Select(j => j.Abbreviation).ToArray(); + AllFlags = _values.Aggregate((l, r) => l | r); + _filterValue = AllFlags; + Flags &= ~ImGuiTableColumnFlags.NoResize; + ComboFlags |= ImGuiComboFlags.HeightLargest; + } + + protected override bool DrawCheckbox(int idx, out bool ret) + { + var job = _jobs.Jobs.Ordered[idx]; + var color = job.Role switch + { + Job.JobRole.Tank => 0xFFFFD0D0, + Job.JobRole.Melee => 0xFFD0D0FF, + Job.JobRole.RangedPhysical => 0xFFD0FFFF, + Job.JobRole.RangedMagical => 0xFFFFD0FF, + Job.JobRole.Healer => 0xFFD0FFD0, + Job.JobRole.Crafter => 0xFF808080, + Job.JobRole.Gatherer => 0xFFD0D0D0, + _ => ImGui.GetColorU32(ImGuiCol.Text), + }; + bool r; + using (ImRaii.PushColor(ImGuiCol.Text, color)) + { + r = base.DrawCheckbox(idx, out ret); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _filterValue = job.Flag & _filterValue; + ret = true; + r = true; + } + + ImUtf8.HoverTooltip("Right-Click to disable all other jobs."u8); + + if (idx < _names.Length - 1 && idx % 2 == 0) + ImGui.SameLine(ImGui.GetFrameHeight() * 4); + return r; } protected override void SetValue(JobFlag value, bool enable) @@ -383,66 +505,70 @@ public class UnlockTable : Table, IDisposable } } - [Flags] - private enum YesNoFlag + private sealed class DyableColumn : ColumnFlags { - Yes = 0x01, - No = 0x02, - }; - - private class YesNoColumn : ColumnFlags - { - public string Tooltip = string.Empty; - - private YesNoFlag _filterValue; - - public override YesNoFlag FilterValue - => _filterValue; - - public YesNoColumn() + [Flags] + public enum Dyable : byte { - AllFlags = YesNoFlag.Yes | YesNoFlag.No; - _filterValue = AllFlags; + No = 1, + Yes = 2, + Two = 4, } - protected override void SetValue(YesNoFlag value, bool enable) - => _filterValue = enable ? _filterValue | value : _filterValue & ~value; + private Dyable _filterValue; - protected virtual bool GetValue(EquipItem item) - => false; + public DyableColumn() + { + AllFlags = Dyable.No | Dyable.Yes | Dyable.Two; + Flags &= ~ImGuiTableColumnFlags.NoResize; + _filterValue = AllFlags; + } + + public override Dyable FilterValue + => _filterValue; + + protected override void SetValue(Dyable value, bool enable) + => _filterValue = enable ? _filterValue | value : _filterValue & ~value; public override float Width => ImGui.GetFrameHeight() * 2; public override bool FilterFunc(EquipItem item) - => GetValue(item) - ? FilterValue.HasFlag(YesNoFlag.Yes) - : FilterValue.HasFlag(YesNoFlag.No); + => GetValue(item) switch + { + 0 => _filterValue.HasFlag(Dyable.No), + ItemFlags.IsDyable2 => _filterValue.HasFlag(Dyable.Yes), + ItemFlags.IsDyable1 => _filterValue.HasFlag(Dyable.Yes), + _ => _filterValue.HasFlag(Dyable.Two), + }; public override int Compare(EquipItem lhs, EquipItem rhs) => GetValue(lhs).CompareTo(GetValue(rhs)); public override void DrawColumn(EquipItem item, int idx) { - using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + using (ImRaii.PushFont(UiBuilder.IconFont)) { - ImGuiUtil.Center(GetValue(item) ? FontAwesomeIcon.Check.ToIconString() : FontAwesomeIcon.Times.ToIconString()); + ImGuiUtil.Center(Icon(item)); } - ImGuiUtil.HoverTooltip(Tooltip); + ImGuiUtil.HoverTooltip("Whether the item is dyable, and how many slots it has."); } + + private static string Icon(EquipItem item) + => GetValue(item) switch + { + 0 => FontAwesomeIcon.Times.ToIconString(), + ItemFlags.IsDyable2 => FontAwesomeIcon.Check.ToIconString(), + ItemFlags.IsDyable1 => FontAwesomeIcon.Check.ToIconString(), + _ => FontAwesomeIcon.DiceTwo.ToIconString(), + }; + + private static ItemFlags GetValue(EquipItem item) + => item.Flags & (ItemFlags.IsDyable1 | ItemFlags.IsDyable2); } - private sealed class DyableColumn : YesNoColumn - { - public DyableColumn() - => Tooltip = "Whether the item is dyable."; - - protected override bool GetValue(EquipItem item) - => item.Flags.HasFlag(ItemFlags.IsDyable); - } - - private sealed class TradableColumn : YesNoColumn + private sealed class TradableColumn : YesNoColumn { public TradableColumn() => Tooltip = "Whether the item is tradable."; @@ -451,7 +577,7 @@ public class UnlockTable : Table, IDisposable => item.Flags.HasFlag(ItemFlags.IsTradable); } - private sealed class CrestColumn : YesNoColumn + private sealed class CrestColumn : YesNoColumn { public CrestColumn() => Tooltip = "Whether a crest can be applied to the item.."; @@ -460,21 +586,16 @@ public class UnlockTable : Table, IDisposable => item.Flags.HasFlag(ItemFlags.IsCrestWorthy); } - private sealed class ItemList : IReadOnlyCollection + private sealed class ItemList(ItemManager items) : IReadOnlyCollection { - private readonly ItemManager _items; - - public ItemList(ItemManager items) - => _items = items; - public IEnumerator GetEnumerator() - => _items.ItemService.AwaitedService.AllItems(true).Select(i => i.Item2).GetEnumerator(); + => items.ItemData.AllItems(true).Select(i => i.Item2).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public int Count - => _items.ItemService.AwaitedService.TotalItemCount(true); + => items.ItemData.Primary.Count; } private void OnObjectUnlock(ObjectUnlocked.Type _1, uint _2, DateTimeOffset _3) diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs index 75f180f..661b2a4 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlocksTab.cs @@ -1,8 +1,6 @@ -using System; -using System.Numerics; -using Dalamud.Interface; +using Dalamud.Interface; using Dalamud.Interface.Windowing; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui; using OtterGui.Widgets; @@ -11,18 +9,19 @@ namespace Glamourer.Gui.Tabs.UnlocksTab; public class UnlocksTab : Window, ITab { - private readonly Configuration _config; + private readonly EphemeralConfig _config; private readonly UnlockOverview _overview; private readonly UnlockTable _table; - public UnlocksTab(Configuration config, UnlockOverview overview, UnlockTable table) + public UnlocksTab(EphemeralConfig config, UnlockOverview overview, UnlockTable table) : base("Unlocked Equipment") { _config = config; _overview = overview; _table = table; - IsOpen = false; + Flags |= ImGuiWindowFlags.NoDocking; + IsOpen = false; SizeConstraints = new WindowSizeConstraints() { MinimumSize = new Vector2(700, 675), @@ -50,6 +49,7 @@ public class UnlocksTab : Window, ITab _table.Draw(ImGui.GetFrameHeightWithSpacing()); else _overview.Draw(); + _table.Flags |= ImGuiTableFlags.Resizable; } public override void Draw() @@ -64,6 +64,8 @@ public class UnlocksTab : Window, ITab var buttonSize = new Vector2(ImGui.GetContentRegionAvail().X / 2, ImGui.GetFrameHeight()); if (!IsOpen) buttonSize.X -= ImGui.GetFrameHeight() / 2; + if (DetailMode) + buttonSize.X -= ImGui.GetFrameHeight() / 2; if (ImGuiUtil.DrawDisabledButton("Overview Mode", buttonSize, "Show tinted icons of sets of unlocks.", !DetailMode)) DetailMode = false; @@ -72,6 +74,15 @@ public class UnlocksTab : Window, ITab if (ImGuiUtil.DrawDisabledButton("Detailed Mode", buttonSize, "Show all unlockable data as a combined filterable and sortable table.", DetailMode)) DetailMode = true; + + if (DetailMode) + { + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Expand.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Restore all columns to their original size.", false, true)) + _table.Flags &= ~ImGuiTableFlags.Resizable; + } + if (!IsOpen) { ImGui.SameLine(); diff --git a/Glamourer/Gui/ToggleDrawData.cs b/Glamourer/Gui/ToggleDrawData.cs new file mode 100644 index 0000000..28afc2c --- /dev/null +++ b/Glamourer/Gui/ToggleDrawData.cs @@ -0,0 +1,116 @@ +using Glamourer.Api.Enums; +using Glamourer.Designs; +using Glamourer.State; +using Penumbra.GameData.Enums; + +namespace Glamourer.Gui; + +public struct ToggleDrawData +{ + private IDesignEditor _editor = null!; + private object _data = null!; + private StateIndex _index; + + public bool Locked; + public bool DisplayApplication; + + public bool CurrentValue; + public bool CurrentApply; + + public string Label = string.Empty; + public string Tooltip = string.Empty; + + + public ToggleDrawData() + { } + + public readonly void SetValue(bool value) + { + switch (_index.GetFlag()) + { + case MetaFlag flag: + _editor.ChangeMetaState(_data, flag.ToIndex(), value, ApplySettings.Manual); + break; + case CrestFlag flag: + _editor.ChangeCrest(_data, flag, value, ApplySettings.Manual); + break; + } + } + + public readonly void SetApply(bool value) + { + var manager = (DesignManager)_editor; + var design = (Design)_data; + switch (_index.GetFlag()) + { + case MetaFlag flag: + manager.ChangeApplyMeta(design, flag.ToIndex(), value); + break; + case CrestFlag flag: + manager.ChangeApplyCrest(design, flag, value); + break; + } + } + + public static ToggleDrawData FromDesign(MetaIndex index, DesignManager manager, Design design) + => new() + { + _index = index, + _editor = manager, + _data = design, + Label = index.ToName(), + Tooltip = string.Empty, + Locked = design.WriteProtected(), + DisplayApplication = true, + CurrentValue = design.DesignData.GetMeta(index), + CurrentApply = design.DoApplyMeta(index), + }; + + public static ToggleDrawData FromState(MetaIndex index, StateManager manager, ActorState state) + => new() + { + _index = index, + _editor = manager, + _data = state, + Label = index.ToName(), + Tooltip = index.ToTooltip(), + Locked = state.IsLocked, + CurrentValue = state.ModelData.GetMeta(index), + }; + + public static ToggleDrawData CrestFromDesign(CrestFlag slot, DesignManager manager, Design design) + => new() + { + _index = slot, + _editor = manager, + _data = design, + Label = $"{slot.ToLabel()} Crest", + Tooltip = string.Empty, + Locked = design.WriteProtected(), + DisplayApplication = true, + CurrentValue = design.DesignData.Crest(slot), + CurrentApply = design.DoApplyCrest(slot), + }; + + public static ToggleDrawData CrestFromState(CrestFlag slot, StateManager manager, ActorState state) + => new() + { + _index = slot, + _editor = manager, + _data = state, + Label = $"{slot.ToLabel()} Crest", + Tooltip = "Hide or show your free company crest on this piece of gear.", + Locked = state.IsLocked, + CurrentValue = state.ModelData.Crest(slot), + }; + + public static ToggleDrawData FromValue(MetaIndex index, bool value) + => new() + { + _index = index, + Label = index.ToName(), + Tooltip = index.ToTooltip(), + Locked = true, + CurrentValue = value, + }; +} diff --git a/Glamourer/Gui/UiHelpers.cs b/Glamourer/Gui/UiHelpers.cs index 3bb4c3a..94ddb06 100644 --- a/Glamourer/Gui/UiHelpers.cs +++ b/Glamourer/Gui/UiHelpers.cs @@ -1,34 +1,17 @@ -using System; -using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Utility; -using Glamourer.Customization; using Glamourer.Services; -using Glamourer.Structs; using Glamourer.Unlocks; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Lumina.Misc; using OtterGui; using OtterGui.Raii; +using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; namespace Glamourer.Gui; -[Flags] -public enum DataChange : byte -{ - None = 0x00, - Item = 0x01, - Stain = 0x02, - ApplyItem = 0x04, - ApplyStain = 0x08, - Item2 = 0x10, - Stain2 = 0x20, - ApplyItem2 = 0x40, - ApplyStain2 = 0x80, -} - public static class UiHelpers { /// Open a combo popup with another method than the combo itself. @@ -41,13 +24,35 @@ public static class UiHelpers public static void DrawIcon(this EquipItem item, TextureService textures, Vector2 size, EquipSlot slot) { - var isEmpty = item.ModelId.Id == 0; + var isEmpty = item.PrimaryId.Id == 0; var (ptr, textureSize, empty) = textures.GetIcon(item, slot); if (empty) { var (bgColor, tint) = isEmpty - ? (ImGui.GetColorU32(ImGuiCol.FrameBg), new Vector4(0.1f, 0.1f, 0.1f, 0.5f)) - : (ImGui.GetColorU32(ImGuiCol.FrameBgActive), new Vector4(0.3f, 0.3f, 0.3f, 0.8f)); + ? (ImGui.GetColorU32(ImGuiCol.FrameBg), Vector4.One) + : (ImGui.GetColorU32(ImGuiCol.FrameBgActive), new Vector4(0.3f, 0.3f, 0.3f, 1f)); + var pos = ImGui.GetCursorScreenPos(); + ImGui.GetWindowDrawList().AddRectFilled(pos, pos + size, bgColor, 5 * ImGuiHelpers.GlobalScale); + if (ptr != nint.Zero) + ImGui.Image(ptr, size, Vector2.Zero, Vector2.One, tint); + else + ImGui.Dummy(size); + } + else + { + ImGuiUtil.HoverIcon(ptr, textureSize, size); + } + } + + public static void DrawIcon(this EquipItem item, TextureService textures, Vector2 size, BonusItemFlag slot) + { + var isEmpty = item.PrimaryId.Id == 0; + var (ptr, textureSize, empty) = textures.GetIcon(item, slot); + if (empty) + { + var (bgColor, tint) = isEmpty + ? (ImGui.GetColorU32(ImGuiCol.FrameBg), Vector4.One) + : (ImGui.GetColorU32(ImGuiCol.FrameBgActive), new Vector4(0.3f, 0.3f, 0.3f, 1f)); var pos = ImGui.GetCursorScreenPos(); ImGui.GetWindowDrawList().AddRectFilled(pos, pos + size, bgColor, 5 * ImGuiHelpers.GlobalScale); if (ptr != nint.Zero) @@ -63,56 +68,57 @@ public static class UiHelpers public static bool DrawCheckbox(string label, string tooltip, bool value, out bool on, bool locked) { - using var disabled = ImRaii.Disabled(locked); - var ret = ImGuiUtil.Checkbox(label, string.Empty, value, v => value = v); - ImGuiUtil.HoverTooltip(tooltip); + var startsWithHash = label.StartsWith("##"); + bool ret; + using (_ = ImRaii.Disabled(locked)) + { + ret = ImGuiUtil.Checkbox(startsWithHash ? label : "##" + label, string.Empty, value, v => value = v); + } + + if (!startsWithHash) + { + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(label); + } + + ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); on = value; return ret; } - public static DataChange DrawMetaToggle(string label, bool currentValue, bool currentApply, out bool newValue, - out bool newApply, - bool locked) + public static (bool, bool) DrawMetaToggle(string label, bool currentValue, bool currentApply, out bool newValue, + out bool newApply, bool locked) { - var flags = currentApply ? currentValue ? 3 : 0 : 2; - bool ret; - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); - using (var disabled = ImRaii.Disabled(locked)) + var flags = (sbyte)(currentApply ? currentValue ? 1 : -1 : 0); + using (_ = ImRaii.Disabled(locked)) { - ret = ImGui.CheckboxFlags("##" + label, ref flags, 3); + if (new TristateCheckbox(ColorId.TriStateCross.Value(), ColorId.TriStateCheck.Value(), ColorId.TriStateNeutral.Value()).Draw( + "##" + label, flags, out flags)) + { + (newValue, newApply) = flags switch + { + -1 => (false, true), + 0 => (true, false), + _ => (true, true), + }; + } + else + { + newValue = currentValue; + newApply = currentApply; + } } ImGuiUtil.HoverTooltip($"This attribute will be {(currentApply ? currentValue ? "enabled." : "disabled." : "kept as is.")}"); - ImGui.SameLine(); + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(label); - if (ret) - { - (newValue, newApply, var change) = (currentValue, currentApply) switch - { - (false, false) => (false, true, DataChange.ApplyItem), - (false, true) => (true, true, DataChange.Item), - (true, false) => (false, false, DataChange.Item), // Should not happen - (true, true) => (false, false, DataChange.Item | DataChange.ApplyItem), - }; - return change; - } - - newValue = currentValue; - newApply = currentApply; - return DataChange.None; + return (currentValue != newValue, currentApply != newApply); } - public static (EquipFlag, CustomizeFlag) ConvertKeysToFlags() - => (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch - { - (false, false) => (EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant), - (true, true) => (EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant), - (true, false) => (EquipFlagExtensions.All, (CustomizeFlag)0), - (false, true) => ((EquipFlag)0, CustomizeFlagExtensions.AllRelevant), - }; - public static (bool, bool) ConvertKeysToBool() => (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch { @@ -132,15 +138,36 @@ public static class UiHelpers using var c = ImRaii.PushColor(ImGuiCol.Text, hovering ? ColorId.FavoriteStarHovered.Value() : favorite ? ColorId.FavoriteStarOn.Value() : ColorId.FavoriteStarOff.Value()); ImGui.TextUnformatted(FontAwesomeIcon.Star.ToIconString()); - if (ImGui.IsItemClicked()) - { - if (favorite) - favorites.Remove(item); - else - favorites.TryAdd(item); - return true; - } + if (!ImGui.IsItemClicked()) + return false; + + if (favorite) + favorites.Remove(item); + else + favorites.TryAdd(item); + return true; - return false; } -} + + public static bool DrawFavoriteStar(FavoriteManager favorites, StainId stain) + { + var favorite = favorites.Contains(stain); + var hovering = ImGui.IsMouseHoveringRect(ImGui.GetCursorScreenPos(), + ImGui.GetCursorScreenPos() + new Vector2(ImGui.GetFrameHeight())); + + using var font = ImRaii.PushFont(UiBuilder.IconFont); + using var c = ImRaii.PushColor(ImGuiCol.Text, + hovering ? ColorId.FavoriteStarHovered.Value() : favorite ? ColorId.FavoriteStarOn.Value() : ColorId.FavoriteStarOff.Value()); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(FontAwesomeIcon.Star.ToIconString()); + if (!ImGui.IsItemClicked()) + return false; + + if (favorite) + favorites.Remove(stain); + else + favorites.TryAdd(stain); + return true; + + } +} \ No newline at end of file diff --git a/Glamourer/Interop/ChangeCustomizeService.cs b/Glamourer/Interop/ChangeCustomizeService.cs index 0b6f2fb..495d69c 100644 --- a/Glamourer/Interop/ChangeCustomizeService.cs +++ b/Glamourer/Interop/ChangeCustomizeService.cs @@ -1,14 +1,10 @@ -using System; -using System.Threading; -using Dalamud.Hooking; +using Dalamud.Hooking; using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Glamourer.Customization; using Glamourer.Events; -using Glamourer.Interop.Structs; using OtterGui.Classes; -using CustomizeData = Penumbra.GameData.Structs.CustomizeData; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; namespace Glamourer.Interop; @@ -17,13 +13,16 @@ namespace Glamourer.Interop; /// Changes in Race, body type or Gender are probably ignored. /// This operates on draw objects, not game objects. /// -public unsafe class ChangeCustomizeService : EventWrapper>, ChangeCustomizeService.Priority> +public unsafe class ChangeCustomizeService : EventWrapperRef2 { - private readonly PenumbraReloaded _penumbraReloaded; - private readonly IGameInteropProvider _interop; + private readonly PenumbraReloaded _penumbraReloaded; + private readonly IGameInteropProvider _interop; + private readonly delegate* unmanaged _original; + private readonly Post _postEvent = new(); + /// Check whether we in a manual customize update, in which case we need to not toggle certain flags. - public static readonly ThreadLocal InUpdate = new(() => false); + public static readonly InMethodChecker InUpdate = new(); public enum Priority { @@ -37,6 +36,8 @@ public unsafe class ChangeCustomizeService : EventWrapper _changeCustomizeHook; - public bool UpdateCustomize(Model model, CustomizeData customize) + public bool UpdateCustomize(Model model, CustomizeArray customize) { if (!model.IsHuman) return false; Glamourer.Log.Verbose($"[ChangeCustomize] Invoked on 0x{model.Address:X} with {customize}."); - InUpdate.Value = true; - var ret = _changeCustomizeHook.Original(model.AsHuman, customize.Data, 1); - InUpdate.Value = false; + using var _ = InUpdate.EnterMethod(); + var ret = _original(model.AsHuman, customize.Data, true); return ret; } - public bool UpdateCustomize(Actor actor, CustomizeData customize) + public bool UpdateCustomize(Actor actor, CustomizeArray customize) => UpdateCustomize(actor.Model, customize); private bool ChangeCustomizeDetour(Human* human, byte* data, byte skipEquipment) { - var customize = new Ref(new Customize(*(CustomizeData*)data)); - Invoke(this, (Model)human, customize); - ((Customize*)data)->Load(customize.Value); - return _changeCustomizeHook.Original(human, data, skipEquipment); + if (!InUpdate.InMethod) + Invoke(human, ref *(CustomizeArray*)data); + + var ret = _changeCustomizeHook.Original(human, data, skipEquipment); + _postEvent.Invoke(human); + return ret; + } + + public void Subscribe(Action action, Post.Priority priority) + => _postEvent.Subscribe(action, priority); + + public void Unsubscribe(Action action) + => _postEvent.Unsubscribe(action); + + public sealed class Post() : EventWrapper(nameof(ChangeCustomizeService) + '.' + nameof(Post)) + { + public enum Priority + { + /// + StateListener = 0, + } } } diff --git a/Glamourer/Interop/CharaFile/CharaFile.cs b/Glamourer/Interop/CharaFile/CharaFile.cs new file mode 100644 index 0000000..aabac2d --- /dev/null +++ b/Glamourer/Interop/CharaFile/CharaFile.cs @@ -0,0 +1,313 @@ +using Glamourer.Designs; +using Glamourer.Services; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Race = Penumbra.GameData.Enums.Race; + +namespace Glamourer.Interop.CharaFile; + +public sealed class CharaFile +{ + public string Name = string.Empty; + public DesignData Data = new(); + public CustomizeFlag ApplyCustomize; + public EquipFlag ApplyEquip; + public BonusItemFlag ApplyBonus; + + public static CharaFile ParseData(ItemManager items, string data, string? name = null) + { + var jObj = JObject.Parse(data); + SanityCheck(jObj); + var ret = new CharaFile(); + ret.Data.SetDefaultEquipment(items); + ret.Data.ModelId = ParseModelId(jObj); + ret.Name = jObj["Nickname"]?.ToObject() ?? name ?? "New Design"; + ret.ApplyCustomize = ParseCustomize(jObj, ref ret.Data.Customize); + ret.ApplyEquip = ParseEquipment(items, jObj, ref ret.Data); + ret.ApplyBonus = ParseBonusItems(items, jObj, ref ret.Data); + return ret; + } + + private static EquipFlag ParseEquipment(ItemManager items, JObject jObj, ref DesignData data) + { + EquipFlag ret = 0; + ParseWeapon(items, jObj, "MainHand", EquipSlot.MainHand, ref data, ref ret); + ParseWeapon(items, jObj, "OffHand", EquipSlot.OffHand, ref data, ref ret); + ParseGear(items, jObj, "HeadGear", EquipSlot.Head, ref data, ref ret); + ParseGear(items, jObj, "Body", EquipSlot.Body, ref data, ref ret); + ParseGear(items, jObj, "Hands", EquipSlot.Hands, ref data, ref ret); + ParseGear(items, jObj, "Legs", EquipSlot.Legs, ref data, ref ret); + ParseGear(items, jObj, "Feet", EquipSlot.Feet, ref data, ref ret); + ParseGear(items, jObj, "Ears", EquipSlot.Ears, ref data, ref ret); + ParseGear(items, jObj, "Neck", EquipSlot.Neck, ref data, ref ret); + ParseGear(items, jObj, "Wrists", EquipSlot.Wrists, ref data, ref ret); + ParseGear(items, jObj, "LeftRing", EquipSlot.LFinger, ref data, ref ret); + ParseGear(items, jObj, "RightRing", EquipSlot.RFinger, ref data, ref ret); + return ret; + } + + private static BonusItemFlag ParseBonusItems(ItemManager items, JObject jObj, ref DesignData data) + { + BonusItemFlag ret = 0; + ParseBonus(items, jObj, "Glasses", "GlassesId", BonusItemFlag.Glasses, ref data, ref ret); + return ret; + } + + private static void ParseWeapon(ItemManager items, JObject jObj, string property, EquipSlot slot, ref DesignData data, ref EquipFlag flags) + { + var jTok = jObj[property]; + if (jTok == null) + return; + + var set = jTok["ModelSet"]?.ToObject() ?? 0; + var type = jTok["ModelBase"]?.ToObject() ?? 0; + var variant = jTok["ModelVariant"]?.ToObject() ?? 0; + var dye = jTok["DyeId"]?.ToObject() ?? 0; + var item = items.Identify(slot, set, type, variant, slot is EquipSlot.OffHand ? data.MainhandType : FullEquipType.Unknown); + if (!item.Valid) + return; + + data.SetItem(slot, item); + data.SetStain(slot, new StainIds(dye)); + if (slot is EquipSlot.MainHand) + data.SetItem(EquipSlot.OffHand, items.GetDefaultOffhand(item)); + flags |= slot.ToFlag(); + flags |= slot.ToStainFlag(); + } + + private static void ParseGear(ItemManager items, JObject jObj, string property, EquipSlot slot, ref DesignData data, ref EquipFlag flags) + { + var jTok = jObj[property]; + if (jTok == null) + return; + + var set = jTok["ModelBase"]?.ToObject() ?? 0; + var variant = jTok["ModelVariant"]?.ToObject() ?? 0; + var dye = jTok["DyeId"]?.ToObject() ?? 0; + var item = items.Identify(slot, set, variant); + if (!item.Valid) + return; + + data.SetItem(slot, item); + data.SetStain(slot, new StainIds(dye)); + flags |= slot.ToFlag(); + flags |= slot.ToStainFlag(); + } + + private static void ParseBonus(ItemManager items, JObject jObj, string property, string subProperty, BonusItemFlag slot, + ref DesignData data, ref BonusItemFlag flags) + { + var id = jObj[property]?[subProperty]?.ToObject(); + if (id is null) + return; + + if (id is 0) + { + data.SetBonusItem(slot, EquipItem.BonusItemNothing(slot)); + flags |= slot; + } + + if (!items.DictBonusItems.TryGetValue((BonusItemId)id.Value, out var item) || item.Type.ToBonus() != slot) + return; + + data.SetBonusItem(slot, item); + flags |= slot; + } + + private static CustomizeFlag ParseCustomize(JObject jObj, ref CustomizeArray customize) + { + CustomizeFlag ret = 0; + customize.Race = ParseRace(jObj, ref ret); + customize.Gender = ParseGender(jObj, ref ret); + customize.Clan = ParseTribe(jObj, ref ret); + ParseByte(jObj, "Height", CustomizeIndex.Height, ref customize, ref ret); + ParseByte(jObj, "Head", CustomizeIndex.Face, ref customize, ref ret); + ParseByte(jObj, "Hair", CustomizeIndex.Hairstyle, ref customize, ref ret); + ParseHighlights(jObj, ref customize, ref ret); + ParseByte(jObj, "Skintone", CustomizeIndex.SkinColor, ref customize, ref ret); + ParseByte(jObj, "REyeColor", CustomizeIndex.EyeColorRight, ref customize, ref ret); + ParseByte(jObj, "HairTone", CustomizeIndex.HairColor, ref customize, ref ret); + ParseByte(jObj, "Highlights", CustomizeIndex.HighlightsColor, ref customize, ref ret); + ParseFacial(jObj, ref customize, ref ret); + ParseByte(jObj, "LimbalEyes", CustomizeIndex.TattooColor, ref customize, ref ret); + ParseByte(jObj, "Eyebrows", CustomizeIndex.Eyebrows, ref customize, ref ret); + ParseByte(jObj, "LEyeColor", CustomizeIndex.EyeColorLeft, ref customize, ref ret); + ParseByte(jObj, "Eyes", CustomizeIndex.EyeShape, ref customize, ref ret); + ParseByte(jObj, "Nose", CustomizeIndex.Nose, ref customize, ref ret); + ParseByte(jObj, "Jaw", CustomizeIndex.Jaw, ref customize, ref ret); + ParseByte(jObj, "Mouth", CustomizeIndex.Mouth, ref customize, ref ret); + ParseByte(jObj, "LipsToneFurPattern", CustomizeIndex.LipColor, ref customize, ref ret); + ParseByte(jObj, "EarMuscleTailSize", CustomizeIndex.MuscleMass, ref customize, ref ret); + ParseByte(jObj, "TailEarsType", CustomizeIndex.TailShape, ref customize, ref ret); + ParseByte(jObj, "Bust", CustomizeIndex.BustSize, ref customize, ref ret); + ParseByte(jObj, "FacePaint", CustomizeIndex.FacePaint, ref customize, ref ret); + ParseByte(jObj, "FacePaintColor", CustomizeIndex.FacePaintColor, ref customize, ref ret); + ParseAge(jObj); + + if (ret.HasFlag(CustomizeFlag.EyeShape)) + ret |= CustomizeFlag.SmallIris; + + if (ret.HasFlag(CustomizeFlag.Mouth)) + ret |= CustomizeFlag.Lipstick; + + if (ret.HasFlag(CustomizeFlag.FacePaint)) + ret |= CustomizeFlag.FacePaintReversed; + + return ret; + } + + private static uint ParseModelId(JObject jObj) + { + var jTok = jObj["ModelType"]; + if (jTok == null) + throw new Exception("No Model ID given."); + + var id = jTok.ToObject(); + if (id != 0) + throw new Exception($"Model ID {id} != 0 not supported."); + + return id; + } + + private static void ParseFacial(JObject jObj, ref CustomizeArray customize, ref CustomizeFlag application) + { + var jTok = jObj["FacialFeatures"]; + if (jTok == null) + return; + + application |= CustomizeFlag.FacialFeature1 + | CustomizeFlag.FacialFeature2 + | CustomizeFlag.FacialFeature3 + | CustomizeFlag.FacialFeature4 + | CustomizeFlag.FacialFeature5 + | CustomizeFlag.FacialFeature6 + | CustomizeFlag.FacialFeature7 + | CustomizeFlag.LegacyTattoo; + + var value = jTok.ToObject()!; + if (value is "None") + return; + + if (value.Contains("First")) + customize[CustomizeIndex.FacialFeature1] = CustomizeValue.Max; + if (value.Contains("Second")) + customize[CustomizeIndex.FacialFeature2] = CustomizeValue.Max; + if (value.Contains("Third")) + customize[CustomizeIndex.FacialFeature3] = CustomizeValue.Max; + if (value.Contains("Fourth")) + customize[CustomizeIndex.FacialFeature4] = CustomizeValue.Max; + if (value.Contains("Fifth")) + customize[CustomizeIndex.FacialFeature5] = CustomizeValue.Max; + if (value.Contains("Sixth")) + customize[CustomizeIndex.FacialFeature6] = CustomizeValue.Max; + if (value.Contains("Seventh")) + customize[CustomizeIndex.FacialFeature7] = CustomizeValue.Max; + if (value.Contains("LegacyTattoo")) + customize[CustomizeIndex.LegacyTattoo] = CustomizeValue.Max; + } + + private static void ParseHighlights(JObject jObj, ref CustomizeArray customize, ref CustomizeFlag application) + { + var jTok = jObj["EnableHighlights"]; + if (jTok == null) + return; + + var value = jTok.ToObject(); + application |= CustomizeFlag.Highlights; + customize[CustomizeIndex.Highlights] = value ? CustomizeValue.Max : CustomizeValue.Zero; + } + + private static Race ParseRace(JObject jObj, ref CustomizeFlag application) + { + var race = jObj["Race"]?.ToObject() switch + { + null => Race.Unknown, + "Hyur" => Race.Hyur, + "Elezen" => Race.Elezen, + "Lalafel" => Race.Lalafell, + "Miqote" => Race.Miqote, + "Roegadyn" => Race.Roegadyn, + "AuRa" => Race.AuRa, + "Hrothgar" => Race.Hrothgar, + "Viera" => Race.Viera, + _ => throw new Exception($"Invalid Race value {jObj["Race"]?.ToObject()}."), + }; + if (race == Race.Unknown) + return Race.Hyur; + + application |= CustomizeFlag.Race; + return race; + } + + private static Gender ParseGender(JObject jObj, ref CustomizeFlag application) + { + var gender = jObj["Gender"]?.ToObject() switch + { + null => Gender.Unknown, + "Masculine" => Gender.Male, + "Feminine" => Gender.Female, + _ => throw new Exception($"Invalid Gender value {jObj["Gender"]?.ToObject()}."), + }; + if (gender == Gender.Unknown) + return Gender.Male; + + application |= CustomizeFlag.Gender; + return gender; + } + + private static void ParseAge(JObject jObj) + { + var age = jObj["Age"]?.ToObject(); + if (age is not null and not "Normal") + throw new Exception($"Age {age} != Normal is not supported."); + } + + private static unsafe void ParseByte(JObject jObj, string property, CustomizeIndex idx, ref CustomizeArray customize, + ref CustomizeFlag application) + { + var jTok = jObj[property]; + if (jTok == null) + return; + + customize.Data[idx.ToByteAndMask().ByteIdx] = jTok.ToObject(); + application |= idx.ToFlag(); + } + + private static SubRace ParseTribe(JObject jObj, ref CustomizeFlag application) + { + var tribe = jObj["Tribe"]?.ToObject() switch + { + null => SubRace.Unknown, + "Midlander" => SubRace.Midlander, + "Highlander" => SubRace.Highlander, + "Wildwood" => SubRace.Wildwood, + "Duskwight" => SubRace.Duskwight, + "Plainsfolk" => SubRace.Plainsfolk, + "Dunesfolk" => SubRace.Dunesfolk, + "SeekerOfTheSun" => SubRace.SeekerOfTheSun, + "KeeperOfTheMoon" => SubRace.KeeperOfTheMoon, + "SeaWolf" => SubRace.Seawolf, + "Hellsguard" => SubRace.Hellsguard, + "Raen" => SubRace.Raen, + "Xaela" => SubRace.Xaela, + "Helions" => SubRace.Helion, + "TheLost" => SubRace.Lost, + "Rava" => SubRace.Rava, + "Veena" => SubRace.Veena, + _ => throw new Exception($"Invalid Tribe value {jObj["Tribe"]?.ToObject()}."), + }; + if (tribe == SubRace.Unknown) + return SubRace.Midlander; + + application |= CustomizeFlag.Clan; + return tribe; + } + + private static void SanityCheck(JObject jObj) + { + var type = jObj["ObjectKind"]?.ToObject(); + if (type is not "Player") + throw new Exception($"ObjectKind {type} != Player is not supported."); + } +} diff --git a/Glamourer/Interop/CharaFile/CmaFile.cs b/Glamourer/Interop/CharaFile/CmaFile.cs new file mode 100644 index 0000000..2e06588 --- /dev/null +++ b/Glamourer/Interop/CharaFile/CmaFile.cs @@ -0,0 +1,111 @@ +using Glamourer.Designs; +using Glamourer.Services; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop.CharaFile; + +public sealed class CmaFile +{ + public string Name = string.Empty; + public DesignData Data = new(); + + public static CmaFile? ParseData(ItemManager items, string data, string? name = null) + { + try + { + var jObj = JObject.Parse(data); + var ret = new CmaFile(); + ret.Data.SetDefaultEquipment(items); + ParseMainHand(items, jObj, ref ret.Data); + ParseOffHand(items, jObj, ref ret.Data); + ret.Name = jObj["Description"]?.ToObject() ?? name ?? "New Design"; + ParseEquipment(items, jObj, ref ret.Data); + ParseCustomization(jObj, ref ret.Data); + return ret; + } + catch + { + return null; + } + } + + private static unsafe void ParseCustomization(JObject jObj, ref DesignData data) + { + var bytes = jObj["CharacterBytes"]?.ToObject() ?? string.Empty; + if (bytes.Length is not 26 * 3 - 1) + return; + + bytes = bytes.Replace(" ", string.Empty); + var byteData = Convert.FromHexString(bytes); + fixed (byte* ptr = byteData) + { + data.Customize.Read(ptr); + } + } + + private static unsafe void ParseEquipment(ItemManager items, JObject jObj, ref DesignData data) + { + var bytes = jObj["EquipmentBytes"]?.ToObject() ?? string.Empty; + bytes = bytes.Replace(" ", string.Empty); + var byteData = Convert.FromHexString(bytes); + fixed (byte* ptr = byteData) + { + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var idx = slot.ToIndex(); + if (idx * 4 + 3 >= byteData.Length) + continue; + + var armor = ((LegacyCharacterArmor*)ptr)[idx]; + var item = items.Identify(slot, armor.Set, armor.Variant); + data.SetItem(slot, item); + data.SetStain(slot, armor.Stain); + } + + data.Customize.Read(ptr); + } + } + + private static void ParseMainHand(ItemManager items, JObject jObj, ref DesignData data) + { + var mainhand = jObj["MainHand"]; + if (mainhand == null) + { + data.SetItem(EquipSlot.MainHand, items.DefaultSword); + data.SetStain(EquipSlot.MainHand, StainIds.None); + return; + } + + var set = mainhand["Item1"]?.ToObject() ?? items.DefaultSword.PrimaryId; + var type = mainhand["Item2"]?.ToObject() ?? items.DefaultSword.SecondaryId; + var variant = mainhand["Item3"]?.ToObject() ?? items.DefaultSword.Variant; + var stain = mainhand["Item4"]?.ToObject() ?? 0; + var item = items.Identify(EquipSlot.MainHand, set, type, variant); + + data.SetItem(EquipSlot.MainHand, item.Valid ? item : items.DefaultSword); + data.SetStain(EquipSlot.MainHand, new StainIds(stain)); + } + + private static void ParseOffHand(ItemManager items, JObject jObj, ref DesignData data) + { + var offhand = jObj["OffHand"]; + var defaultOffhand = items.GetDefaultOffhand(data.Item(EquipSlot.MainHand)); + if (offhand == null) + { + data.SetItem(EquipSlot.MainHand, defaultOffhand); + data.SetStain(EquipSlot.MainHand, defaultOffhand.PrimaryId.Id == 0 ? StainIds.None : data.Stain(EquipSlot.MainHand)); + return; + } + + var set = offhand["Item1"]?.ToObject() ?? items.DefaultSword.PrimaryId; + var type = offhand["Item2"]?.ToObject() ?? items.DefaultSword.SecondaryId; + var variant = offhand["Item3"]?.ToObject() ?? items.DefaultSword.Variant; + var stain = offhand["Item4"]?.ToObject() ?? 0; + var item = items.Identify(EquipSlot.OffHand, set, type, variant, data.MainhandType); + + data.SetItem(EquipSlot.OffHand, item.Valid ? item : defaultOffhand); + data.SetStain(EquipSlot.OffHand, defaultOffhand.PrimaryId.Id == 0 ? StainIds.None : new StainIds(stain)); + } +} diff --git a/Glamourer/Interop/ContextMenuService.cs b/Glamourer/Interop/ContextMenuService.cs index c210f1f..1f85612 100644 --- a/Glamourer/Interop/ContextMenuService.cs +++ b/Glamourer/Interop/ContextMenuService.cs @@ -1,160 +1,158 @@ -using System; -using Dalamud.ContextMenu; -using Dalamud.Game.Text; -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Plugin; +using Dalamud.Game.Gui.ContextMenu; using Dalamud.Plugin.Services; -using Glamourer.Events; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using Glamourer.Designs; using Glamourer.Services; using Glamourer.State; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.Interop; public class ContextMenuService : IDisposable { - public const int ItemSearchContextItemId = 0x1738; - public const int ChatLogContextItemId = 0x948; + public const int ChatLogContextItemId = 0x958; private readonly ItemManager _items; - private readonly DalamudContextMenu _contextMenu; + private readonly IContextMenu _contextMenu; private readonly StateManager _state; - private readonly ObjectManager _objects; - private readonly IGameGui _gameGui; + private readonly ActorObjectManager _objects; + private EquipItem _lastItem; + private readonly StainId[] _lastStains = new StainId[StainId.NumStains]; - public ContextMenuService(ItemManager items, StateManager state, ObjectManager objects, IGameGui gameGui, Configuration config, - DalamudPluginInterface pi) + private readonly MenuItem _inventoryItem; + + public ContextMenuService(ItemManager items, StateManager state, ActorObjectManager objects, Configuration config, + IContextMenu context) { - _contextMenu = new DalamudContextMenu(pi); + _contextMenu = context; _items = items; _state = state; _objects = objects; - _gameGui = gameGui; if (config.EnableGameContextMenu) Enable(); + + _inventoryItem = new MenuItem + { + IsEnabled = true, + IsReturn = false, + PrefixChar = 'G', + Name = "Try On", + OnClicked = OnClick, + IsSubmenu = false, + PrefixColor = 541, + }; + } + + private unsafe void OnMenuOpened(IMenuOpenedArgs args) + { + if (args.MenuType is ContextMenuType.Inventory) + { + var arg = (MenuTargetInventory)args.Target; + if (arg.TargetItem.HasValue && HandleItem(arg.TargetItem.Value.ItemId)) + { + for (var i = 0; i < arg.TargetItem.Value.Stains.Length; ++i) + _lastStains[i] = arg.TargetItem.Value.Stains[i]; + args.AddMenuItem(_inventoryItem); + } + } + else + { + switch (args.AddonName) + { + case "ItemSearch" when args.AgentPtr != nint.Zero: + { + if (HandleItem((ItemId)AgentContext.Instance()->UpdateCheckerParam)) + args.AddMenuItem(_inventoryItem); + + break; + } + case "ChatLog": + { + var agent = AgentChatLog.Instance(); + if (agent == null || !ValidateChatLogContext(agent)) + return; + + if (HandleItem(*(ItemId*)(agent + ChatLogContextItemId))) + { + for (var i = 0; i < _lastStains.Length; ++i) + _lastStains[i] = 0; + args.AddMenuItem(_inventoryItem); + } + + break; + } + case "RecipeNote": + { + var agent = AgentRecipeNote.Instance(); + if (agent == null) + return; + + if (HandleItem(agent->ContextMenuResultItemId)) + { + for (var i = 0; i < _lastStains.Length; ++i) + _lastStains[i] = 0; + args.AddMenuItem(_inventoryItem); + } + + break; + } + case "InclusionShop": + { + var agent = AgentRecipeItemContext.Instance(); + if (agent == null) + return; + + if (HandleItem(agent->ResultItemId)) + { + for (var i = 0; i < _lastStains.Length; ++i) + _lastStains[i] = 0; + args.AddMenuItem(_inventoryItem); + } + + break; + } + } + } } public void Enable() - { - _contextMenu.OnOpenGameObjectContextMenu += AddGameObjectItem; - _contextMenu.OnOpenInventoryContextMenu += AddInventoryItem; - } + => _contextMenu.OnMenuOpened += OnMenuOpened; public void Disable() - { - _contextMenu.OnOpenGameObjectContextMenu -= AddGameObjectItem; - _contextMenu.OnOpenInventoryContextMenu -= AddInventoryItem; - } + => _contextMenu.OnMenuOpened -= OnMenuOpened; public void Dispose() + => Disable(); + + private void OnClick(IMenuItemClickedArgs _) { - Disable(); - _contextMenu.Dispose(); + var (id, playerData) = _objects.PlayerData; + if (!playerData.Valid) + return; + + if (!_state.GetOrCreate(id, playerData.Objects[0], out var state)) + return; + + var slot = _lastItem.Type.ToSlot(); + _state.ChangeEquip(state, slot, _lastItem, _lastStains[0], ApplySettings.Manual); + if (!_lastItem.Type.ValidOffhand().IsOffhandType()) + return; + + if (_lastItem.PrimaryId.Id is > 1600 and < 1651 + && _items.ItemData.TryGetValue(_lastItem.ItemId, EquipSlot.Hands, out var gauntlets)) + _state.ChangeEquip(state, EquipSlot.Hands, gauntlets, _lastStains[0], ApplySettings.Manual); + if (_items.ItemData.TryGetValue(_lastItem.ItemId, EquipSlot.OffHand, out var offhand)) + _state.ChangeEquip(state, EquipSlot.OffHand, offhand, _lastStains[0], ApplySettings.Manual); } - private static readonly SeString TryOnString = new SeStringBuilder().AddUiForeground(SeIconChar.BoxedLetterG.ToIconString(), 541) - .AddText(" Try On").AddUiForegroundOff().BuiltString; - - private void AddInventoryItem(InventoryContextMenuOpenArgs args) + private bool HandleItem(ItemId id) { - var item = CheckInventoryItem(args.ItemId); - if (item != null) - args.AddCustomItem(item); + var itemId = id.StripModifiers; + return _items.ItemData.TryGetValue(itemId, EquipSlot.MainHand, out _lastItem); } - private InventoryContextMenuItem? CheckInventoryItem(uint itemId) - { - if (itemId > 500000) - itemId -= 500000; - - if (!_items.ItemService.AwaitedService.TryGetValue(itemId, EquipSlot.MainHand, out var item)) - return null; - - return new InventoryContextMenuItem(TryOnString, GetInventoryAction(item)); - } - - - private GameObjectContextMenuItem? CheckGameObjectItem(uint itemId) - { - if (itemId > 500000) - itemId -= 500000; - - if (!_items.ItemService.AwaitedService.TryGetValue(itemId, EquipSlot.MainHand, out var item)) - return null; - - return new GameObjectContextMenuItem(TryOnString, GetGameObjectAction(item)); - } - - private unsafe GameObjectContextMenuItem? CheckGameObjectItem(IntPtr agent, int offset, Func validate) - => agent != IntPtr.Zero && validate(agent) ? CheckGameObjectItem(*(uint*)(agent + offset)) : null; - - private unsafe GameObjectContextMenuItem? CheckGameObjectItem(IntPtr agent, int offset) - => agent != IntPtr.Zero ? CheckGameObjectItem(*(uint*)(agent + offset)) : null; - - private GameObjectContextMenuItem? CheckGameObjectItem(string name, int offset, Func validate) - => CheckGameObjectItem(_gameGui.FindAgentInterface(name), offset, validate); - - private void AddGameObjectItem(GameObjectContextMenuOpenArgs args) - { - var item = args.ParentAddonName switch - { - "ItemSearch" => CheckGameObjectItem(args.Agent, ItemSearchContextItemId), - "ChatLog" => CheckGameObjectItem("ChatLog", ChatLogContextItemId, ValidateChatLogContext), - _ => null, - }; - if (item != null) - args.AddCustomItem(item); - } - - private DalamudContextMenu.InventoryContextMenuItemSelectedDelegate GetInventoryAction(EquipItem item) - { - return _ => - { - var (id, playerData) = _objects.PlayerData; - if (!playerData.Valid) - return; - - if (!_state.GetOrCreate(id, playerData.Objects[0], out var state)) - return; - - var slot = item.Type.ToSlot(); - _state.ChangeEquip(state, slot, item, 0, StateChanged.Source.Manual); - if (item.Type.ValidOffhand().IsOffhandType()) - { - if (item.ModelId.Id is > 1600 and < 1651 - && _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.Hands, out var gauntlets)) - _state.ChangeEquip(state, EquipSlot.Hands, gauntlets, 0, StateChanged.Source.Manual); - if (_items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) - _state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, StateChanged.Source.Manual); - } - }; - } - - private DalamudContextMenu.GameObjectContextMenuItemSelectedDelegate GetGameObjectAction(EquipItem item) - { - return _ => - { - var (id, playerData) = _objects.PlayerData; - if (!playerData.Valid) - return; - - if (!_state.GetOrCreate(id, playerData.Objects[0], out var state)) - return; - - var slot = item.Type.ToSlot(); - _state.ChangeEquip(state, slot, item, 0, StateChanged.Source.Manual); - if (item.Type.ValidOffhand().IsOffhandType()) - { - if (item.ModelId.Id is > 1600 and < 1651 - && _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.Hands, out var gauntlets)) - _state.ChangeEquip(state, EquipSlot.Hands, gauntlets, 0, StateChanged.Source.Manual); - if (_items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) - _state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, StateChanged.Source.Manual); - } - }; - } - - private static unsafe bool ValidateChatLogContext(nint agent) - => *(uint*)(agent + ChatLogContextItemId + 8) == 3; + private static unsafe bool ValidateChatLogContext(AgentChatLog* agent) + => *(&agent->ContextItemId + 8) == 3; } diff --git a/Glamourer/Interop/CrestService.cs b/Glamourer/Interop/CrestService.cs new file mode 100644 index 0000000..2b55f94 --- /dev/null +++ b/Glamourer/Interop/CrestService.cs @@ -0,0 +1,176 @@ +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Event; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Classes; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; + +namespace Glamourer.Interop; + +/// +/// Triggered when the crest visibility is updated on a model. +/// +/// Parameter is the model with an update. +/// Parameter is the equipment slot changed. +/// Parameter is whether the crest will be shown. +/// +/// +public sealed unsafe class CrestService : EventWrapperRef3 +{ + public enum Priority + { + /// + StateListener = 0, + } + + public CrestService(IGameInteropProvider interop) + : base(nameof(CrestService)) + { + interop.InitializeFromAttributes(this); + _humanSetFreeCompanyCrestVisibleOnSlot = + interop.HookFromAddress(_humanVTable[109], HumanSetFreeCompanyCrestVisibleOnSlotDetour); + _weaponSetFreeCompanyCrestVisibleOnSlot = + interop.HookFromAddress(_weaponVTable[109], WeaponSetFreeCompanyCrestVisibleOnSlotDetour); + _humanSetFreeCompanyCrestVisibleOnSlot.Enable(); + _weaponSetFreeCompanyCrestVisibleOnSlot.Enable(); + _crestChangeHook.Enable(); + _crestChangeCallerHook.Enable(); + } + + public void UpdateCrests(Actor gameObject, CrestFlag flags) + { + if (!gameObject.IsCharacter) + return; + + flags &= CrestExtensions.AllRelevant; + var currentCrests = gameObject.CrestBitfield; + using var update = _inUpdate.EnterMethod(); + _crestChangeHook.Original(&gameObject.AsCharacter->DrawData, (byte) flags); + gameObject.CrestBitfield = currentCrests; + } + + public delegate void DrawObjectCrestUpdateDelegate(Model drawObject, CrestFlag slot, ref bool value); + + public event DrawObjectCrestUpdateDelegate? ModelCrestSetup; + + protected override void Dispose(bool _) + { + _humanSetFreeCompanyCrestVisibleOnSlot.Dispose(); + _weaponSetFreeCompanyCrestVisibleOnSlot.Dispose(); + _crestChangeHook.Dispose(); + _crestChangeCallerHook.Dispose(); + } + + private delegate void CrestChangeDelegate(DrawDataContainer* container, byte crestFlags); + + [Signature(Sigs.SetFreeCompanyCrestBitfield, DetourName = nameof(CrestChangeDetour))] + private readonly Hook _crestChangeHook = null!; + + private void CrestChangeDetour(DrawDataContainer* container, byte crestFlags) + { + var actor = (Actor)container->OwnerObject; + foreach (var slot in CrestExtensions.AllRelevantSet) + { + var newValue = ((CrestFlag)crestFlags).HasFlag(slot); + Invoke(actor, slot, ref newValue); + crestFlags = (byte)(newValue ? crestFlags | (byte)slot : crestFlags & (byte)~slot); + } + + Glamourer.Log.Verbose( + $"Called CrestChange on {(ulong)container:X} with {crestFlags:X} and prior flags {actor.CrestBitfield}."); + using var _ = _inUpdate.EnterMethod(); + _crestChangeHook.Original(container, crestFlags); + } + + [Signature(Sigs.CrestChangeCaller, DetourName = nameof(CrestChangeCallerDetour))] + private readonly Hook _crestChangeCallerHook = null!; + + private delegate void CrestChangeCallerDelegate(DrawDataContainer* container, byte* data); + + private void CrestChangeCallerDetour(DrawDataContainer* container, byte* data) + { + var actor = (Actor)container->OwnerObject; + ref var flags = ref data[16]; + foreach (var slot in CrestExtensions.AllRelevantSet) + { + var newValue = ((CrestFlag)flags).HasFlag(slot); + Invoke(actor, slot, ref newValue); + flags = (byte)(newValue ? flags | (byte)slot : flags & (byte)~slot); + } + Glamourer.Log.Verbose( + $"Called inlined CrestChange via CrestChangeCaller on {(ulong)container:X} with {(flags & 0x1F):X} and prior flags {actor.CrestBitfield}."); + + using var _ = _inUpdate.EnterMethod(); + _crestChangeCallerHook.Original(container, data); + } + + public static bool GetModelCrest(Actor gameObject, CrestFlag slot) + { + if (!gameObject.IsCharacter) + return false; + + var (type, index) = slot.ToIndex(); + switch (type) + { + case CrestType.Human: + { + var model = gameObject.Model; + if (!model.IsHuman) + return false; + + return model.AsHuman->IsFreeCompanyCrestVisibleOnSlot(index); + } + case CrestType.Offhand: + { + var model = (Model)gameObject.AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).DrawObject; + if (!model.IsWeapon) + return false; + + return model.AsWeapon->IsFreeCompanyCrestVisibleOnSlot(index); + } + } + + return false; + } + + private readonly InMethodChecker _inUpdate = new(); + + private delegate void SetCrestDelegateIntern(DrawObject* drawObject, byte slot, byte visible); + + [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _humanVTable = null!; + + [Signature(Sigs.WeaponVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _weaponVTable = null!; + + private readonly Hook _humanSetFreeCompanyCrestVisibleOnSlot; + private readonly Hook _weaponSetFreeCompanyCrestVisibleOnSlot; + + private void HumanSetFreeCompanyCrestVisibleOnSlotDetour(DrawObject* drawObject, byte slotIdx, byte visible) + { + var rVisible = visible != 0; + var inUpdate = _inUpdate.InMethod; + var slot = (CrestFlag)((ushort)CrestFlag.Head << slotIdx); + if (!inUpdate) + ModelCrestSetup?.Invoke(drawObject, slot, ref rVisible); + + Glamourer.Log.Excessive( + $"[Human.SetFreeCompanyCrestVisibleOnSlot] Called with 0x{(ulong)drawObject:X} for slot {slot} with {rVisible} (original: {visible != 0}, in update: {inUpdate})."); + _humanSetFreeCompanyCrestVisibleOnSlot.Original(drawObject, slotIdx, rVisible ? (byte)1 : (byte)0); + } + + private void WeaponSetFreeCompanyCrestVisibleOnSlotDetour(DrawObject* drawObject, byte slotIdx, byte visible) + { + var rVisible = visible != 0; + var inUpdate = _inUpdate.InMethod; + if (!inUpdate && slotIdx == 0) + ModelCrestSetup?.Invoke(drawObject, CrestFlag.OffHand, ref rVisible); + Glamourer.Log.Excessive( + $"[Weapon.SetFreeCompanyCrestVisibleOnSlot] Called with 0x{(ulong)drawObject:X} with {rVisible} (original: {visible != 0}, in update: {inUpdate})."); + _weaponSetFreeCompanyCrestVisibleOnSlot.Original(drawObject, slotIdx, rVisible ? (byte)1 : (byte)0); + } +} diff --git a/Glamourer/Interop/DatFileService.cs b/Glamourer/Interop/DatFileService.cs deleted file mode 100644 index 0d27bcc..0000000 --- a/Glamourer/Interop/DatFileService.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using Dalamud.Interface.DragDrop; -using Dalamud.Interface.Internal.Notifications; -using Glamourer.Customization; -using Glamourer.Services; -using Glamourer.Unlocks; -using ImGuiNET; -using OtterGui.Classes; - -namespace Glamourer.Interop; - -public class DatFileService -{ - private readonly CustomizationService _customizations; - private readonly CustomizeUnlockManager _unlocks; - private readonly IDragDropManager _dragDropManager; - - public DatFileService(CustomizationService customizations, CustomizeUnlockManager unlocks, IDragDropManager dragDropManager) - { - _customizations = customizations; - _unlocks = unlocks; - _dragDropManager = dragDropManager; - } - - public void CreateSource() - { - _dragDropManager.CreateImGuiSource("DatDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".dat"), m => - { - ImGui.TextUnformatted($"Dragging {Path.GetFileName(m.Files[0])} to import customizations for Glamourer..."); - return true; - }); - } - - public bool CreateImGuiTarget(out DatCharacterFile file) - { - if (!_dragDropManager.CreateImGuiTarget("DatDragger", out var files, out _) || files.Count != 1) - { - file = default; - return false; - } - - return LoadDesign(files[0], out file); - } - - public bool LoadDesign(string path, out DatCharacterFile file) - { - if (!File.Exists(path)) - { - file = default; - return false; - } - - try - { - using var stream = File.OpenRead(path); - if (!DatCharacterFile.Read(stream, out file)) - return false; - - if (!Verify(file)) - return false; - } - catch (Exception ex) - { - Glamourer.Messager.NotificationMessage(ex, $"Could not read character data file {path}.", NotificationType.Error); - file = default; - } - - return true; - } - - public bool SaveDesign(string path, in Customize input, string description) - { - if (!Verify(input, out var voice)) - return false; - - if (description.Length > 40) - return false; - - if (path.Length == 0) - return false; - - try - { - var file = new DatCharacterFile(input, voice, description); - var directories = Path.GetDirectoryName(path); - if (directories != null) - Directory.CreateDirectory(directories); - using var stream = File.Open(path, File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew); - file.Write(stream); - - return true; - } - catch (Exception ex) - { - Glamourer.Messager.NotificationMessage(ex, $"Could not save character data to file {path}.", "Failure", NotificationType.Error); - return false; - } - } - - public bool Verify(in Customize input, out byte voice, byte? inputVoice = null) - { - voice = 0; - if (_customizations.ValidateClan(input.Clan, input.Race, out _, out _).Length > 0) - return false; - if (!_customizations.IsGenderValid(input.Race, input.Gender)) - return false; - if (input.BodyType.Value != 1) - return false; - - var set = _customizations.AwaitedService.GetList(input.Clan, input.Gender); - voice = set.Voices[0]; - if (inputVoice.HasValue && !set.Voices.Contains(inputVoice.Value)) - return false; - - foreach (var index in CustomizationExtensions.AllBasic) - { - if (!CustomizationService.IsCustomizationValid(set, input.Face, index, input[index])) - return false; - } - - if (input[CustomizeIndex.LegacyTattoo].Value != 0) - return false; - - return true; - } - - public bool Verify(in DatCharacterFile datFile) - { - var customize = datFile.Customize; - if (!Verify(customize, out _, (byte)datFile.Voice)) - return false; - - if (datFile.Time < DateTimeOffset.UnixEpoch || datFile.Time > DateTimeOffset.UtcNow) - return false; - - if (datFile.Checksum != datFile.CalculateChecksum()) - return false; - - return true; - } -} diff --git a/Glamourer/Interop/ImportService.cs b/Glamourer/Interop/ImportService.cs new file mode 100644 index 0000000..c6e90fd --- /dev/null +++ b/Glamourer/Interop/ImportService.cs @@ -0,0 +1,207 @@ +using Dalamud.Interface.DragDrop; +using Dalamud.Interface.ImGuiNotification; +using Glamourer.Designs; +using Glamourer.Interop.CharaFile; +using Glamourer.Services; +using Dalamud.Bindings.ImGui; +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop; + +public class ImportService(CustomizeService _customizations, IDragDropManager _dragDropManager, ItemManager _items) +{ + public void CreateDatSource() + => _dragDropManager.CreateImGuiSource("DatDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".dat"), m => + { + ImGui.TextUnformatted($"Dragging {Path.GetFileName(m.Files[0])} to import customizations for Glamourer..."); + return true; + }); + + public void CreateCharaSource() + => _dragDropManager.CreateImGuiSource("CharaDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".chara") || m.Extensions.Contains(".cma"), m => + { + ImGui.TextUnformatted($"Dragging {Path.GetFileName(m.Files[0])} to import Anamnesis/CMTool data for Glamourer..."); + return true; + }); + + public bool CreateDatTarget(out DatCharacterFile file) + { + if (!_dragDropManager.CreateImGuiTarget("DatDragger", out var files, out _) || files.Count != 1) + { + file = default; + return false; + } + + return LoadDat(files[0], out file); + } + + public bool CreateCharaTarget([NotNullWhen(true)] out DesignBase? design, out string name) + { + if (!_dragDropManager.CreateImGuiTarget("CharaDragger", out var files, out _) || files.Count != 1) + { + design = null; + name = string.Empty; + return false; + } + + return Path.GetExtension(files[0]) is ".chara" ? LoadChara(files[0], out design, out name) : LoadCma(files[0], out design, out name); + } + + public bool LoadChara(string path, [NotNullWhen(true)] out DesignBase? design, out string name) + { + if (!File.Exists(path)) + { + design = null; + name = string.Empty; + return false; + } + + try + { + var text = File.ReadAllText(path); + var file = CharaFile.CharaFile.ParseData(_items, text, Path.GetFileNameWithoutExtension(path)); + + name = file.Name; + design = new DesignBase(_customizations, file.Data, file.ApplyEquip, file.ApplyCustomize, file.ApplyBonus); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Could not read .chara file {path}.", NotificationType.Error); + design = null; + name = string.Empty; + return false; + } + + return true; + } + + public bool LoadCma(string path, [NotNullWhen(true)] out DesignBase? design, out string name) + { + if (!File.Exists(path)) + { + design = null; + name = string.Empty; + return false; + } + + try + { + var text = File.ReadAllText(path); + var file = CmaFile.ParseData(_items, text, Path.GetFileNameWithoutExtension(path)); + if (file == null) + throw new Exception(); + + name = file.Name; + design = new DesignBase(_customizations, file.Data, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, 0); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Could not read .cma file {path}.", NotificationType.Error); + design = null; + name = string.Empty; + return false; + } + + return true; + } + + public bool LoadDat(string path, out DatCharacterFile file) + { + if (!File.Exists(path)) + { + file = default; + return false; + } + + try + { + using var stream = File.OpenRead(path); + if (!DatCharacterFile.Read(stream, out file)) + return false; + + if (!Verify(file)) + return false; + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Could not read character data file {path}.", NotificationType.Error); + file = default; + } + + return true; + } + + public bool SaveDesignAsDat(string path, in CustomizeArray input, string description) + { + if (!Verify(input, out var voice)) + return false; + + if (description.Length > 40) + return false; + + if (path.Length == 0) + return false; + + try + { + var file = new DatCharacterFile(input, voice, description); + var directories = Path.GetDirectoryName(path); + if (directories != null) + Directory.CreateDirectory(directories); + using var stream = File.Open(path, File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew); + file.Write(stream); + + return true; + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Could not save character data to file {path}.", "Failure", NotificationType.Error); + return false; + } + } + + public bool Verify(in CustomizeArray input, out byte voice, byte? inputVoice = null) + { + voice = 0; + if (_customizations.ValidateClan(input.Clan, input.Race, out _, out _).Length > 0) + return false; + if (!_customizations.IsGenderValid(input.Race, input.Gender)) + return false; + if (input.BodyType.Value != 1) + return false; + + var set = _customizations.Manager.GetSet(input.Clan, input.Gender); + voice = set.Voices[0]; + if (inputVoice.HasValue && !set.Voices.Contains(inputVoice.Value)) + return false; + + foreach (var index in CustomizationExtensions.AllBasic) + { + if (!CustomizeService.IsCustomizationValid(set, input.Face, index, input[index])) + return false; + } + + if (input[CustomizeIndex.LegacyTattoo].Value != 0) + return false; + + return true; + } + + public bool Verify(in DatCharacterFile datFile) + { + var customize = datFile.Customize; + if (!Verify(customize, out _, (byte)datFile.Voice)) + return false; + + if (datFile.Time < DateTimeOffset.UnixEpoch || datFile.Time > DateTimeOffset.UtcNow) + return false; + + if (datFile.Checksum != datFile.CalculateChecksum()) + return false; + + return true; + } +} diff --git a/Glamourer/Interop/InventoryService.cs b/Glamourer/Interop/InventoryService.cs index f2832bd..c30ae06 100644 --- a/Glamourer/Interop/InventoryService.cs +++ b/Glamourer/Interop/InventoryService.cs @@ -1,27 +1,29 @@ -using System; -using System.Collections.Generic; -using Dalamud.Hooking; +using Dalamud.Hooking; using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using Glamourer.Events; +using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.String; namespace Glamourer.Interop; -public unsafe class InventoryService : IDisposable +public sealed unsafe class InventoryService : IDisposable, IRequiredService { - private readonly MovedEquipment _event; - private readonly List<(EquipSlot, uint, StainId)> _itemList = new(12); + private readonly MovedEquipment _movedItemsEvent; + private readonly EquippedGearset _gearsetEvent; + private readonly List<(EquipSlot, uint, StainIds)> _itemList = new(12); - public InventoryService(MovedEquipment @event, IGameInteropProvider interop) + public InventoryService(MovedEquipment movedItemsEvent, IGameInteropProvider interop, EquippedGearset gearsetEvent) { - _event = @event; + _movedItemsEvent = movedItemsEvent; + _gearsetEvent = gearsetEvent; _moveItemHook = interop.HookFromAddress((nint)InventoryManager.MemberFunctionPointers.MoveItemSlot, MoveItemDetour); - _equipGearsetHook = - interop.HookFromAddress((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearset, EquipGearSetDetour); + _equipGearsetHook = interop.HookFromAddress((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearsetInternal, EquipGearSetDetour); _moveItemHook.Enable(); _equipGearsetHook.Enable(); @@ -33,17 +35,20 @@ public unsafe class InventoryService : IDisposable _equipGearsetHook.Dispose(); } - private delegate int EquipGearsetDelegate(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId); + private delegate nint EquipGearsetInternalDelegate(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId); - private readonly Hook _equipGearsetHook; + private readonly Hook _equipGearsetHook = null!; - private int EquipGearSetDetour(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId) + private nint EquipGearSetDetour(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId) { + var prior = module->CurrentGearsetIndex; var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId); - Glamourer.Log.Excessive($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); + var set = module->GetGearset((int)gearsetId); + _gearsetEvent.Invoke(new ByteString(set->Name).ToString(), (int)gearsetId, prior, glamourPlateId, set->ClassJob); + Glamourer.Log.Verbose($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); if (ret == 0) { - var entry = module->GetGearset(gearsetId); + var entry = module->GetGearset((int)gearsetId); if (entry == null) return ret; @@ -55,64 +60,67 @@ public unsafe class InventoryService : IDisposable if (glamourPlateId != 0) { - void Add(EquipSlot slot, uint glamourId, StainId glamourStain, ref RaptureGearsetModule.GearsetItem item) + void Add(EquipSlot slot, uint glamourId, StainIds glamourStain, ref RaptureGearsetModule.GearsetItem item) { - if (item.ItemID == 0) - _itemList.Add((slot, 0, 0)); + if (item.ItemId == 0) + _itemList.Add((slot, 0, StainIds.None)); else if (glamourId != 0) _itemList.Add((slot, glamourId, glamourStain)); else if (item.GlamourId != 0) - _itemList.Add((slot, item.GlamourId, item.Stain)); + _itemList.Add((slot, item.GlamourId, StainIds.FromGearsetItem(item))); else - _itemList.Add((slot, item.ItemID, item.Stain)); + _itemList.Add((slot, FixId(item.ItemId), StainIds.FromGearsetItem(item))); } - var plate = MirageManager.Instance()->GlamourPlatesSpan[glamourPlateId - 1]; - Add(EquipSlot.MainHand, plate.ItemIds[0], plate.StainIds[0], ref entry->MainHand); - Add(EquipSlot.OffHand, plate.ItemIds[1], plate.StainIds[10], ref entry->OffHand); - Add(EquipSlot.Head, plate.ItemIds[2], plate.StainIds[2], ref entry->Head); - Add(EquipSlot.Body, plate.ItemIds[3], plate.StainIds[3], ref entry->Body); - Add(EquipSlot.Hands, plate.ItemIds[4], plate.StainIds[4], ref entry->Hands); - Add(EquipSlot.Legs, plate.ItemIds[5], plate.StainIds[5], ref entry->Legs); - Add(EquipSlot.Feet, plate.ItemIds[6], plate.StainIds[6], ref entry->Feet); - Add(EquipSlot.Ears, plate.ItemIds[7], plate.StainIds[7], ref entry->Ears); - Add(EquipSlot.Neck, plate.ItemIds[8], plate.StainIds[8], ref entry->Neck); - Add(EquipSlot.Wrists, plate.ItemIds[9], plate.StainIds[9], ref entry->Wrists); - Add(EquipSlot.RFinger, plate.ItemIds[10], plate.StainIds[10], ref entry->RingRight); - Add(EquipSlot.LFinger, plate.ItemIds[11], plate.StainIds[11], ref entry->RingLeft); + var plate = MirageManager.Instance()->GlamourPlates[glamourPlateId - 1]; + Add(EquipSlot.MainHand, plate.ItemIds[0], StainIds.FromGlamourPlate(plate, 0), ref entry->Items[0]); + Add(EquipSlot.OffHand, plate.ItemIds[1], StainIds.FromGlamourPlate(plate, 1), ref entry->Items[1]); + Add(EquipSlot.Head, plate.ItemIds[2], StainIds.FromGlamourPlate(plate, 2), ref entry->Items[2]); + Add(EquipSlot.Body, plate.ItemIds[3], StainIds.FromGlamourPlate(plate, 3), ref entry->Items[3]); + Add(EquipSlot.Hands, plate.ItemIds[4], StainIds.FromGlamourPlate(plate, 4), ref entry->Items[5]); + Add(EquipSlot.Legs, plate.ItemIds[5], StainIds.FromGlamourPlate(plate, 5), ref entry->Items[6]); + Add(EquipSlot.Feet, plate.ItemIds[6], StainIds.FromGlamourPlate(plate, 6), ref entry->Items[7]); + Add(EquipSlot.Ears, plate.ItemIds[7], StainIds.FromGlamourPlate(plate, 7), ref entry->Items[8]); + Add(EquipSlot.Neck, plate.ItemIds[8], StainIds.FromGlamourPlate(plate, 8), ref entry->Items[9]); + Add(EquipSlot.Wrists, plate.ItemIds[9], StainIds.FromGlamourPlate(plate, 9), ref entry->Items[10]); + Add(EquipSlot.RFinger, plate.ItemIds[10], StainIds.FromGlamourPlate(plate, 10), ref entry->Items[11]); + Add(EquipSlot.LFinger, plate.ItemIds[11], StainIds.FromGlamourPlate(plate, 11), ref entry->Items[12]); } else { void Add(EquipSlot slot, ref RaptureGearsetModule.GearsetItem item) { - if (item.ItemID == 0) - _itemList.Add((slot, 0, 0)); + if (item.ItemId == 0) + _itemList.Add((slot, 0, StainIds.None)); else if (item.GlamourId != 0) - _itemList.Add((slot, item.GlamourId, item.Stain)); + _itemList.Add((slot, item.GlamourId, StainIds.FromGearsetItem(item))); else - _itemList.Add((slot, item.ItemID, item.Stain)); + _itemList.Add((slot, FixId(item.ItemId), StainIds.FromGearsetItem(item))); } - Add(EquipSlot.MainHand, ref entry->MainHand); - Add(EquipSlot.OffHand, ref entry->OffHand); - Add(EquipSlot.Head, ref entry->Head); - Add(EquipSlot.Body, ref entry->Body); - Add(EquipSlot.Hands, ref entry->Hands); - Add(EquipSlot.Legs, ref entry->Legs); - Add(EquipSlot.Feet, ref entry->Feet); - Add(EquipSlot.Ears, ref entry->Ears); - Add(EquipSlot.Neck, ref entry->Neck); - Add(EquipSlot.Wrists, ref entry->Wrists); - Add(EquipSlot.RFinger, ref entry->RingRight); - Add(EquipSlot.LFinger, ref entry->RingLeft); + Add(EquipSlot.MainHand, ref entry->Items[0]); + Add(EquipSlot.OffHand, ref entry->Items[1]); + Add(EquipSlot.Head, ref entry->Items[2]); + Add(EquipSlot.Body, ref entry->Items[3]); + Add(EquipSlot.Hands, ref entry->Items[5]); + Add(EquipSlot.Legs, ref entry->Items[6]); + Add(EquipSlot.Feet, ref entry->Items[7]); + Add(EquipSlot.Ears, ref entry->Items[8]); + Add(EquipSlot.Neck, ref entry->Items[9]); + Add(EquipSlot.Wrists, ref entry->Items[10]); + Add(EquipSlot.RFinger, ref entry->Items[11]); + Add(EquipSlot.LFinger, ref entry->Items[12]); } - _event.Invoke(_itemList.ToArray()); + _movedItemsEvent.Invoke(_itemList.ToArray()); } return ret; } + private static uint FixId(uint itemId) + => itemId % 50000; + private delegate int MoveItemDelegate(InventoryManager* manager, InventoryType sourceContainer, ushort sourceSlot, InventoryType targetContainer, ushort targetSlot, byte unk); @@ -127,18 +135,18 @@ public unsafe class InventoryService : IDisposable { if (InvokeSource(sourceContainer, sourceSlot, out var source)) if (InvokeTarget(manager, targetContainer, targetSlot, out var target)) - _event.Invoke(new[] + _movedItemsEvent.Invoke(new[] { source, target, }); else - _event.Invoke(new[] + _movedItemsEvent.Invoke(new[] { source, }); else if (InvokeTarget(manager, targetContainer, targetSlot, out var target)) - _event.Invoke(new[] + _movedItemsEvent.Invoke(new[] { target, }); @@ -147,7 +155,7 @@ public unsafe class InventoryService : IDisposable return ret; } - private static bool InvokeSource(InventoryType sourceContainer, uint sourceSlot, out (EquipSlot, uint, StainId) tuple) + private static bool InvokeSource(InventoryType sourceContainer, uint sourceSlot, out (EquipSlot, uint, StainIds) tuple) { tuple = default; if (sourceContainer is not InventoryType.EquippedItems) @@ -157,12 +165,12 @@ public unsafe class InventoryService : IDisposable if (slot is EquipSlot.Unknown) return false; - tuple = (slot, 0u, 0); + tuple = (slot, 0u, StainIds.None); return true; } private static bool InvokeTarget(InventoryManager* manager, InventoryType targetContainer, uint targetSlot, - out (EquipSlot, uint, StainId) tuple) + out (EquipSlot, uint, StainIds) tuple) { tuple = default; if (targetContainer is not InventoryType.EquippedItems) @@ -174,14 +182,14 @@ public unsafe class InventoryService : IDisposable // Invoked after calling Original, so the item is already moved. var inventory = manager->GetInventoryContainer(targetContainer); - if (inventory == null || inventory->Loaded == 0 || inventory->Size <= targetSlot) + if (inventory == null || inventory->IsLoaded || inventory->Size <= targetSlot) return false; var item = inventory->GetInventorySlot((int)targetSlot); if (item == null) return false; - tuple = (slot, item->GlamourID != 0 ? item->GlamourID : item->ItemID, item->Stain); + tuple = (slot, item->GlamourId != 0 ? item->GlamourId : item->ItemId, new StainIds(item->Stains)); return true; } diff --git a/Glamourer/Interop/JobService.cs b/Glamourer/Interop/JobService.cs index 2fe322f..1797809 100644 --- a/Glamourer/Interop/JobService.cs +++ b/Glamourer/Interop/JobService.cs @@ -1,12 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; -using Glamourer.Interop.Structs; -using Glamourer.Structs; +using Penumbra.GameData; +using Penumbra.GameData.DataContainers; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; namespace Glamourer.Interop; @@ -14,19 +13,20 @@ public class JobService : IDisposable { private readonly nint _characterDataOffset; - public readonly IReadOnlyDictionary Jobs; - public readonly IReadOnlyDictionary JobGroups; - public readonly IReadOnlyList AllJobGroups; + public readonly DictJob Jobs; + public readonly DictJobGroup JobGroups; + + public IReadOnlyList AllJobGroups + => JobGroups.AllJobGroups; public event Action? JobChanged; - public JobService(IDataManager gameData, IGameInteropProvider interop) + public JobService(DictJob jobs, DictJobGroup jobGroups, IGameInteropProvider interop) { interop.InitializeFromAttributes(this); _characterDataOffset = Marshal.OffsetOf(nameof(Character.CharacterData)); - Jobs = GameData.Jobs(gameData); - AllJobGroups = GameData.AllJobGroups(gameData); - JobGroups = GameData.JobGroups(gameData); + Jobs = jobs; + JobGroups = jobGroups; _changeJobHook.Enable(); } diff --git a/Glamourer/Interop/Material/DirectXService.cs b/Glamourer/Interop/Material/DirectXService.cs new file mode 100644 index 0000000..8006a2f --- /dev/null +++ b/Glamourer/Interop/Material/DirectXService.cs @@ -0,0 +1,182 @@ +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.String.Functions; +using SharpGen.Runtime; +using Vortice.Direct3D11; +using MapFlags = Vortice.Direct3D11.MapFlags; +using Texture = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture; + +namespace Glamourer.Interop.Material; + +public unsafe class DirectXService(IFramework framework) : IService +{ + private readonly object _lock = new(); + private readonly ConcurrentDictionary _textures = []; + + /// Generate a color table the way the game does inside the original texture, and release the original. + /// The original texture that will be replaced with a new one. + /// The input color table. + /// Success or failure. + public bool ReplaceColorTable(Texture** original, in ColorTable.Table colorTable) + { + if (original == null) + return false; + + var textureSize = stackalloc int[2]; + textureSize[0] = MaterialService.TextureWidth; + textureSize[1] = MaterialService.TextureHeight; + + lock (_lock) + { + using var texture = new SafeTextureHandle(MaterialService.CreateColorTableTexture(), false); + if (texture.IsInvalid) + return false; + + fixed (ColorTable.Table* ptr = &colorTable) + { + if (!texture.Texture->InitializeContents(ptr)) + return false; + } + + Glamourer.Log.Verbose($"[{Thread.CurrentThread.ManagedThreadId}] Replaced texture {(ulong)*original:X} with new ColorTable."); + texture.Exchange(ref *(nint*)original); + } + + return true; + } + + public bool TryGetColorTable(Texture* texture, out ColorTable.Table table) + { + if (_textures.TryGetValue((nint)texture, out var p) && framework.LastUpdateUTC == p.Update) + { + table = p.Table; + return true; + } + + lock (_lock) + { + if (!TextureColorTable(texture, out table)) + return false; + } + + _textures[(nint)texture] = (framework.LastUpdateUTC, table); + return true; + } + + /// Try to turn a color table GPU-loaded texture (R16G16B16A16Float, 4 Width, 16 Height) into an actual color table. + /// A pointer to the internal texture struct containing the GPU handle. + /// The returned color table. + /// Whether the table could be fetched. + private static bool TextureColorTable(Texture* texture, out ColorTable.Table table) + { + if (texture == null) + { + table = default; + return false; + } + + try + { + // Create direct x resource and ensure that it is kept alive. + using var tex = new ID3D11Texture2D1((nint)texture->D3D11Texture2D); + tex.AddRef(); + + table = GetResourceData(tex, CreateStagedClone, GetTextureData); + return true; + } + catch + { + table = default; + return false; + } + } + + /// Create a staging clone of the existing texture handle for stability reasons. + private static ID3D11Texture2D1 CreateStagedClone(ID3D11Texture2D1 resource) + { + var desc = resource.Description1 with + { + Usage = ResourceUsage.Staging, + BindFlags = 0, + CPUAccessFlags = CpuAccessFlags.Read, + MiscFlags = 0, + }; + + var ret = resource.Device.As().CreateTexture2D1(desc); + Glamourer.Log.Excessive( + $"[{Thread.CurrentThread.ManagedThreadId}] Cloning resource {resource.NativePointer:X} to {ret.NativePointer:X}"); + return ret; + } + + /// Turn a mapped texture into a color table. + private static ColorTable.Table GetTextureData(ID3D11Texture2D1 resource, MappedSubresource map) + { + var desc = resource.Description1; + + if (desc.Format is not Vortice.DXGI.Format.R16G16B16A16_Float + || desc.Width != MaterialService.TextureWidth + || desc.Height != MaterialService.TextureHeight + || map.DepthPitch != map.RowPitch * desc.Height) + throw new InvalidDataException("The texture was not a valid color table texture."); + + return ReadTexture(map.DataPointer, map.DepthPitch, desc.Height, map.RowPitch); + } + + /// Transform the GPU data into the color table. + /// The pointer to the raw texture data. + /// The size of the raw texture data. + /// The height of the texture. (Needs to be 16). + /// The stride in the texture data. + /// + private static ColorTable.Table ReadTexture(nint data, int length, int height, int pitch) + { + // Check that the data has sufficient dimension and size. + var expectedSize = sizeof(Half) * MaterialService.TextureWidth * height * 4; + if (length < expectedSize || sizeof(ColorTable.Table) != expectedSize || height != MaterialService.TextureHeight) + return default; + + var ret = new ColorTable.Table(); + var target = (byte*)&ret; + // If the stride is the same as in the table, just copy. + if (pitch == MaterialService.TextureWidth) + MemoryUtility.MemCpyUnchecked(target, (void*)data, length); + // Otherwise, adapt the stride. + else + + for (var y = 0; y < height; ++y) + { + MemoryUtility.MemCpyUnchecked(target + y * MaterialService.TextureWidth * sizeof(Half) * 4, (byte*)data + y * pitch, + MaterialService.TextureWidth * sizeof(Half) * 4); + } + + return ret; + } + + /// Get resources of a texture. + private static TRet GetResourceData(T res, Func cloneResource, Func getData) + where T : ID3D11Resource + { + using var stagingRes = cloneResource(res); + + res.Device.ImmediateContext.CopyResource(stagingRes, res); + Glamourer.Log.Excessive( + $"[{Thread.CurrentThread.ManagedThreadId}] Copied resource data {res.NativePointer:X} to {stagingRes.NativePointer:X}"); + stagingRes.Device.ImmediateContext.Map(stagingRes, 0, MapMode.Read, MapFlags.None, out var mapInfo).CheckError(); + Glamourer.Log.Excessive( + $"[{Thread.CurrentThread.ManagedThreadId}] Mapped resource data for {stagingRes.NativePointer:X} to {mapInfo.DataPointer:X}"); + + try + { + return getData(stagingRes, mapInfo); + } + finally + { + Glamourer.Log.Excessive($"[{Thread.CurrentThread.ManagedThreadId}] Obtained resource data."); + stagingRes.Device.ImmediateContext.Unmap(stagingRes, 0); + Glamourer.Log.Excessive($"[{Thread.CurrentThread.ManagedThreadId}] Unmapped resource data for {stagingRes.NativePointer:X}"); + } + } + + private static readonly Result WasStillDrawing = new(0x887A000A); +} diff --git a/Glamourer/Interop/Material/LiveColorTablePreviewer.cs b/Glamourer/Interop/Material/LiveColorTablePreviewer.cs new file mode 100644 index 0000000..3b9edb7 --- /dev/null +++ b/Glamourer/Interop/Material/LiveColorTablePreviewer.cs @@ -0,0 +1,127 @@ +using Dalamud.Plugin.Services; +using Dalamud.Bindings.ImGui; +using OtterGui.Services; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop.Material; + +public sealed unsafe class LiveColorTablePreviewer : IService, IDisposable +{ + private readonly global::Penumbra.GameData.Interop.ObjectManager _objects; + private readonly IFramework _framework; + private readonly DirectXService _directXService; + + public MaterialValueIndex LastValueIndex { get; private set; } = MaterialValueIndex.Invalid; + public ColorTable.Table LastOriginalColorTable { get; private set; } + private MaterialValueIndex _valueIndex = MaterialValueIndex.Invalid; + private ObjectIndex _lastObjectIndex = ObjectIndex.AnyIndex; + private ObjectIndex _objectIndex = ObjectIndex.AnyIndex; + private ColorTable.Table _originalColorTable; + + public LiveColorTablePreviewer(global::Penumbra.GameData.Interop.ObjectManager objects, IFramework framework, DirectXService directXService) + { + _objects = objects; + _framework = framework; + _directXService = directXService; + _framework.Update += OnFramework; + } + + private void Reset() + { + if (LastValueIndex.DrawObject is MaterialValueIndex.DrawObjectType.Invalid || _lastObjectIndex == ObjectIndex.AnyIndex) + return; + + var actor = _objects[_lastObjectIndex]; + if (actor.IsCharacter && LastValueIndex.TryGetTexture(actor, out var texture)) + _directXService.ReplaceColorTable(texture, LastOriginalColorTable); + + LastValueIndex = MaterialValueIndex.Invalid; + _lastObjectIndex = ObjectIndex.AnyIndex; + } + + private void OnFramework(IFramework _) + { + if (_valueIndex.DrawObject is MaterialValueIndex.DrawObjectType.Invalid || _objectIndex == ObjectIndex.AnyIndex) + { + Reset(); + _valueIndex = MaterialValueIndex.Invalid; + _objectIndex = ObjectIndex.AnyIndex; + return; + } + + var actor = _objects[_objectIndex]; + if (!actor.IsCharacter) + { + _valueIndex = MaterialValueIndex.Invalid; + _objectIndex = ObjectIndex.AnyIndex; + return; + } + + if (_valueIndex != LastValueIndex || _lastObjectIndex != _objectIndex) + { + Reset(); + LastValueIndex = _valueIndex; + _lastObjectIndex = _objectIndex; + LastOriginalColorTable = _originalColorTable; + } + + if (_valueIndex.TryGetTexture(actor, out var texture)) + { + var diffuse = CalculateDiffuse(); + var emissive = diffuse / 8; + var table = LastOriginalColorTable; + if (_valueIndex.RowIndex != byte.MaxValue) + { + table[_valueIndex.RowIndex].DiffuseColor = (HalfColor)diffuse; + table[_valueIndex.RowIndex].EmissiveColor = (HalfColor)emissive; + } + else + { + for (var i = 0; i < ColorTable.NumRows; ++i) + { + table[i].DiffuseColor = (HalfColor)diffuse; + table[i].EmissiveColor = (HalfColor)emissive; + } + } + + _directXService.ReplaceColorTable(texture, table); + } + + _valueIndex = MaterialValueIndex.Invalid; + _objectIndex = ObjectIndex.AnyIndex; + } + + public void OnHover(MaterialValueIndex index, ObjectIndex objectIndex, in ColorTable.Table table) + { + if (_valueIndex.DrawObject is not MaterialValueIndex.DrawObjectType.Invalid) + return; + + _valueIndex = index; + _objectIndex = objectIndex; + if (LastValueIndex.DrawObject is MaterialValueIndex.DrawObjectType.Invalid + || _lastObjectIndex == ObjectIndex.AnyIndex + || LastValueIndex.MaterialIndex != _valueIndex.MaterialIndex + || LastValueIndex.DrawObject != _valueIndex.DrawObject + || LastValueIndex.SlotIndex != _valueIndex.SlotIndex) + _originalColorTable = table; + } + + private static Vector3 CalculateDiffuse() + { + const long frameLength = TimeSpan.TicksPerMillisecond * 5; + const long steps = 2000; + var frame = DateTimeOffset.UtcNow.UtcTicks; + var hueByte = frame % (steps * frameLength) / frameLength; + var hue = (float)hueByte / steps; + Vector3 ret; + ImGui.ColorConvertHSVtoRGB(hue, 1, 1, &ret.X, &ret.Y, &ret.Z); + return ret; + } + + public void Dispose() + { + Reset(); + _framework.Update -= OnFramework; + } +} diff --git a/Glamourer/Interop/Material/MaterialManager.cs b/Glamourer/Interop/Material/MaterialManager.cs new file mode 100644 index 0000000..43e500b --- /dev/null +++ b/Glamourer/Interop/Material/MaterialManager.cs @@ -0,0 +1,228 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.Havok.Animation.Rig; +using Glamourer.Designs; +using Glamourer.Interop.Penumbra; +using Glamourer.State; +using OtterGui.Services; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop.Material; + +public sealed unsafe class MaterialManager : IRequiredService, IDisposable +{ + private readonly PrepareColorSet _event; + private readonly StateManager _stateManager; + private readonly PenumbraService _penumbra; + private readonly ActorManager _actors; + private readonly Configuration _config; + + private int _lastSlot; + + private readonly ThreadLocal> _deleteList = new(() => []); + + public MaterialManager(PrepareColorSet prepareColorSet, StateManager stateManager, ActorManager actors, PenumbraService penumbra, + Configuration config) + { + _stateManager = stateManager; + _actors = actors; + _penumbra = penumbra; + _config = config; + _event = prepareColorSet; + _event.Subscribe(OnPrepareColorSet, PrepareColorSet.Priority.MaterialManager); + } + + public void Dispose() + => _event.Unsubscribe(OnPrepareColorSet); + + private void OnPrepareColorSet(CharacterBase* characterBase, MaterialResourceHandle* material, ref StainIds stain, ref nint ret) + { + var actor = _penumbra.GameObjectFromDrawObject(characterBase); + var validType = FindType(characterBase, actor, out var type); + var (slotId, materialId) = FindMaterial(characterBase, material); + + if (!validType + || type is not MaterialValueIndex.DrawObjectType.Human && slotId > 0 + || !actor.Identifier(_actors, out var identifier) + || !_stateManager.TryGetValue(identifier, out var state)) + return; + + var min = MaterialValueIndex.Min(type, slotId, materialId); + var max = MaterialValueIndex.Max(type, slotId, materialId); + var values = state.Materials.GetValues(min, max); + if (values.Length == 0) + return; + + if (!PrepareColorSet.TryGetColorTable(material, stain, out var baseColorSet)) + return; + + var drawData = type switch + { + MaterialValueIndex.DrawObjectType.Human => GetTempSlot((Human*)characterBase, (HumanSlot)slotId), + _ => GetTempSlot((Weapon*)characterBase), + }; + var mode = PrepareColorSet.GetMode(material); + UpdateMaterialValues(state, values, drawData, ref baseColorSet, mode); + + if (MaterialService.GenerateNewColorTable(baseColorSet, out var texture)) + ret = (nint)texture; + } + + /// Update and apply the glamourer state of an actor according to the application sources when updated by the game. + private void UpdateMaterialValues(ActorState state, ReadOnlySpan<(uint Key, MaterialValueState Value)> values, CharacterWeapon drawData, + ref ColorTable.Table colorTable, ColorRow.Mode mode) + { + var deleteList = _deleteList.Value!; + deleteList.Clear(); + for (var i = 0; i < values.Length; ++i) + { + var idx = MaterialValueIndex.FromKey(values[i].Key); + var materialValue = values[i].Value; + ref var row = ref colorTable[idx.RowIndex]; + var newGame = new ColorRow(row); + if (materialValue.EqualGame(newGame, drawData)) + materialValue.Model.Apply(ref row, mode); + else + switch (materialValue.Source) + { + case StateSource.Pending: + materialValue.Model.Apply(ref row, mode); + state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, StateSource.Manual), + out _); + break; + case StateSource.IpcPending: + materialValue.Model.Apply(ref row, mode); + state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, StateSource.IpcManual), + out _); + break; + case StateSource.IpcManual: + case StateSource.Manual: + deleteList.Add(idx); + break; + case StateSource.Fixed: + case StateSource.IpcFixed: + materialValue.Model.Apply(ref row, mode); + state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, materialValue.Source), + out _); + break; + } + } + + foreach (var idx in deleteList) + _stateManager.ResetMaterialValue(state, idx, ApplySettings.Game); + } + + /// + /// Find the index of a material by searching through a draw objects pointers. + /// Tries to take shortcuts for consecutive searches like when a character is newly created. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private (byte SlotId, byte MaterialId) FindMaterial(CharacterBase* characterBase, MaterialResourceHandle* material) + { + for (var i = _lastSlot; i < characterBase->SlotCount; ++i) + { + var idx = MaterialService.MaterialsPerModel * i; + for (var j = 0; j < MaterialService.MaterialsPerModel; ++j) + { + var mat = (nint)characterBase->Materials[idx++]; + if (mat != (nint)material) + continue; + + _lastSlot = i; + return ((byte)i, (byte)j); + } + } + + for (var i = 0; i < _lastSlot; ++i) + { + var idx = MaterialService.MaterialsPerModel * i; + for (var j = 0; j < MaterialService.MaterialsPerModel; ++j) + { + var mat = (nint)characterBase->Materials[idx++]; + if (mat != (nint)material) + continue; + + _lastSlot = i; + return ((byte)i, (byte)j); + } + } + + return (byte.MaxValue, byte.MaxValue); + } + + /// Find the type of the given draw object by checking the actors pointers. + private static bool FindType(CharacterBase* characterBase, Actor actor, out MaterialValueIndex.DrawObjectType type) + { + if (!actor.Valid) + { + type = MaterialValueIndex.DrawObjectType.Invalid; + return false; + } + + if (actor.Model.AsCharacterBase == characterBase && ((Model)characterBase).IsHuman) + { + type = MaterialValueIndex.DrawObjectType.Human; + return true; + } + + if (!actor.AsObject->IsCharacter()) + { + type = MaterialValueIndex.DrawObjectType.Invalid; + return false; + } + + if (actor.AsCharacter->DrawData.WeaponData[0].DrawObject == characterBase) + { + type = MaterialValueIndex.DrawObjectType.Mainhand; + return true; + } + + if (actor.AsCharacter->DrawData.WeaponData[1].DrawObject == characterBase) + { + type = MaterialValueIndex.DrawObjectType.Offhand; + return true; + } + + type = MaterialValueIndex.DrawObjectType.Invalid; + return false; + } + + /// We need to get the temporary set, variant and stain that is currently being set if it is available. + private static CharacterWeapon GetTempSlot(Human* human, HumanSlot slotId) + { + if (human->ChangedEquipData is null) + return slotId.ToSpecificEnum() switch + { + EquipSlot slot => ((Model)human).GetArmor(slot).ToWeapon(0), + BonusItemFlag bonus => ((Model)human).GetBonus(bonus).ToWeapon(0), + _ => default, + }; + + if (!slotId.ToSlotIndex(out var index)) + return default; + + var item = (ChangedEquipData*)human->ChangedEquipData + index; + if (index < 10) + return ((CharacterArmor*)item)->ToWeapon(0); + + return new CharacterWeapon(item->BonusModel, 0, item->BonusVariant, StainIds.None); + } + + /// + /// We need to get the temporary set, variant and stain that is currently being set if it is available. + /// Weapons do not change in skeleton id without being reconstructed, so this is not changeable data. + /// + private static CharacterWeapon GetTempSlot(Weapon* weapon) + { + var changedData = weapon->ChangedData; + if (changedData == null) + return new CharacterWeapon(weapon->ModelSetId, weapon->SecondaryId, (Variant)weapon->Variant, StainIds.FromWeapon(*weapon)); + + return new CharacterWeapon(weapon->ModelSetId, changedData->SecondaryId, changedData->Variant, + new StainIds(changedData->Stain0, changedData->Stain1)); + } +} diff --git a/Glamourer/Interop/Material/MaterialService.cs b/Glamourer/Interop/Material/MaterialService.cs new file mode 100644 index 0000000..4893e14 --- /dev/null +++ b/Glamourer/Interop/Material/MaterialService.cs @@ -0,0 +1,76 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Interop; +using Texture = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture; + +namespace Glamourer.Interop.Material; + +public static unsafe class MaterialService +{ + private const TextureFormat Format = TextureFormat.R16G16B16A16_FLOAT; + private const TextureFlags Flags = TextureFlags.TextureType2D | TextureFlags.Managed | TextureFlags.Immutable; + + public const int TextureWidth = 8; + public const int TextureHeight = ColorTable.NumRows; + public const int MaterialsPerModel = 10; + + public static Texture* CreateColorTableTexture() + { + var textureSize = stackalloc int[2]; + textureSize[0] = TextureWidth; + textureSize[1] = TextureHeight; + return Device.Instance()->CreateTexture2D(textureSize, 1, Format, Flags, 7); + } + + public static bool GenerateNewColorTable(in ColorTable.Table colorTable, out Texture* texture) + { + texture = CreateColorTableTexture(); + if (texture == null) + return false; + + fixed (ColorTable.Table* ptr = &colorTable) + { + return texture->InitializeContents(ptr); + } + } + + /// Obtain a pointer to the models pointer to a specific color table texture. + /// + /// + /// + /// + public static Texture** GetColorTableTexture(Model model, int modelSlot, byte materialSlot) + { + if (!model.IsCharacterBase) + return null; + + var index = modelSlot * MaterialsPerModel + materialSlot; + if (index < 0 || index >= model.AsCharacterBase->ColorTableTexturesSpan.Length) + return null; + + var texture = (Texture**)Unsafe.AsPointer(ref model.AsCharacterBase->ColorTableTexturesSpan[index]); + return texture; + } + + /// Obtain a pointer to the color table of a certain material from a model. + /// The draw object. + /// The model slot. + /// The material slot in the model. + /// A pointer to the color table or null. + public static ColorTable.Table* GetMaterialColorTable(Model model, int modelSlot, byte materialSlot) + { + if (!model.IsCharacterBase) + return null; + + var index = modelSlot * MaterialsPerModel + materialSlot; + if (index < 0 || index >= model.AsCharacterBase->MaterialsSpan.Length) + return null; + + var material = (MaterialResourceHandle*) model.AsCharacterBase->MaterialsSpan[index].Value; + if (material == null || material->DataSet == null || material->DataSetSize < sizeof(ColorTable.Table) || !material->HasColorTable) + return null; + + return (ColorTable.Table*)material->DataSet; + } +} diff --git a/Glamourer/Interop/Material/MaterialValueIndex.cs b/Glamourer/Interop/Material/MaterialValueIndex.cs new file mode 100644 index 0000000..eb3f71f --- /dev/null +++ b/Glamourer/Interop/Material/MaterialValueIndex.cs @@ -0,0 +1,272 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.Interop; +using Newtonsoft.Json; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Interop; +using CsMaterial = FFXIVClientStructs.FFXIV.Client.Graphics.Render.Material; + +namespace Glamourer.Interop.Material; + +[JsonConverter(typeof(Converter))] +public readonly record struct MaterialValueIndex( + MaterialValueIndex.DrawObjectType DrawObject, + byte SlotIndex, + byte MaterialIndex, + byte RowIndex) +{ + public static readonly MaterialValueIndex Invalid = new(DrawObjectType.Invalid, 0, 0, 0); + + public uint Key + => ToKey(DrawObject, SlotIndex, MaterialIndex, RowIndex); + + public bool Valid + => Validate(DrawObject) && ValidateSlot(DrawObject, SlotIndex) && ValidateMaterial(MaterialIndex) && ValidateRow(RowIndex); + + public static bool FromKey(uint key, out MaterialValueIndex index) + { + index = new MaterialValueIndex(key); + return index.Valid; + } + + public static MaterialValueIndex FromSlot(EquipSlot slot) + { + if (slot is EquipSlot.MainHand) + return new MaterialValueIndex(DrawObjectType.Mainhand, 0, 0, 0); + if (slot is EquipSlot.OffHand) + return new MaterialValueIndex(DrawObjectType.Offhand, 0, 0, 0); + + var idx = slot.ToIndex(); + if (idx < 10) + return new MaterialValueIndex(DrawObjectType.Human, (byte)idx, 0, 0); + + return Invalid; + } + + public static MaterialValueIndex FromSlot(BonusItemFlag slot) + { + var idx = slot.ToIndex(); + return idx > 2 ? Invalid : new MaterialValueIndex(DrawObjectType.Human, (byte)(idx + 16), 0, 0); + } + + public string SlotName() + { + var slot = ToEquipSlot(); + if (slot is not EquipSlot.Unknown) + return slot.ToName(); + + if (DrawObject is DrawObjectType.Human && SlotIndex is 16) + return BonusItemFlag.Glasses.ToString(); + + return EquipSlot.Unknown.ToName(); + } + + public EquipSlot ToEquipSlot() + => DrawObject switch + { + DrawObjectType.Human when SlotIndex < 10 => ((uint)SlotIndex).ToEquipSlot(), + DrawObjectType.Mainhand when SlotIndex == 0 => EquipSlot.MainHand, + DrawObjectType.Offhand when SlotIndex == 0 => EquipSlot.OffHand, + _ => EquipSlot.Unknown, + }; + + public BonusItemFlag ToBonusSlot() + => DrawObject switch + { + DrawObjectType.Human when SlotIndex > 15 => ((uint)SlotIndex - 16).ToBonusSlot(), + _ => BonusItemFlag.Unknown, + }; + + public unsafe bool TryGetModel(Actor actor, out Model model) + { + if (!actor.Valid) + { + model = Model.Null; + return false; + } + + model = DrawObject switch + { + DrawObjectType.Human => actor.Model, + DrawObjectType.Mainhand => actor.IsCharacter ? actor.AsCharacter->DrawData.WeaponData[0].DrawObject : Model.Null, + DrawObjectType.Offhand => actor.IsCharacter ? actor.AsCharacter->DrawData.WeaponData[1].DrawObject : Model.Null, + _ => Model.Null, + }; + return model.IsCharacterBase; + } + + public unsafe bool TryGetTextures(Actor actor, out ReadOnlySpan> textures, out ReadOnlySpan> materials) + { + if (!TryGetModel(actor, out var model) + || SlotIndex >= model.AsCharacterBase->SlotCount + || model.AsCharacterBase->ColorTableTexturesSpan.Length < (SlotIndex + 1) * MaterialService.MaterialsPerModel) + { + textures = []; + materials = []; + return false; + } + + var from = SlotIndex * MaterialService.MaterialsPerModel; + textures = model.AsCharacterBase->ColorTableTexturesSpan.Slice(from, MaterialService.MaterialsPerModel); + materials = model.AsCharacterBase->MaterialsSpan.Slice(from, MaterialService.MaterialsPerModel); + return true; + } + + public unsafe bool TryGetTextures(Actor actor, out ReadOnlySpan> textures) + { + if (!TryGetModel(actor, out var model) + || SlotIndex >= model.AsCharacterBase->SlotCount + || model.AsCharacterBase->ColorTableTexturesSpan.Length < (SlotIndex + 1) * MaterialService.MaterialsPerModel) + { + textures = []; + return false; + } + + var from = SlotIndex * MaterialService.MaterialsPerModel; + textures = model.AsCharacterBase->ColorTableTexturesSpan.Slice(from, MaterialService.MaterialsPerModel); + return true; + } + + + public unsafe bool TryGetTexture(Actor actor, out Texture** texture) + { + if (TryGetTextures(actor, out var textures)) + return TryGetTexture(textures, out texture); + + texture = null; + return false; + } + + public unsafe bool TryGetTexture(Actor actor, out Texture** texture, out ColorRow.Mode mode) + { + if (TryGetTextures(actor, out var textures, out var materials)) + return TryGetTexture(textures, materials, out texture, out mode); + + mode = ColorRow.Mode.Dawntrail; + texture = null; + return false; + } + + public unsafe bool TryGetTexture(ReadOnlySpan> textures, ReadOnlySpan> materials, + out Texture** texture, out ColorRow.Mode mode) + { + mode = MaterialIndex >= materials.Length + ? ColorRow.Mode.Dawntrail + : PrepareColorSet.GetMode((MaterialResourceHandle*)materials[MaterialIndex].Value); + + + if (MaterialIndex >= textures.Length || textures[MaterialIndex].Value == null) + { + texture = null; + return false; + } + + fixed (Pointer* ptr = textures) + { + texture = (Texture**)ptr + MaterialIndex; + } + + return true; + } + + public unsafe bool TryGetTexture(ReadOnlySpan> textures, out Texture** texture) + { + if (MaterialIndex >= textures.Length || textures[MaterialIndex].Value == null) + { + texture = null; + return false; + } + + fixed (Pointer* ptr = textures) + { + texture = (Texture**)ptr + MaterialIndex; + } + + return true; + } + + + public static MaterialValueIndex FromKey(uint key) + => new(key); + + public static MaterialValueIndex Min(DrawObjectType drawObject = 0, byte slotIndex = 0, byte materialIndex = 0, byte rowIndex = 0) + => new(drawObject, slotIndex, materialIndex, rowIndex); + + public static MaterialValueIndex Max(DrawObjectType drawObject = (DrawObjectType)byte.MaxValue, byte slotIndex = byte.MaxValue, + byte materialIndex = byte.MaxValue, byte rowIndex = byte.MaxValue) + => new(drawObject, slotIndex, materialIndex, rowIndex); + + public enum DrawObjectType : byte + { + Invalid, + Human, + Mainhand, + Offhand, + }; + + public static bool Validate(DrawObjectType type) + => type is not DrawObjectType.Invalid && Enum.IsDefined(type); + + public static bool ValidateSlot(DrawObjectType type, byte slotIndex) + => type switch + { + DrawObjectType.Human => slotIndex < 18, + DrawObjectType.Mainhand => slotIndex == 0, + DrawObjectType.Offhand => slotIndex == 0, + _ => false, + }; + + public static bool ValidateMaterial(byte materialIndex) + => materialIndex < MaterialService.MaterialsPerModel; + + public static bool ValidateRow(byte rowIndex) + => rowIndex < ColorTable.NumRows; + + private static uint ToKey(DrawObjectType type, byte slotIndex, byte materialIndex, byte rowIndex) + { + var result = (uint)rowIndex; + result |= (uint)materialIndex << 8; + result |= (uint)slotIndex << 16; + result |= (uint)((byte)type << 24); + return result; + } + + private MaterialValueIndex(uint key) + : this((DrawObjectType)(key >> 24), (byte)(key >> 16), (byte)(key >> 8), (byte)key) + { } + + public override string ToString() + => DrawObject switch + { + DrawObjectType.Invalid => "Invalid", + DrawObjectType.Human when SlotIndex < 10 => $"{((uint)SlotIndex).ToEquipSlot().ToName()} {MaterialString()} {RowString()}", + DrawObjectType.Human when SlotIndex == 10 => $"{BodySlot.Hair} {MaterialString()} {RowString()}", + DrawObjectType.Human when SlotIndex == 11 => $"{BodySlot.Face} {MaterialString()} {RowString()}", + DrawObjectType.Human when SlotIndex == 12 => $"{BodySlot.Tail} / {BodySlot.Ear} {MaterialString()} {RowString()}", + DrawObjectType.Human when SlotIndex == 13 => $"Connectors {MaterialString()} {RowString()}", + DrawObjectType.Human when SlotIndex == 16 => $"{BonusItemFlag.Glasses.ToName()} {MaterialString()} {RowString()}", + DrawObjectType.Human when SlotIndex == 17 => $"{BonusItemFlag.UnkSlot.ToName()} {MaterialString()} {RowString()}", + DrawObjectType.Mainhand when SlotIndex == 0 => $"{EquipSlot.MainHand.ToName()} {MaterialString()} {RowString()}", + DrawObjectType.Offhand when SlotIndex == 0 => $"{EquipSlot.OffHand.ToName()} {MaterialString()} {RowString()}", + _ => $"{DrawObject} Slot {SlotIndex} {MaterialString()} {RowString()}", + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string MaterialString() + => $"Material {(char)(MaterialIndex + 'A')}"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string RowString() + => $"Row {RowIndex / 2 + 1}{(char)(RowIndex % 2 + 'A')}"; + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, MaterialValueIndex value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Key); + + public override MaterialValueIndex ReadJson(JsonReader reader, Type objectType, MaterialValueIndex existingValue, bool hasExistingValue, + JsonSerializer serializer) + => FromKey(serializer.Deserialize(reader), out var value) ? value : throw new Exception($"Invalid material key {value.Key}."); + } +} diff --git a/Glamourer/Interop/Material/MaterialValueManager.cs b/Glamourer/Interop/Material/MaterialValueManager.cs new file mode 100644 index 0000000..01cb479 --- /dev/null +++ b/Glamourer/Interop/Material/MaterialValueManager.cs @@ -0,0 +1,463 @@ +global using StateMaterialManager = Glamourer.Interop.Material.MaterialValueManager; +global using DesignMaterialManager = Glamourer.Interop.Material.MaterialValueManager; +using Glamourer.GameData; +using Glamourer.State; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Structs; + + +namespace Glamourer.Interop.Material; + +/// Values are not squared. +public struct ColorRow(Vector3 diffuse, Vector3 specular, Vector3 emissive, float specularStrength, float glossStrength) +{ + public enum Mode + { + Legacy, + Dawntrail, + } + + public static readonly ColorRow Empty = new(Vector3.Zero, Vector3.Zero, Vector3.Zero, 1f, 1f); + + public Vector3 Diffuse = diffuse; + public Vector3 Specular = specular; + public Vector3 Emissive = emissive; + public float SpecularStrength = specularStrength; + public float GlossStrength = glossStrength; + + public ColorRow(in ColorTableRow row) + : this(Root((Vector3)row.DiffuseColor), Root((Vector3)row.SpecularColor), Root((Vector3)row.EmissiveColor), + (float)row.LegacySpecularStrength(), + (float)row.LegacyGloss()) + { } + + public readonly bool NearEqual(in ColorRow rhs) + => Diffuse.NearEqual(rhs.Diffuse) + && Specular.NearEqual(rhs.Specular) + && Emissive.NearEqual(rhs.Emissive) + && SpecularStrength.NearEqual(rhs.SpecularStrength) + && GlossStrength.NearEqual(rhs.GlossStrength); + + private static Vector3 Square(Vector3 value) + => new(Square(value.X), Square(value.Y), Square(value.Z)); + + private static float Square(float value) + => value < 0 ? -value * value : value * value; + + private static Vector3 Root(Vector3 value) + => new(Root(value.X), Root(value.Y), Root(value.Z)); + + private static float Root(float value) + => value < 0 ? MathF.Sqrt(-value) : MathF.Sqrt(value); + + public readonly bool Apply(ref ColorTableRow row, Mode mode) + { + var ret = false; + var d = Square(Diffuse); + if (!((Vector3)row.DiffuseColor).NearEqual(d)) + { + row.DiffuseColor = (HalfColor)d; + ret = true; + } + + var s = Square(Specular); + if (!((Vector3)row.SpecularColor).NearEqual(s)) + { + row.SpecularColor = (HalfColor)s; + ret = true; + } + + var e = Square(Emissive); + if (!((Vector3)row.EmissiveColor).NearEqual(e)) + { + row.EmissiveColor = (HalfColor)e; + ret = true; + } + + if (mode is Mode.Legacy) + { + if (!((float)row.LegacySpecularStrength()).NearEqual(SpecularStrength)) + { + row.LegacySpecularStrengthWrite() = (Half)SpecularStrength; + ret = true; + } + + if (!((float)row.LegacyGloss()).NearEqual(GlossStrength)) + { + row.LegacyGlossWrite() = (Half)GlossStrength; + ret = true; + } + } + + return ret; + } +} + +internal static class ColorTableRowExtensions +{ + internal static Half LegacySpecularStrength(this in ColorTableRow row) + => row[7]; + + internal static Half LegacyGloss(this in ColorTableRow row) + => row[3]; + + internal static ref Half LegacySpecularStrengthWrite(this ref ColorTableRow row) + => ref row[7]; + + internal static ref Half LegacyGlossWrite(this ref ColorTableRow row) + => ref row[3]; +} + +[JsonConverter(typeof(Converter))] +public struct MaterialValueDesign(ColorRow value, bool enabled, bool revert) +{ + public ColorRow Value = value; + public bool Enabled = enabled; + public bool Revert = revert; + + public readonly bool Apply(ref MaterialValueState state) + { + if (!Enabled) + return false; + + if (Revert) + { + if (state.Model.NearEqual(state.Game)) + return false; + + state.Model = state.Game; + return true; + } + + if (state.Model.NearEqual(Value)) + return false; + + state.Model = Value; + return true; + } + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, MaterialValueDesign value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("Revert"); + writer.WriteValue(value.Revert); + writer.WritePropertyName("DiffuseR"); + writer.WriteValue(value.Value.Diffuse.X); + writer.WritePropertyName("DiffuseG"); + writer.WriteValue(value.Value.Diffuse.Y); + writer.WritePropertyName("DiffuseB"); + writer.WriteValue(value.Value.Diffuse.Z); + writer.WritePropertyName("SpecularR"); + writer.WriteValue(value.Value.Specular.X); + writer.WritePropertyName("SpecularG"); + writer.WriteValue(value.Value.Specular.Y); + writer.WritePropertyName("SpecularB"); + writer.WriteValue(value.Value.Specular.Z); + writer.WritePropertyName("SpecularA"); + writer.WriteValue(value.Value.SpecularStrength); + writer.WritePropertyName("EmissiveR"); + writer.WriteValue(value.Value.Emissive.X); + writer.WritePropertyName("EmissiveG"); + writer.WriteValue(value.Value.Emissive.Y); + writer.WritePropertyName("EmissiveB"); + writer.WriteValue(value.Value.Emissive.Z); + writer.WritePropertyName("Gloss"); + writer.WriteValue(value.Value.GlossStrength); + writer.WritePropertyName("Enabled"); + writer.WriteValue(value.Enabled); + writer.WriteEndObject(); + } + + public override MaterialValueDesign ReadJson(JsonReader reader, Type objectType, MaterialValueDesign existingValue, + bool hasExistingValue, + JsonSerializer serializer) + { + var obj = JObject.Load(reader); + Set(ref existingValue.Revert, obj["Revert"]?.Value()); + Set(ref existingValue.Value.Diffuse.X, obj["DiffuseR"]?.Value()); + Set(ref existingValue.Value.Diffuse.Y, obj["DiffuseG"]?.Value()); + Set(ref existingValue.Value.Diffuse.Z, obj["DiffuseB"]?.Value()); + Set(ref existingValue.Value.Specular.X, obj["SpecularR"]?.Value()); + Set(ref existingValue.Value.Specular.Y, obj["SpecularG"]?.Value()); + Set(ref existingValue.Value.Specular.Z, obj["SpecularB"]?.Value()); + Set(ref existingValue.Value.SpecularStrength, obj["SpecularA"]?.Value()); + Set(ref existingValue.Value.Emissive.X, obj["EmissiveR"]?.Value()); + Set(ref existingValue.Value.Emissive.Y, obj["EmissiveG"]?.Value()); + Set(ref existingValue.Value.Emissive.Z, obj["EmissiveB"]?.Value()); + Set(ref existingValue.Value.GlossStrength, obj["Gloss"]?.Value()); + existingValue.Enabled = obj["Enabled"]?.Value() ?? false; + return existingValue; + + static void Set(ref T target, T? value) + where T : struct + { + if (value.HasValue) + target = value.Value; + } + } + } +} + +public struct MaterialValueState( + in ColorRow game, + in ColorRow model, + CharacterWeapon drawData, + StateSource source) +{ + public MaterialValueState(in ColorRow gameRow, in ColorRow modelRow, CharacterArmor armor, StateSource source) + : this(gameRow, modelRow, armor.ToWeapon(0), source) + { } + + public ColorRow Game = game; + public ColorRow Model = model; + public readonly CharacterWeapon DrawData = drawData; + public readonly StateSource Source = source; + + public readonly bool EqualGame(in ColorRow rhsRow, CharacterWeapon rhsData) + => DrawData.Skeleton == rhsData.Skeleton + && DrawData.Weapon == rhsData.Weapon + && DrawData.Variant == rhsData.Variant + && DrawData.Stains == rhsData.Stains + && Game.NearEqual(rhsRow); + + public readonly MaterialValueDesign Convert() + => new(Model, true, false); +} + +public readonly struct MaterialValueManager +{ + private readonly List<(uint Key, T Value)> _values = []; + + public MaterialValueManager() + { } + + public void Clear() + => _values.Clear(); + + public MaterialValueManager Clone() + { + var ret = new MaterialValueManager(); + ret._values.AddRange(_values); + return ret; + } + + public bool TryGetValue(MaterialValueIndex index, out T value) + => TryGetValue(index.Key, out value); + + public bool TryGetValue(uint key, out T value) + { + if (_values.Count == 0) + { + value = default!; + return false; + } + + var idx = Search(key); + if (idx >= 0) + { + value = _values[idx].Value; + return true; + } + + value = default!; + return false; + } + + public bool TryAddValue(MaterialValueIndex index, in T value) + => TryAddValue(index.Key, value); + + public bool TryAddValue(uint key, in T value) + { + var idx = Search(key); + if (idx >= 0) + return false; + + _values.Insert(~idx, (key, value)); + return true; + } + + public bool CheckExistenceSlot(MaterialValueIndex index) + { + var key = CheckExistence(index); + return key.Valid && key.DrawObject == index.DrawObject && key.SlotIndex == index.SlotIndex; + } + + public bool CheckExistenceMaterial(MaterialValueIndex index) + { + var key = CheckExistence(index); + return key.Valid && key.DrawObject == index.DrawObject && key.SlotIndex == index.SlotIndex && key.MaterialIndex == index.MaterialIndex; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private MaterialValueIndex CheckExistence(MaterialValueIndex index) + { + if (_values.Count == 0) + return MaterialValueIndex.Invalid; + + var key = index.Key; + var idx = Search(key); + if (idx >= 0) + return index; + + idx = ~idx; + if (idx >= _values.Count) + return MaterialValueIndex.Invalid; + + return MaterialValueIndex.FromKey(_values[idx].Key); + } + + public bool RemoveValue(MaterialValueIndex index) + => RemoveValue(index.Key); + + public bool RemoveValue(uint key) + { + if (_values.Count == 0) + return false; + + var idx = Search(key); + if (idx < 0) + return false; + + _values.RemoveAt(idx); + return true; + } + + public void AddOrUpdateValue(MaterialValueIndex index, in T value) + => AddOrUpdateValue(index.Key, value); + + public void AddOrUpdateValue(uint key, in T value) + { + var idx = Search(key); + if (idx < 0) + _values.Insert(~idx, (key, value)); + else + _values[idx] = (key, value); + } + + public bool UpdateValue(MaterialValueIndex index, in T value, out T oldValue) + => UpdateValue(index.Key, value, out oldValue); + + public bool UpdateValue(uint key, in T value, out T oldValue) + { + if (_values.Count == 0) + { + oldValue = default!; + return false; + } + + var idx = Search(key); + if (idx < 0) + { + oldValue = default!; + return false; + } + + oldValue = _values[idx].Value; + _values[idx] = (key, value); + return true; + } + + public IReadOnlyList<(uint Key, T Value)> Values + => _values; + + public int RemoveValues(MaterialValueIndex min, MaterialValueIndex max) + { + var (minIdx, maxIdx) = MaterialValueManager.GetMinMax(CollectionsMarshal.AsSpan(_values), min.Key, max.Key); + if (minIdx < 0) + return 0; + + var count = maxIdx - minIdx; + _values.RemoveRange(minIdx, count); + return count; + } + + public ReadOnlySpan<(uint Key, T Value)> GetValues(MaterialValueIndex min, MaterialValueIndex max) + => MaterialValueManager.Filter(CollectionsMarshal.AsSpan(_values), min, max); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private int Search(uint key) + => _values.BinarySearch((key, default!), MaterialValueManager.Comparer.Instance); +} + +public static class MaterialValueManager +{ + internal class Comparer : IComparer<(uint Key, T Value)> + { + public static readonly Comparer Instance = new(); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + int IComparer<(uint Key, T Value)>.Compare((uint Key, T Value) x, (uint Key, T Value) y) + => x.Key.CompareTo(y.Key); + } + + public static bool GetSpecific(ReadOnlySpan<(uint Key, T Value)> values, MaterialValueIndex index, out T ret) + { + var idx = values.BinarySearch((index.Key, default!), Comparer.Instance); + if (idx < 0) + { + ret = default!; + return false; + } + + ret = values[idx].Value; + return true; + } + + public static ReadOnlySpan<(uint Key, T Value)> Filter(ReadOnlySpan<(uint Key, T Value)> values, MaterialValueIndex min, + MaterialValueIndex max) + { + var (minIdx, maxIdx) = GetMinMax(values, min.Key, max.Key); + return minIdx < 0 ? [] : values[minIdx..(maxIdx + 1)]; + } + + /// Obtain the minimum index and maximum index for a minimum and maximum key. + internal static (int MinIdx, int MaxIdx) GetMinMax(ReadOnlySpan<(uint Key, T Value)> values, uint minKey, uint maxKey) + { + // Find the minimum index by binary search. + var idx = values.BinarySearch((minKey, default!), Comparer.Instance); + var minIdx = idx; + + // If the key does not exist, check if it is an invalid range or set it correctly. + if (minIdx < 0) + { + minIdx = ~minIdx; + if (minIdx == values.Length || values[minIdx].Key > maxKey) + return (-1, -1); + + idx = minIdx; + } + else + { + // If it does exist, go upwards until the first key is reached that is actually smaller. + while (minIdx > 0 && values[minIdx - 1].Key >= minKey) + --minIdx; + } + + // Check if the range can be valid. + if (values[minIdx].Key < minKey || values[minIdx].Key > maxKey) + return (-1, -1); + + + // Do pretty much the same but in the other direction with the maximum key. + var maxIdx = values[idx..].BinarySearch((maxKey, default!), Comparer.Instance); + if (maxIdx < 0) + { + maxIdx = ~maxIdx + idx; + return maxIdx > minIdx ? (minIdx, maxIdx - 1) : (-1, -1); + } + + maxIdx += idx; + + while (maxIdx < values.Length - 1 && values[maxIdx + 1].Key <= maxKey) + ++maxIdx; + + if (values[maxIdx].Key < minKey || values[maxIdx].Key > maxKey) + return (-1, -1); + + return (minIdx, maxIdx); + } +} diff --git a/Glamourer/Interop/Material/PrepareColorSet.cs b/Glamourer/Interop/Material/PrepareColorSet.cs new file mode 100644 index 0000000..821a152 --- /dev/null +++ b/Glamourer/Interop/Material/PrepareColorSet.cs @@ -0,0 +1,158 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop.Material; + +public sealed unsafe class PrepareColorSet + : EventWrapperPtr12Ref34, IHookService +{ + private readonly UpdateColorSets _updateColorSets; + + public enum Priority + { + /// + MaterialManager = 0, + } + + public PrepareColorSet(HookManager hooks, UpdateColorSets updateColorSets) + : base("Prepare Color Set ") + { + _updateColorSets = updateColorSets; + hooks.Provider.InitializeFromAttributes(this); + _task = hooks.CreateHook(Name, Sigs.PrepareColorSet, Detour, true); + } + + private readonly Task> _task; + + public nint Address + => (nint)CharacterBase.MemberFunctionPointers.Destroy; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate Texture* Delegate(MaterialResourceHandle* material, StainId stainId1, StainId stainId2); + + private Texture* Detour(MaterialResourceHandle* material, StainId stainId1, StainId stainId2) + { + Glamourer.Log.Excessive($"[{Name}] Triggered with 0x{(nint)material:X} {stainId1.Id} {stainId2.Id}."); + var characterBase = _updateColorSets.Get(); + if (!characterBase.IsCharacterBase) + return _task.Result.Original(material, stainId1, stainId2); + + var ret = nint.Zero; + var stainIds = new StainIds(stainId1, stainId2); + Invoke(characterBase.AsCharacterBase, material, ref stainIds, ref ret); + if (ret != nint.Zero) + return (Texture*)ret; + + return _task.Result.Original(material, stainIds.Stain1, stainIds.Stain2); + } + + public static bool TryGetColorTable(MaterialResourceHandle* material, StainIds stainIds, + out ColorTable.Table table) + { + if (material->DataSet == null || material->DataSetSize < sizeof(ColorTable.Table) || !material->HasColorTable) + { + table = default; + return false; + } + + var newTable = *(ColorTable.Table*)material->DataSet; + if (GetDyeTable(material, out var dyeTable)) + { + if (stainIds.Stain1.Id != 0) + material->ReadStainingTemplate(dyeTable, stainIds.Stain1.Id, (Half*)&newTable, 0); + + if (stainIds.Stain2.Id != 0) + material->ReadStainingTemplate(dyeTable, stainIds.Stain2.Id, (Half*)&newTable, 1); + } + + table = newTable; + return true; + } + + /// Assumes the actor is valid. + public static bool TryGetColorTable(Actor actor, MaterialValueIndex index, out ColorTable.Table table, out ColorRow.Mode mode) + { + var idx = index.SlotIndex * MaterialService.MaterialsPerModel + index.MaterialIndex; + if (!index.TryGetModel(actor, out var model)) + { + mode = ColorRow.Mode.Dawntrail; + table = default; + return false; + } + + var handle = (MaterialResourceHandle*)model.AsCharacterBase->Materials[idx]; + if (handle == null) + { + mode = ColorRow.Mode.Dawntrail; + table = default; + return false; + } + + mode = GetMode(handle); + return TryGetColorTable(handle, GetStains(), out table); + + StainIds GetStains() + { + switch (index.DrawObject) + { + case MaterialValueIndex.DrawObjectType.Human: + return index.SlotIndex < 10 ? actor.Model.GetArmor(((uint)index.SlotIndex).ToEquipSlot()).Stains : StainIds.None; + case MaterialValueIndex.DrawObjectType.Mainhand: + var mainhand = (Model)actor.AsCharacter->DrawData.WeaponData[0].DrawObject; + return mainhand.IsWeapon ? StainIds.FromWeapon(*mainhand.AsWeapon) : StainIds.None; + case MaterialValueIndex.DrawObjectType.Offhand: + var offhand = (Model)actor.AsCharacter->DrawData.WeaponData[1].DrawObject; + return offhand.IsWeapon ? StainIds.FromWeapon(*offhand.AsWeapon) : StainIds.None; + default: return StainIds.None; + } + } + } + + /// Get the shader mode of the material. + public static ColorRow.Mode GetMode(MaterialResourceHandle* handle) + => handle == null + ? ColorRow.Mode.Dawntrail + : handle->ShpkName.AsSpan().SequenceEqual("characterlegacy.shpk"u8) + ? ColorRow.Mode.Legacy + : ColorRow.Mode.Dawntrail; + + /// Get the correct dye table for a material. + private static bool GetDyeTable(MaterialResourceHandle* material, out ushort* ptr) + { + ptr = null; + if (material->AdditionalDataSize is 0 || material->AdditionalData is null) + return false; + + var flags1 = material->AdditionalData[0]; + if ((flags1 & 0xF0) is 0) + { + ptr = (ushort*)material + 0x100; + return true; + } + + var flags2 = material->AdditionalData[1]; + var offset = 4 * (1 << (flags1 >> 4)) * (1 << (flags2 & 0x0F)); + ptr = (ushort*)material->DataSet + offset; + return true; + } +} diff --git a/Glamourer/Interop/Material/SafeTextureHandle.cs b/Glamourer/Interop/Material/SafeTextureHandle.cs new file mode 100644 index 0000000..20e6f65 --- /dev/null +++ b/Glamourer/Interop/Material/SafeTextureHandle.cs @@ -0,0 +1,49 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; + +namespace Glamourer.Interop.Material; + +public unsafe class SafeTextureHandle : SafeHandle +{ + public Texture* Texture + => (Texture*)handle; + + public override bool IsInvalid + => handle == 0; + + public SafeTextureHandle(Texture* handle, bool incRef, bool ownsHandle = true) + : base(0, ownsHandle) + { + if (incRef && !ownsHandle) + throw new ArgumentException("Non-owning SafeTextureHandle with IncRef is unsupported"); + + if (incRef && handle != null) + handle->IncRef(); + SetHandle((nint)handle); + } + + public void Exchange(ref nint ppTexture) + { + lock (this) + { + handle = Interlocked.Exchange(ref ppTexture, handle); + } + } + + public static SafeTextureHandle CreateInvalid() + => new(null, false); + + protected override bool ReleaseHandle() + { + nint handle; + lock (this) + { + handle = this.handle; + this.handle = 0; + } + + if (handle != 0) + ((Texture*)handle)->DecRef(); + + return true; + } +} \ No newline at end of file diff --git a/Glamourer/Interop/Material/UpdateColorSets.cs b/Glamourer/Interop/Material/UpdateColorSets.cs new file mode 100644 index 0000000..e503bc6 --- /dev/null +++ b/Glamourer/Interop/Material/UpdateColorSets.cs @@ -0,0 +1,25 @@ +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.GameData.Interop; + +namespace Glamourer.Interop.Material; + +public sealed class UpdateColorSets : FastHook +{ + public delegate void Delegate(Model model, uint unk); + + private readonly ThreadLocal _updatingModel = new(() => Model.Null); + + public UpdateColorSets(HookManager hooks) + => Task = hooks.CreateHook("Update Color Sets", Sigs.UpdateColorSets, Detour, true); + + private void Detour(Model model, uint unk) + { + _updatingModel.Value = model; + Task.Result.Original(model, unk); + _updatingModel.Value = Model.Null; + } + + public Model Get() + => _updatingModel.Value; +} diff --git a/Glamourer/Interop/MetaService.cs b/Glamourer/Interop/MetaService.cs index 7ed0948..6225986 100644 --- a/Glamourer/Interop/MetaService.cs +++ b/Glamourer/Interop/MetaService.cs @@ -1,9 +1,8 @@ -using System; -using Dalamud.Hooking; +using Dalamud.Hooking; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using Glamourer.Events; -using Glamourer.Interop.Structs; +using Penumbra.GameData.Interop; namespace Glamourer.Interop; @@ -14,7 +13,7 @@ public unsafe class MetaService : IDisposable private readonly VisorStateChanged _visorEvent; private delegate void HideHatGearDelegate(DrawDataContainer* drawData, uint id, byte value); - private delegate void HideWeaponsDelegate(DrawDataContainer* drawData, bool value); + private delegate void HideWeaponsDelegate(DrawDataContainer* drawData, byte value); private readonly Hook _hideHatGearHook; private readonly Hook _hideWeaponsHook; @@ -49,12 +48,12 @@ public unsafe class MetaService : IDisposable if (!actor.IsCharacter) return; - // The function seems to not do anything if the head is 0, sometimes? - var old = actor.AsCharacter->DrawData.Head.Id; - if (old == 0) - actor.AsCharacter->DrawData.Head.Id = 1; + // The function seems to not do anything if the head is 0, but also breaks for carbuncles turned human, sometimes? + var old = actor.AsCharacter->DrawData.Equipment(DrawDataContainer.EquipmentSlot.Head).Id; + if (old == 0 && actor.AsCharacter->ModelContainer.ModelCharaId == 0) + actor.AsCharacter->DrawData.Equipment(DrawDataContainer.EquipmentSlot.Head).Id = 1; _hideHatGearHook.Original(&actor.AsCharacter->DrawData, 0, (byte)(value ? 0 : 1)); - actor.AsCharacter->DrawData.Head.Id = old; + actor.AsCharacter->DrawData.Equipment(DrawDataContainer.EquipmentSlot.Head).Id = old; } public void SetWeaponState(Actor actor, bool value) @@ -62,7 +61,9 @@ public unsafe class MetaService : IDisposable if (!actor.IsCharacter) return; - _hideWeaponsHook.Original(&actor.AsCharacter->DrawData, !value); + var old = actor.AsCharacter->DrawData.IsWeaponHidden; + _hideWeaponsHook.Original(&actor.AsCharacter->DrawData, (byte)(value ? 0 : 1)); + actor.AsCharacter->DrawData.IsWeaponHidden = old; } private void HideHatDetour(DrawDataContainer* drawData, uint id, byte value) @@ -73,7 +74,7 @@ public unsafe class MetaService : IDisposable return; } - Actor actor = drawData->Parent; + Actor actor = drawData->OwnerObject; var v = value == 0; _headGearEvent.Invoke(actor, ref v); value = (byte)(v ? 0 : 1); @@ -81,21 +82,21 @@ public unsafe class MetaService : IDisposable _hideHatGearHook.Original(drawData, id, value); } - private void HideWeaponsDetour(DrawDataContainer* drawData, bool value) + private void HideWeaponsDetour(DrawDataContainer* drawData, byte value) { - Actor actor = drawData->Parent; - value = !value; - _weaponEvent.Invoke(actor, ref value); - value = !value; + Actor actor = drawData->OwnerObject; + var v = value == 0; + _weaponEvent.Invoke(actor, ref v); Glamourer.Log.Verbose($"[MetaService] Hide Weapon triggered with 0x{(nint)drawData:X} {value} for {actor.Utf8Name}."); - _hideWeaponsHook.Original(drawData, value); + _hideWeaponsHook.Original(drawData, (byte)(v ? 0 : 1)); } - private void ToggleVisorDetour(DrawDataContainer* drawData, bool value) + private void ToggleVisorDetour(DrawDataContainer* drawData, byte value) { - Actor actor = drawData->Parent; - _visorEvent.Invoke(actor.Model, ref value); + Actor actor = drawData->OwnerObject; + var v = value != 0; + _visorEvent.Invoke(actor.Model, true, ref v); Glamourer.Log.Verbose($"[MetaService] Toggle Visor triggered with 0x{(nint)drawData:X} {value} for {actor.Utf8Name}."); - _toggleVisorHook.Original(drawData, value); + _toggleVisorHook.Original(drawData, (byte)(v ? 1 : 0)); } } diff --git a/Glamourer/Interop/ObjectManager.cs b/Glamourer/Interop/ObjectManager.cs deleted file mode 100644 index 15caeee..0000000 --- a/Glamourer/Interop/ObjectManager.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Plugin.Services; -using Glamourer.Interop.Structs; -using Glamourer.Services; -using Penumbra.GameData.Actors; - -namespace Glamourer.Interop; - -public class ObjectManager : IReadOnlyDictionary -{ - private readonly IFramework _framework; - private readonly IClientState _clientState; - private readonly IObjectTable _objects; - private readonly ActorService _actors; - private readonly ITargetManager _targets; - - public IObjectTable Objects - => _objects; - - public ObjectManager(IFramework framework, IClientState clientState, IObjectTable objects, ActorService actors, ITargetManager targets) - { - _framework = framework; - _clientState = clientState; - _objects = objects; - _actors = actors; - _targets = targets; - } - - public DateTime LastUpdate { get; private set; } - - public bool IsInGPose { get; private set; } - public ushort World { get; private set; } - - private readonly Dictionary _identifiers = new(200); - private readonly Dictionary _allWorldIdentifiers = new(200); - private readonly Dictionary _nonOwnedIdentifiers = new(200); - - public IReadOnlyDictionary Identifiers - => _identifiers; - - public void Update() - { - var lastUpdate = _framework.LastUpdate; - if (lastUpdate <= LastUpdate) - return; - - LastUpdate = lastUpdate; - World = (ushort)(_clientState.LocalPlayer?.CurrentWorld.Id ?? 0u); - _identifiers.Clear(); - _allWorldIdentifiers.Clear(); - _nonOwnedIdentifiers.Clear(); - - for (var i = 0; i < (int)ScreenActor.CutsceneStart; ++i) - { - Actor character = _objects.GetObjectAddress(i); - if (character.Identifier(_actors.AwaitedService, out var identifier)) - HandleIdentifier(identifier, character); - } - - for (var i = (int)ScreenActor.CutsceneStart; i < (int)ScreenActor.CutsceneEnd; ++i) - { - Actor character = _objects.GetObjectAddress(i); - if (!character.Valid) - break; - - HandleIdentifier(character.GetIdentifier(_actors.AwaitedService), character); - } - - void AddSpecial(ScreenActor idx, string label) - { - Actor actor = _objects.GetObjectAddress((int)idx); - if (actor.Identifier(_actors.AwaitedService, out var ident)) - { - var data = new ActorData(actor, label); - _identifiers.Add(ident, data); - } - } - - AddSpecial(ScreenActor.CharacterScreen, "Character Screen Actor"); - AddSpecial(ScreenActor.ExamineScreen, "Examine Screen Actor"); - AddSpecial(ScreenActor.FittingRoom, "Fitting Room Actor"); - AddSpecial(ScreenActor.DyePreview, "Dye Preview Actor"); - AddSpecial(ScreenActor.Portrait, "Portrait Actor"); - AddSpecial(ScreenActor.Card6, "Card Actor 6"); - AddSpecial(ScreenActor.Card7, "Card Actor 7"); - AddSpecial(ScreenActor.Card8, "Card Actor 8"); - - for (var i = (int)ScreenActor.ScreenEnd; i < _objects.Length; ++i) - { - Actor character = _objects.GetObjectAddress(i); - if (character.Identifier(_actors.AwaitedService, out var identifier)) - HandleIdentifier(identifier, character); - } - - var gPose = GPosePlayer; - IsInGPose = gPose.Utf8Name.Length > 0; - } - - private void HandleIdentifier(ActorIdentifier identifier, Actor character) - { - if (!character.Model || !identifier.IsValid) - return; - - if (!_identifiers.TryGetValue(identifier, out var data)) - { - data = new ActorData(character, identifier.ToString()); - _identifiers[identifier] = data; - } - else - { - data.Objects.Add(character); - } - - if (identifier.Type is IdentifierType.Player or IdentifierType.Owned) - { - var allWorld = _actors.AwaitedService.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue, - identifier.Kind, - identifier.DataId); - - if (!_allWorldIdentifiers.TryGetValue(allWorld, out var allWorldData)) - { - allWorldData = new ActorData(character, allWorld.ToString()); - _allWorldIdentifiers[allWorld] = allWorldData; - } - else - { - allWorldData.Objects.Add(character); - } - } - - if (identifier.Type is IdentifierType.Owned) - { - var nonOwned = _actors.AwaitedService.CreateNpc(identifier.Kind, identifier.DataId); - if (!_nonOwnedIdentifiers.TryGetValue(nonOwned, out var nonOwnedData)) - { - nonOwnedData = new ActorData(character, nonOwned.ToString()); - _nonOwnedIdentifiers[nonOwned] = nonOwnedData; - } - else - { - nonOwnedData.Objects.Add(character); - } - } - } - - public Actor GPosePlayer - => _objects.GetObjectAddress((int)ScreenActor.GPosePlayer); - - public Actor Player - => _objects.GetObjectAddress(0); - - public Actor Target - => _targets.Target?.Address ?? nint.Zero; - - public Actor Focus - => _targets.FocusTarget?.Address ?? nint.Zero; - - public Actor MouseOver - => _targets.MouseOverTarget?.Address ?? nint.Zero; - - public (ActorIdentifier Identifier, ActorData Data) PlayerData - { - get - { - Update(); - return Player.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data) - ? (ident, data) - : (ident, ActorData.Invalid); - } - } - - public (ActorIdentifier Identifier, ActorData Data) TargetData - { - get - { - Update(); - return Target.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data) - ? (ident, data) - : (ident, ActorData.Invalid); - } - } - - public IEnumerator> GetEnumerator() - => Identifiers.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public int Count - => Identifiers.Count; - - /// Also handles All Worlds players and non-owned NPCs. - public bool ContainsKey(ActorIdentifier key) - => Identifiers.ContainsKey(key) || _allWorldIdentifiers.ContainsKey(key) || _nonOwnedIdentifiers.ContainsKey(key); - - public bool TryGetValue(ActorIdentifier key, out ActorData value) - => Identifiers.TryGetValue(key, out value); - - public bool TryGetValueAllWorld(ActorIdentifier key, out ActorData value) - => _allWorldIdentifiers.TryGetValue(key, out value); - - public bool TryGetValueNonOwned(ActorIdentifier key, out ActorData value) - => _nonOwnedIdentifiers.TryGetValue(key, out value); - - public ActorData this[ActorIdentifier key] - => Identifiers[key]; - - public IEnumerable Keys - => Identifiers.Keys; - - public IEnumerable Values - => Identifiers.Values; - - public bool GetName(string lowerName, out Actor actor) - { - (actor, var ret) = lowerName switch - { - "" => (Actor.Null, true), - "" => (Player, true), - "self" => (Player, true), - "" => (Target, true), - "target" => (Target, true), - "" => (Focus, true), - "focus" => (Focus, true), - "" => (MouseOver, true), - "mouseover" => (MouseOver, true), - _ => (Actor.Null, false), - }; - return ret; - } -} diff --git a/Glamourer/Interop/PalettePlus/PaletteImport.cs b/Glamourer/Interop/PalettePlus/PaletteImport.cs new file mode 100644 index 0000000..4887255 --- /dev/null +++ b/Glamourer/Interop/PalettePlus/PaletteImport.cs @@ -0,0 +1,162 @@ +using Dalamud.Plugin; +using Glamourer.Designs; +using Glamourer.GameData; +using Newtonsoft.Json.Linq; +using OtterGui.Services; + +namespace Glamourer.Interop.PalettePlus; + +public class PaletteImport(IDalamudPluginInterface pluginInterface, DesignManager designManager, DesignFileSystem designFileSystem) : IService +{ + private string ConfigFile + => Path.Combine(Path.GetDirectoryName(pluginInterface.GetPluginConfigDirectory())!, "PalettePlus.json"); + + private readonly Dictionary _data = []; + + public IReadOnlyDictionary Data + { + get + { + if (_data.Count > 0) + return _data; + + PopulateDict(); + return _data; + } + } + + public void ImportDesigns() + { + foreach (var (name, (palette, flags)) in Data) + { + var fullPath = $"PalettePlus/{name}"; + if (designFileSystem.Find(fullPath, out _)) + { + Glamourer.Log.Information($"Skipped adding palette {name} because {fullPath} already exists."); + continue; + } + + var design = designManager.CreateEmpty(fullPath, true); + design.Application = ApplicationCollection.None; + foreach (var flag in flags.Iterate()) + { + designManager.ChangeApplyParameter(design, flag, true); + designManager.ChangeCustomizeParameter(design, flag, palette[flag]); + } + + Glamourer.Log.Information($"Added design for palette {name} at {fullPath}."); + } + } + + private void PopulateDict() + { + var path = ConfigFile; + if (!File.Exists(path)) + return; + + try + { + var text = File.ReadAllText(path); + var obj = JObject.Parse(text); + var palettes = obj["SavedPalettes"]; + if (palettes == null) + return; + + foreach (var child in palettes.Children()) + { + var name = child["Name"]?.ToObject() ?? string.Empty; + if (name.Length == 0) + continue; + + var conditions = child["Conditions"]?.ToObject() ?? 0; + var parameters = child["ShaderParams"]; + if (parameters == null) + continue; + + var orig = name; + var counter = 1; + while (_data.ContainsKey(name)) + name = $"{orig} #{++counter}"; + + var data = new CustomizeParameterData(); + CustomizeParameterFlag flags = 0; + var discard = 0f; + Parse("SkinTone", CustomizeParameterFlag.SkinDiffuse, + ref data.SkinDiffuse.X, ref data.SkinDiffuse.Y, ref data.SkinDiffuse.Z, ref discard); + Parse("SkinGloss", CustomizeParameterFlag.SkinSpecular, + ref data.SkinSpecular.X, ref data.SkinSpecular.Y, ref data.SkinSpecular.Z, ref discard); + Parse("LipColor", CustomizeParameterFlag.LipDiffuse, + ref data.LipDiffuse.X, ref data.LipDiffuse.Y, ref data.LipDiffuse.Z, ref data.LipDiffuse.W); + Parse("HairColor", CustomizeParameterFlag.HairDiffuse, + ref data.HairDiffuse.X, ref data.HairDiffuse.Y, ref data.HairDiffuse.Z, ref discard); + Parse("HairShine", CustomizeParameterFlag.HairSpecular, + ref data.HairSpecular.X, ref data.HairSpecular.Y, ref data.HairSpecular.Z, ref discard); + Parse("LeftEyeColor", CustomizeParameterFlag.LeftEye, + ref data.LeftEye.X, ref data.LeftEye.Y, ref data.LeftEye.Z, ref discard); + Parse("RaceFeatureColor", CustomizeParameterFlag.FeatureColor, + ref data.FeatureColor.X, ref data.FeatureColor.Y, ref data.FeatureColor.Z, ref discard); + Parse("FacePaintColor", CustomizeParameterFlag.DecalColor, + ref data.DecalColor.X, ref data.DecalColor.Y, ref data.DecalColor.Z, ref data.DecalColor.W); + // Highlights is flag 2. + if ((conditions & 2) == 2) + Parse("HighlightsColor", CustomizeParameterFlag.HairHighlight, + ref data.HairHighlight.X, ref data.HairHighlight.Y, ref data.HairHighlight.Z, ref discard); + // Heterochromia is flag 1 + if ((conditions & 1) == 1) + { + Parse("RightEyeColor", CustomizeParameterFlag.RightEye, + ref data.RightEye.X, ref data.RightEye.Y, ref data.RightEye.Z, ref discard); + } + else if (flags.HasFlag(CustomizeParameterFlag.LeftEye)) + { + data.RightEye = data.LeftEye; + flags |= CustomizeParameterFlag.RightEye; + } + + ParseSingle("FacePaintOffset", CustomizeParameterFlag.FacePaintUvOffset, ref data.FacePaintUvOffset); + ParseSingle("FacePaintWidth", CustomizeParameterFlag.FacePaintUvMultiplier, ref data.FacePaintUvMultiplier); + ParseSingle("MuscleTone", CustomizeParameterFlag.MuscleTone, ref data.MuscleTone); + + while (!_data.TryAdd(name, (data, flags))) + name = $"{orig} ({++counter})"; + continue; + + + void Parse(string attribute, CustomizeParameterFlag flag, ref float x, ref float y, ref float z, ref float w) + { + var node = parameters![attribute]; + if (node == null) + return; + + flags |= flag; + var xVal = node["X"]?.ToObject(); + var yVal = node["Y"]?.ToObject(); + var zVal = node["Z"]?.ToObject(); + var wVal = node["W"]?.ToObject(); + if (xVal.HasValue) + x = xVal.Value > 0 ? MathF.Sqrt(xVal.Value) : -MathF.Sqrt(-xVal.Value); + if (yVal.HasValue) + y = yVal.Value > 0 ? MathF.Sqrt(yVal.Value) : -MathF.Sqrt(-yVal.Value); + if (zVal.HasValue) + z = zVal.Value > 0 ? MathF.Sqrt(zVal.Value) : -MathF.Sqrt(-zVal.Value); + if (wVal.HasValue) + w = wVal.Value; + } + + void ParseSingle(string attribute, CustomizeParameterFlag flag, ref float value) + { + var node = parameters![attribute]?.ToObject(); + if (!node.HasValue) + return; + + value = node.Value; + flags |= flag; + } + } + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not read Palette+ configuration:\n{ex}"); + } + } +} diff --git a/Glamourer/Interop/Penumbra/ModSettingApplier.cs b/Glamourer/Interop/Penumbra/ModSettingApplier.cs new file mode 100644 index 0000000..b94be09 --- /dev/null +++ b/Glamourer/Interop/Penumbra/ModSettingApplier.cs @@ -0,0 +1,95 @@ +using Glamourer.Designs.Links; +using Glamourer.Services; +using Glamourer.State; +using OtterGui.Services; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop.Penumbra; + +public class ModSettingApplier(PenumbraService penumbra, PenumbraAutoRedrawSkip autoRedrawSkip, Configuration config, ActorObjectManager objects, CollectionOverrideService overrides) + : IService +{ + private readonly HashSet _collectionTracker = []; + + public void HandleStateApplication(ActorState state, MergedDesign design, StateSource source, bool skipAutoRedraw, bool respectManual) + { + if (!config.AlwaysApplyAssociatedMods || (design.AssociatedMods.Count == 0 && !design.ResetTemporarySettings)) + return; + + if (!objects.TryGetValue(state.Identifier, out var data)) + { + Glamourer.Log.Verbose( + $"[Mod Applier] No mod settings applied because no actor for {state.Identifier.Incognito(null)} could be found to associate collection."); + return; + } + + _collectionTracker.Clear(); + using var skip = autoRedrawSkip.SkipAutoUpdates(skipAutoRedraw); + foreach (var actor in data.Objects) + { + var (collection, _, overridden) = overrides.GetCollection(actor, state.Identifier); + if (collection == Guid.Empty) + continue; + + if (!_collectionTracker.Add(collection)) + continue; + + var index = ResetOldSettings(collection, actor, source, design.ResetTemporarySettings, respectManual); + foreach (var (mod, setting) in design.AssociatedMods) + { + var message = penumbra.SetMod(mod, setting, source, respectManual, collection, index); + if (message.Length > 0) + Glamourer.Log.Verbose($"[Mod Applier] Error applying mod settings: {message}"); + else + Glamourer.Log.Verbose( + $"[Mod Applier] Set mod settings for {mod.DirectoryName} in {collection}{(overridden ? " (overridden by settings)" : string.Empty)}."); + } + } + } + + public (List Messages, int Applied, Guid Collection, string Name, bool Overridden) ApplyModSettings( + IReadOnlyDictionary settings, Actor actor, StateSource source, bool resetOther) + { + var (collection, name, overridden) = overrides.GetCollection(actor); + if (collection == Guid.Empty) + return ([$"{actor.Utf8Name} uses no mods."], 0, Guid.Empty, string.Empty, false); + + var messages = new List(); + var appliedMods = 0; + + var index = ResetOldSettings(collection, actor, source, resetOther, true); + foreach (var (mod, setting) in settings) + { + var message = penumbra.SetMod(mod, setting, source, false, collection, index); + if (message.Length > 0) + messages.Add($"Error applying mod settings: {message}"); + else + ++appliedMods; + } + + return (messages, appliedMods, collection, name, overridden); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ObjectIndex? ResetOldSettings(Guid collection, Actor actor, StateSource source, bool resetOther, bool respectManual) + { + ObjectIndex? index = actor.Valid ? actor.Index : null; + if (!resetOther) + return index; + + if (index == null) + { + penumbra.RemoveAllTemporarySettings(collection, source); + if (!respectManual && source.IsFixed()) + penumbra.RemoveAllTemporarySettings(collection, StateSource.Manual); + } + else + { + penumbra.RemoveAllTemporarySettings(index.Value, source); + if (!respectManual && source.IsFixed()) + penumbra.RemoveAllTemporarySettings(index.Value, StateSource.Manual); + } + return index; + } +} diff --git a/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs b/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs index ce4f99e..4e3c8e3 100644 --- a/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs +++ b/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs @@ -1,67 +1,120 @@ -using System; +using Dalamud.Plugin.Services; +using Glamourer.Api.Enums; +using Glamourer.Designs.History; +using Glamourer.Events; using Glamourer.State; +using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Api.Enums; +using Penumbra.GameData.Interop; namespace Glamourer.Interop.Penumbra; -public class PenumbraAutoRedraw : IDisposable +public class PenumbraAutoRedraw : IDisposable, IRequiredService { - private readonly Configuration _config; - private readonly PenumbraService _penumbra; - private readonly StateManager _state; - private readonly ObjectManager _objects; - private bool _enabled; + private const int WaitFrames = 5; + private readonly Configuration _config; + private readonly PenumbraService _penumbra; + private readonly StateManager _state; + private readonly ActorObjectManager _objects; + private readonly IFramework _framework; + private readonly StateChanged _stateChanged; + private readonly PenumbraAutoRedrawSkip _skip; - public PenumbraAutoRedraw(PenumbraService penumbra, Configuration config, StateManager state, ObjectManager objects) + + public PenumbraAutoRedraw(PenumbraService penumbra, Configuration config, StateManager state, ActorObjectManager objects, + IFramework framework, + StateChanged stateChanged, PenumbraAutoRedrawSkip skip) { - _penumbra = penumbra; - _config = config; - _state = state; - _objects = objects; - if (_config.AutoRedrawEquipOnChanges) - Enable(); - } - - public void SetState(bool value) - { - if (value == _config.AutoRedrawEquipOnChanges) - return; - - _config.AutoRedrawEquipOnChanges = value; - _config.Save(); - if (value) - Enable(); - else - Disable(); - } - - public void Enable() - { - if (_enabled) - return; - + _penumbra = penumbra; + _config = config; + _state = state; + _objects = objects; + _framework = framework; + _stateChanged = stateChanged; + _skip = skip; _penumbra.ModSettingChanged += OnModSettingChange; - _enabled = true; - } - - public void Disable() - { - if (!_enabled) - return; - - _penumbra.ModSettingChanged -= OnModSettingChange; - _enabled = false; + _framework.Update += OnFramework; + _stateChanged.Subscribe(OnStateChanged, StateChanged.Priority.PenumbraAutoRedraw); } public void Dispose() { - Disable(); + _penumbra.ModSettingChanged -= OnModSettingChange; + _framework.Update -= OnFramework; + _stateChanged.Unsubscribe(OnStateChanged); } - private void OnModSettingChange(ModSettingChange type, string name, string mod, bool inherited) + private readonly ConcurrentQueue<(ActorState, Action, int)> _actions = []; + private readonly ConcurrentSet _skips = []; + private DateTime _frame; + + private void OnStateChanged(StateChangeType type, StateSource source, ActorState state, ActorData _1, ITransaction? _2) { - var playerName = _penumbra.GetCurrentPlayerCollection(); - if (playerName == name) - _state.ReapplyState(_objects.Player); + if (type is StateChangeType.Design && source.IsIpc()) + _skips.TryAdd(state); + } + + private void OnFramework(IFramework _) + { + var count = _actions.Count; + while (_actions.TryDequeue(out var tuple) && count-- > 0) + { + if (_skips.ContainsKey(tuple.Item1)) + { + _skips.TryRemove(tuple.Item1); + continue; + } + + if (tuple.Item3 > 0) + _actions.Enqueue((tuple.Item1, tuple.Item2, tuple.Item3 - 1)); + else + tuple.Item2(); + } + } + + private void OnModSettingChange(ModSettingChange type, Guid collectionId, string mod, bool inherited) + { + if (type is ModSettingChange.TemporaryMod) + { + _framework.RunOnFrameworkThread(() => + { + foreach (var (id, state) in _state) + { + if (!_objects.TryGetValue(id, out var actors) || !actors.Valid) + continue; + + var collection = _penumbra.GetActorCollection(actors.Objects[0], out _); + if (collection != collectionId) + continue; + + _actions.Enqueue((state, () => + { + foreach (var actor in actors.Objects) + _state.ReapplyState(actor, state, false, StateSource.IpcManual, true); + Glamourer.Log.Debug($"Automatically applied mod settings of type {type} to {id.Incognito(null)}."); + }, WaitFrames)); + } + }); + } + else if (_config.AutoRedrawEquipOnChanges && !_skip.Skip) + { + // Only update once per frame. + var playerName = _penumbra.GetCurrentPlayerCollection(); + if (playerName != collectionId) + return; + + var currentFrame = _framework.LastUpdateUTC; + if (currentFrame == _frame) + return; + + _frame = currentFrame; + _framework.RunOnFrameworkThread(() => + { + _state.ReapplyState(_objects.Player, false, StateSource.IpcManual, true); + Glamourer.Log.Debug( + $"Automatically applied mod settings of type {type} to {_objects.PlayerData.Identifier.Incognito(null)} (Local Player)."); + }); + } } } diff --git a/Glamourer/Interop/Penumbra/PenumbraAutoRedrawSkip.cs b/Glamourer/Interop/Penumbra/PenumbraAutoRedrawSkip.cs new file mode 100644 index 0000000..8ef522c --- /dev/null +++ b/Glamourer/Interop/Penumbra/PenumbraAutoRedrawSkip.cs @@ -0,0 +1,15 @@ +using OtterGui.Classes; +using OtterGui.Services; + +namespace Glamourer.Interop.Penumbra; + +public class PenumbraAutoRedrawSkip : IService +{ + private bool _skipAutoUpdates; + + public BoolSetter SkipAutoUpdates(bool skip) + => new(ref _skipAutoUpdates, skip); + + public bool Skip + => _skipAutoUpdates; +} diff --git a/Glamourer/Interop/Penumbra/PenumbraService.cs b/Glamourer/Interop/Penumbra/PenumbraService.cs index 4515b7c..b2813cd 100644 --- a/Glamourer/Interop/Penumbra/PenumbraService.cs +++ b/Glamourer/Interop/Penumbra/PenumbraService.cs @@ -1,23 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using Dalamud.Interface.Internal.Notifications; -using Dalamud.Logging; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin; +using Dalamud.Plugin.Ipc.Exceptions; using Glamourer.Events; -using Glamourer.Interop.Structs; +using Glamourer.State; +using Newtonsoft.Json.Linq; using OtterGui.Classes; -using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.Interop.Penumbra; -using CurrentSettings = ValueTuple>, bool)?>; - public readonly record struct Mod(string Name, string DirectoryName) : IComparable { public int CompareTo(Mod other) @@ -30,58 +24,91 @@ public readonly record struct Mod(string Name, string DirectoryName) : IComparab } } -public readonly record struct ModSettings(IDictionary> Settings, int Priority, bool Enabled) +public readonly record struct ModSettings(Dictionary> Settings, int Priority, bool Enabled, bool ForceInherit, bool Remove) { public ModSettings() - : this(new Dictionary>(), 0, false) + : this(new Dictionary>(), 0, false, false, false) { } public static ModSettings Empty => new(); } -public unsafe class PenumbraService : IDisposable +public class PenumbraService : IDisposable { - public const int RequiredPenumbraBreakingVersion = 4; - public const int RequiredPenumbraFeatureVersion = 15; + public const int RequiredPenumbraBreakingVersion = 5; + public const int RequiredPenumbraFeatureVersion = 13; - private readonly DalamudPluginInterface _pluginInterface; - private readonly EventSubscriber _tooltipSubscriber; - private readonly EventSubscriber _clickSubscriber; - private readonly EventSubscriber _creatingCharacterBase; - private readonly EventSubscriber _createdCharacterBase; - private readonly EventSubscriber _modSettingChanged; - private ActionSubscriber _redrawSubscriber; - private FuncSubscriber _drawObjectInfo; - private FuncSubscriber _cutsceneParent; - private FuncSubscriber _objectCollection; - private FuncSubscriber> _getMods; - private FuncSubscriber _currentCollection; - private FuncSubscriber _getCurrentSettings; - private FuncSubscriber _setMod; - private FuncSubscriber _setModPriority; - private FuncSubscriber _setModSetting; - private FuncSubscriber, PenumbraApiEc> _setModSettings; - private FuncSubscriber _openModPage; + private const int KeyFixed = -1610; + private const string NameFixed = "Glamourer (Automation)"; + private const int KeyManual = -6160; + private const string NameManual = "Glamourer (Manually)"; - private readonly EventSubscriber _initializedEvent; - private readonly EventSubscriber _disposedEvent; + private readonly IDalamudPluginInterface _pluginInterface; + private readonly Configuration _config; + private readonly EventSubscriber _tooltipSubscriber; + private readonly EventSubscriber _clickSubscriber; + private readonly EventSubscriber _creatingCharacterBase; + private readonly EventSubscriber _createdCharacterBase; + private readonly EventSubscriber _modSettingChanged; + private readonly EventSubscriber _pcpParsed; + private readonly EventSubscriber _pcpCreated; + + private global::Penumbra.Api.IpcSubscribers.GetCollectionsByIdentifier? _collectionByIdentifier; + private global::Penumbra.Api.IpcSubscribers.GetCollections? _collections; + private global::Penumbra.Api.IpcSubscribers.RedrawObject? _redraw; + private global::Penumbra.Api.IpcSubscribers.GetCollectionForObject? _objectCollection; + private global::Penumbra.Api.IpcSubscribers.GetModList? _getMods; + private global::Penumbra.Api.IpcSubscribers.GetCollection? _currentCollection; + private global::Penumbra.Api.IpcSubscribers.GetCurrentModSettingsWithTemp? _getCurrentSettingsWithTemp; + private global::Penumbra.Api.IpcSubscribers.GetCurrentModSettings? _getCurrentSettings; + private global::Penumbra.Api.IpcSubscribers.GetAllModSettings? _getAllSettings; + private global::Penumbra.Api.IpcSubscribers.TryInheritMod? _inheritMod; + private global::Penumbra.Api.IpcSubscribers.TrySetMod? _setMod; + private global::Penumbra.Api.IpcSubscribers.TrySetModPriority? _setModPriority; + private global::Penumbra.Api.IpcSubscribers.TrySetModSetting? _setModSetting; + private global::Penumbra.Api.IpcSubscribers.TrySetModSettings? _setModSettings; + private global::Penumbra.Api.IpcSubscribers.SetTemporaryModSettings? _setTemporaryModSettings; + private global::Penumbra.Api.IpcSubscribers.SetTemporaryModSettingsPlayer? _setTemporaryModSettingsPlayer; + private global::Penumbra.Api.IpcSubscribers.RemoveTemporaryModSettings? _removeTemporaryModSettings; + private global::Penumbra.Api.IpcSubscribers.RemoveTemporaryModSettingsPlayer? _removeTemporaryModSettingsPlayer; + private global::Penumbra.Api.IpcSubscribers.RemoveAllTemporaryModSettings? _removeAllTemporaryModSettings; + private global::Penumbra.Api.IpcSubscribers.RemoveAllTemporaryModSettingsPlayer? _removeAllTemporaryModSettingsPlayer; + private global::Penumbra.Api.IpcSubscribers.QueryTemporaryModSettings? _queryTemporaryModSettings; + private global::Penumbra.Api.IpcSubscribers.QueryTemporaryModSettingsPlayer? _queryTemporaryModSettingsPlayer; + private global::Penumbra.Api.IpcSubscribers.OpenMainWindow? _openModPage; + private global::Penumbra.Api.IpcSubscribers.GetChangedItems? _getChangedItems; + private global::Penumbra.Api.IpcSubscribers.RegisterSettingsSection? _registerSettingsSection; + private global::Penumbra.Api.IpcSubscribers.UnregisterSettingsSection? _unregisterSettingsSection; + private IReadOnlyList<(string ModDirectory, IReadOnlyDictionary ChangedItems)>? _changedItems; + private Func? _checkCurrentChangedItems; + private Func? _checkCutsceneParent; + private Func? _getGameObject; + + private readonly IDisposable _initializedEvent; + private readonly IDisposable _disposedEvent; private readonly PenumbraReloaded _penumbraReloaded; - public bool Available { get; private set; } + public bool Available { get; private set; } + public int CurrentMajor { get; private set; } + public int CurrentMinor { get; private set; } + public DateTime AttachTime { get; private set; } - public PenumbraService(DalamudPluginInterface pi, PenumbraReloaded penumbraReloaded) + public PenumbraService(IDalamudPluginInterface pi, PenumbraReloaded penumbraReloaded, Configuration config) { _pluginInterface = pi; _penumbraReloaded = penumbraReloaded; - _initializedEvent = Ipc.Initialized.Subscriber(pi, Reattach); - _disposedEvent = Ipc.Disposed.Subscriber(pi, Unattach); - _tooltipSubscriber = Ipc.ChangedItemTooltip.Subscriber(pi); - _clickSubscriber = Ipc.ChangedItemClick.Subscriber(pi); - _createdCharacterBase = Ipc.CreatedCharacterBase.Subscriber(pi); - _creatingCharacterBase = Ipc.CreatingCharacterBase.Subscriber(pi); - _modSettingChanged = Ipc.ModSettingChanged.Subscriber(pi); + _config = config; + _initializedEvent = global::Penumbra.Api.IpcSubscribers.Initialized.Subscriber(pi, Reattach); + _disposedEvent = global::Penumbra.Api.IpcSubscribers.Disposed.Subscriber(pi, Unattach); + _tooltipSubscriber = global::Penumbra.Api.IpcSubscribers.ChangedItemTooltip.Subscriber(pi); + _clickSubscriber = global::Penumbra.Api.IpcSubscribers.ChangedItemClicked.Subscriber(pi); + _createdCharacterBase = global::Penumbra.Api.IpcSubscribers.CreatedCharacterBase.Subscriber(pi); + _creatingCharacterBase = global::Penumbra.Api.IpcSubscribers.CreatingCharacterBase.Subscriber(pi); + _modSettingChanged = global::Penumbra.Api.IpcSubscribers.ModSettingChanged.Subscriber(pi); + _pcpCreated = global::Penumbra.Api.IpcSubscribers.CreatingPcp.Subscriber(pi); + _pcpParsed = global::Penumbra.Api.IpcSubscribers.ParsingPcp.Subscriber(pi); Reattach(); } @@ -98,67 +125,185 @@ public unsafe class PenumbraService : IDisposable } - public event Action CreatingCharacterBase + public event Action CreatingCharacterBase { add => _creatingCharacterBase.Event += value; remove => _creatingCharacterBase.Event -= value; } - public event Action CreatedCharacterBase + public event Action CreatedCharacterBase { add => _createdCharacterBase.Event += value; remove => _createdCharacterBase.Event -= value; } - public event Action ModSettingChanged + public event Action ModSettingChanged { add => _modSettingChanged.Event += value; remove => _modSettingChanged.Event -= value; } - public IReadOnlyList<(Mod Mod, ModSettings Settings)> GetMods() + public event Action PcpCreated { + add => _pcpCreated.Event += value; + remove => _pcpCreated.Event -= value; + } + + public event Action PcpParsed + { + add => _pcpParsed.Event += value; + remove => _pcpParsed.Event -= value; + } + + public event Action? DrawSettingsSection; + + private void InvokeDrawSettingsSection() + => DrawSettingsSection?.Invoke(); + + public Dictionary GetCollections() + => Available ? _collections!.Invoke() : []; + + public ModSettings GetModSettings(in Mod mod, out string source) + { + source = string.Empty; if (!Available) - return Array.Empty<(Mod Mod, ModSettings Settings)>(); + return ModSettings.Empty; try { - var allMods = _getMods.Invoke(); - var collection = _currentCollection.Invoke(ApiCollectionType.Current); - return allMods - .Select(m => (m.Item1, m.Item2, _getCurrentSettings.Invoke(collection, m.Item1, m.Item2, true))) - .Where(t => t.Item3.Item1 is PenumbraApiEc.Success) - .Select(t => (new Mod(t.Item2, t.Item1), - !t.Item3.Item2.HasValue - ? ModSettings.Empty - : new ModSettings(t.Item3.Item2!.Value.Item3, t.Item3.Item2!.Value.Item2, t.Item3.Item2!.Value.Item1))) - .OrderByDescending(p => p.Item2.Enabled) - .ThenBy(p => p.Item1.Name) - .ThenBy(p => p.Item1.DirectoryName) - .ThenByDescending(p => p.Item2.Priority) - .ToList(); + var collection = _currentCollection!.Invoke(ApiCollectionType.Current); + return GetSettings(collection!.Value.Id, mod.DirectoryName, mod.Name, out source); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Error fetching mod settings for {mod.DirectoryName} from Penumbra:\n{ex}"); + return ModSettings.Empty; + } + } + + private ModSettings GetSettings(Guid collection, string modDirectory, string modName, out string source) + { + if (_getCurrentSettingsWithTemp != null) + { + source = string.Empty; + var (ec, tuple) = _getCurrentSettingsWithTemp!.Invoke(collection, modDirectory, modName, false, false, KeyFixed); + if (ec is not PenumbraApiEc.Success) + return ModSettings.Empty; + + return tuple.HasValue + ? new ModSettings(tuple.Value.Item3, tuple.Value.Item2, tuple.Value.Item1, false, false) + : ModSettings.Empty; + } + + if (_queryTemporaryModSettings != null) + { + var tempEc = _queryTemporaryModSettings.Invoke(collection, modDirectory, out var tempTuple, out source, 0, modName); + if (tempEc is PenumbraApiEc.Success && tempTuple != null) + return new ModSettings(tempTuple.Value.Settings, tempTuple.Value.Priority, tempTuple.Value.Enabled, + tempTuple.Value.ForceInherit, false); + } + + source = string.Empty; + var (ec2, tuple2) = _getCurrentSettings!.Invoke(collection, modDirectory, modName); + if (ec2 is not PenumbraApiEc.Success) + return ModSettings.Empty; + + return tuple2.HasValue + ? new ModSettings(tuple2.Value.Item3, tuple2.Value.Item2, tuple2.Value.Item1, false, false) + : ModSettings.Empty; + } + + public (Guid Id, string Name)? CollectionByIdentifier(string identifier) + { + if (!Available) + return null; + + var ret = _collectionByIdentifier!.Invoke(identifier); + if (ret.Count == 0) + return null; + + return ret[0]; + } + + public IReadOnlyList<(Mod Mod, ModSettings Settings, int Count)> GetMods(IReadOnlyList data) + { + if (!Available) + return []; + + try + { + var allMods = _getMods!.Invoke(); + var currentCollection = _currentCollection!.Invoke(ApiCollectionType.Current); + var withSettings = WithSettings(allMods, currentCollection!.Value.Id); + var withCounts = WithCounts(withSettings, allMods.Count); + return OrderList(withCounts, allMods.Count); + + IEnumerable<(Mod Mod, ModSettings Settings)> WithSettings(Dictionary mods, Guid collection) + { + if (_getAllSettings != null) + { + var allSettings = _getAllSettings.Invoke(collection, false, false, KeyFixed); + if (allSettings.Item1 is PenumbraApiEc.Success) + return mods.Select(m => (new Mod(m.Value, m.Key), + allSettings.Item2!.TryGetValue(m.Key, out var s) + ? new ModSettings(s.Item3, s.Item2, s.Item1, s is { Item4: true, Item5: true }, false) + : ModSettings.Empty)); + } + + return mods.Select(m => (new Mod(m.Value, m.Key), GetSettings(collection, m.Key, m.Value, out _))); + } + + IEnumerable<(Mod Mod, ModSettings Settings, int Count)> WithCounts(IEnumerable<(Mod Mod, ModSettings Settings)> mods, int count) + { + if (_changedItems != null && _changedItems.Count == count) + return mods.Select((m, idx) => (m.Mod, m.Settings, CountItems(_changedItems[idx].ChangedItems, data))); + + return mods.Select(p => (p.Item1, p.Item2, CountItems(_getChangedItems!.Invoke(p.Item1.DirectoryName, p.Item1.Name), data))); + + static int CountItems(IReadOnlyDictionary dict, IReadOnlyList data) + => data.Count(dict.ContainsKey); + } + + static IReadOnlyList<(Mod Mod, ModSettings Settings, int Count)> OrderList( + IEnumerable<(Mod Mod, ModSettings Settings, int Count)> enumerable, int count) + { + var array = new (Mod Mod, ModSettings Settings, int Count)[count]; + var i = 0; + foreach (var t in enumerable.OrderByDescending(p => p.Item2.Enabled) + .ThenByDescending(p => p.Item3) + .ThenBy(p => p.Item1.Name) + .ThenBy(p => p.Item1.DirectoryName) + .ThenByDescending(p => p.Item2.Priority)) + array[i++] = t; + return array; + } } catch (Exception ex) { Glamourer.Log.Error($"Error fetching mods from Penumbra:\n{ex}"); - return Array.Empty<(Mod Mod, ModSettings Settings)>(); + return []; } } public void OpenModPage(Mod mod) { - if (_openModPage.Invoke(TabType.Mods, mod.DirectoryName, mod.Name) == PenumbraApiEc.ModMissing) - Glamourer.Messager.NotificationMessage($"Could not open the mod {mod.Name}, no fitting mod was found in your Penumbra install.", NotificationType.Info, false); + if (!Available) + return; + + if (_openModPage!.Invoke(TabType.Mods, mod.DirectoryName, mod.Name) == PenumbraApiEc.ModMissing) + Glamourer.Messager.NotificationMessage($"Could not open the mod {mod.Name}, no fitting mod was found in your Penumbra install.", + NotificationType.Info, false); } - public string CurrentCollection - => Available ? _currentCollection.Invoke(ApiCollectionType.Current) : ""; + public (Guid Id, string Name) CurrentCollection + => Available ? _currentCollection!.Invoke(ApiCollectionType.Current)!.Value : (Guid.Empty, ""); /// /// Try to set all mod settings as desired. Only sets when the mod should be enabled. /// If it is disabled, ignore all other settings. /// - public string SetMod(Mod mod, ModSettings settings) + public string SetMod(Mod mod, ModSettings settings, StateSource source, bool respectManual, Guid? collectionInput = null, + ObjectIndex? index = null) { if (!Available) return "Penumbra is not available."; @@ -166,37 +311,11 @@ public unsafe class PenumbraService : IDisposable var sb = new StringBuilder(); try { - var collection = _currentCollection.Invoke(ApiCollectionType.Current); - var ec = _setMod.Invoke(collection, mod.DirectoryName, mod.Name, settings.Enabled); - if (ec is PenumbraApiEc.ModMissing) - return $"The mod {mod.Name} [{mod.DirectoryName}] could not be found."; - - Debug.Assert(ec is not PenumbraApiEc.CollectionMissing, "Missing collection should not be possible."); - - if (!settings.Enabled) - return string.Empty; - - ec = _setModPriority.Invoke(collection, mod.DirectoryName, mod.Name, settings.Priority); - Debug.Assert(ec is PenumbraApiEc.Success or PenumbraApiEc.NothingChanged, "Setting Priority should not be able to fail."); - - foreach (var (setting, list) in settings.Settings) - { - ec = list.Count == 1 - ? _setModSetting.Invoke(collection, mod.DirectoryName, mod.Name, setting, list[0]) - : _setModSettings.Invoke(collection, mod.DirectoryName, mod.Name, setting, (IReadOnlyList)list); - switch (ec) - { - case PenumbraApiEc.OptionGroupMissing: - sb.AppendLine($"Could not find the option group {setting} in mod {mod.Name}."); - break; - case PenumbraApiEc.OptionMissing: - sb.AppendLine($"Could not find all desired options in the option group {setting} in mod {mod.Name}."); - break; - } - - Debug.Assert(ec is PenumbraApiEc.Success or PenumbraApiEc.NothingChanged, - "Missing Mod or Collection should not be possible here."); - } + var collection = collectionInput ?? _currentCollection!.Invoke(ApiCollectionType.Current)!.Value.Id; + if (_config.UseTemporarySettings && _setTemporaryModSettings != null) + SetModTemporary(sb, mod, settings, collection, respectManual, index, source); + else + SetModPermanent(sb, mod, settings, collection); return sb.ToString(); } @@ -206,46 +325,179 @@ public unsafe class PenumbraService : IDisposable } } + public void RemoveAllTemporarySettings(Guid collection, StateSource source) + => _removeAllTemporaryModSettings?.Invoke(collection, source.IsFixed() ? KeyFixed : KeyManual); + + public void RemoveAllTemporarySettings(ObjectIndex index, StateSource source) + => _removeAllTemporaryModSettingsPlayer?.Invoke(index.Index, source.IsFixed() ? KeyFixed : KeyManual); + + public void ClearAllTemporarySettings(bool fix, bool manual) + { + if (!Available || _removeAllTemporaryModSettings == null) + return; + + var collections = _collections!.Invoke(); + foreach (var collection in collections) + { + if (fix) + RemoveAllTemporarySettings(collection.Key, StateSource.Fixed); + if (manual) + RemoveAllTemporarySettings(collection.Key, StateSource.Manual); + } + } + + public (string ModDirectory, string ModName)[] CheckCurrentChangedItem(string changedItem) + => _checkCurrentChangedItems?.Invoke(changedItem) ?? []; + + private void SetModTemporary(StringBuilder sb, Mod mod, ModSettings settings, Guid collection, bool respectManual, ObjectIndex? index, + StateSource source) + { + var (key, name) = source.IsFixed() ? (KeyFixed, NameFixed) : (KeyManual, NameManual); + // Check for existing manual settings and do not apply fixed on top of them if respecting manual changes. + if (key is KeyFixed && respectManual) + { + var existingSource = string.Empty; + var ec = index.HasValue + ? _queryTemporaryModSettingsPlayer?.Invoke(index.Value.Index, mod.DirectoryName, out _, + out existingSource, key, mod.Name) + ?? PenumbraApiEc.InvalidArgument + : _queryTemporaryModSettings?.Invoke(collection, mod.DirectoryName, out _, + out existingSource, key, mod.Name) + ?? PenumbraApiEc.InvalidArgument; + if (ec is PenumbraApiEc.Success && existingSource is NameManual) + { + Glamourer.Log.Debug( + $"Skipped applying mod settings for [{mod.Name}] through automation because manual settings from Glamourer existed."); + return; + } + } + + var ex = settings.Remove + ? index.HasValue + ? _removeTemporaryModSettingsPlayer!.Invoke(index.Value.Index, mod.DirectoryName, key, mod.Name) + : _removeTemporaryModSettings!.Invoke(collection, mod.DirectoryName, key, mod.Name) + : index.HasValue + ? _setTemporaryModSettingsPlayer!.Invoke(index.Value.Index, mod.DirectoryName, settings.ForceInherit, settings.Enabled, + settings.Priority, + settings.Settings.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value), name, key, mod.Name) + : _setTemporaryModSettings!.Invoke(collection, mod.DirectoryName, settings.ForceInherit, settings.Enabled, settings.Priority, + settings.Settings.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value), name, key, mod.Name); + switch (ex) + { + case PenumbraApiEc.InvalidArgument: + sb.Append($"No actor with index {index!.Value.Index} could be identified."); + return; + case PenumbraApiEc.ModMissing: + sb.Append($"The mod {mod.Name} [{mod.DirectoryName}] could not be found."); + return; + case PenumbraApiEc.CollectionMissing: + sb.Append($"The collection {collection} could not be found."); + return; + case PenumbraApiEc.TemporarySettingImpossible: + sb.Append($"The collection {collection} can not have settings."); + return; + case PenumbraApiEc.TemporarySettingDisallowed: + sb.Append($"The mod {mod.Name} [{mod.DirectoryName}] already has temporary settings with a different key in {collection}."); + return; + case PenumbraApiEc.OptionGroupMissing: + case PenumbraApiEc.OptionMissing: + sb.Append($"The provided settings for {mod.Name} [{mod.DirectoryName}] did not correspond to its actual options."); + return; + } + } + + private void SetModPermanent(StringBuilder sb, Mod mod, ModSettings settings, Guid collection) + { + var ec = settings.ForceInherit + ? _inheritMod!.Invoke(collection, mod.DirectoryName, true, mod.Name) + : _setMod!.Invoke(collection, mod.DirectoryName, settings.Enabled, mod.Name); + switch (ec) + { + case PenumbraApiEc.ModMissing: + sb.Append($"The mod {mod.Name} [{mod.DirectoryName}] could not be found."); + return; + case PenumbraApiEc.CollectionMissing: + sb.Append($"The collection {collection} could not be found."); + return; + } + + if (settings.ForceInherit || !settings.Enabled) + return; + + ec = _setModPriority!.Invoke(collection, mod.DirectoryName, settings.Priority, mod.Name); + Debug.Assert(ec is PenumbraApiEc.Success or PenumbraApiEc.NothingChanged, "Setting Priority should not be able to fail."); + + foreach (var (setting, list) in settings.Settings) + { + ec = list.Count == 1 + ? _setModSetting!.Invoke(collection, mod.DirectoryName, setting, list[0], mod.Name) + : _setModSettings!.Invoke(collection, mod.DirectoryName, setting, list, mod.Name); + switch (ec) + { + case PenumbraApiEc.OptionGroupMissing: sb.AppendLine($"Could not find the option group {setting} in mod {mod.Name}."); break; + case PenumbraApiEc.OptionMissing: + sb.AppendLine($"Could not find all desired options in the option group {setting} in mod {mod.Name}."); + break; + case PenumbraApiEc.Success: + case PenumbraApiEc.NothingChanged: + break; + default: + sb.AppendLine($"Could not apply options in the option group {setting} in mod {mod.Name} for unknown reason {ec}."); + break; + } + } + } + + /// Obtain the name of the collection currently assigned to the player. - public string GetCurrentPlayerCollection() + public Guid GetCurrentPlayerCollection() { if (!Available) - return string.Empty; + return Guid.Empty; - var (valid, _, name) = _objectCollection.Invoke(0); - return valid ? name : string.Empty; + var (valid, _, (id, _)) = _objectCollection!.Invoke(0); + return valid ? id : Guid.Empty; + } + + /// Obtain the name of the collection currently assigned to the given actor. + public Guid GetActorCollection(Actor actor, out string name) + { + if (!Available) + { + name = string.Empty; + return Guid.Empty; + } + + (var valid, _, (var id, name)) = _objectCollection!.Invoke(actor.Index.Index); + return valid ? id : Guid.Empty; } /// Obtain the game object corresponding to a draw object. public Actor GameObjectFromDrawObject(Model drawObject) - => Available ? _drawObjectInfo.Invoke(drawObject.Address).Item1 : Actor.Null; + => _getGameObject?.Invoke(drawObject.Address) ?? Actor.Null; /// Obtain the parent of a cutscene actor if it is known. - public int CutsceneParent(int idx) - => Available ? _cutsceneParent.Invoke(idx) : -1; + public short CutsceneParent(ushort idx) + => (short)(_checkCutsceneParent?.Invoke(idx) ?? -1); /// Try to redraw the given actor. public void RedrawObject(Actor actor, RedrawType settings) { - if (!actor || !Available) + if (!actor) return; - try - { - _redrawSubscriber.Invoke(actor.AsObject->ObjectIndex, settings); - } - catch (Exception e) - { - Glamourer.Log.Debug($"Failure redrawing object:\n{e}"); - } + RedrawObject(actor.Index, settings); } /// Try to redraw the given actor. public void RedrawObject(ObjectIndex index, RedrawType settings) { + if (!Available) + return; + try { - _redrawSubscriber.Invoke(index.Index, settings); + _redraw!.Invoke(index.Index, settings); } catch (Exception e) { @@ -260,34 +512,79 @@ public unsafe class PenumbraService : IDisposable { Unattach(); - var (breaking, feature) = Ipc.ApiVersions.Subscriber(_pluginInterface).Invoke(); - if (breaking != RequiredPenumbraBreakingVersion || feature < RequiredPenumbraFeatureVersion) + AttachTime = DateTime.UtcNow; + try + { + (CurrentMajor, CurrentMinor) = new global::Penumbra.Api.IpcSubscribers.ApiVersion(_pluginInterface).Invoke(); + } + catch + { + try + { + (CurrentMajor, CurrentMinor) = new global::Penumbra.Api.IpcSubscribers.Legacy.ApiVersions(_pluginInterface).Invoke(); + } + catch + { + CurrentMajor = 0; + CurrentMinor = 0; + throw; + } + } + + if (CurrentMajor != RequiredPenumbraBreakingVersion || CurrentMinor < RequiredPenumbraFeatureVersion) throw new Exception( - $"Invalid Version {breaking}.{feature:D4}, required major Version {RequiredPenumbraBreakingVersion} with feature greater or equal to {RequiredPenumbraFeatureVersion}."); + $"Invalid Version {CurrentMajor}.{CurrentMinor:D4}, required major Version {RequiredPenumbraBreakingVersion} with feature greater or equal to {RequiredPenumbraFeatureVersion}."); _tooltipSubscriber.Enable(); _clickSubscriber.Enable(); _creatingCharacterBase.Enable(); _createdCharacterBase.Enable(); + _pcpCreated.Enable(); + _pcpParsed.Enable(); _modSettingChanged.Enable(); - _drawObjectInfo = Ipc.GetDrawObjectInfo.Subscriber(_pluginInterface); - _cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface); - _redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface); - _objectCollection = Ipc.GetCollectionForObject.Subscriber(_pluginInterface); - _getMods = Ipc.GetMods.Subscriber(_pluginInterface); - _currentCollection = Ipc.GetCollectionForType.Subscriber(_pluginInterface); - _getCurrentSettings = Ipc.GetCurrentModSettings.Subscriber(_pluginInterface); - _setMod = Ipc.TrySetMod.Subscriber(_pluginInterface); - _setModPriority = Ipc.TrySetModPriority.Subscriber(_pluginInterface); - _setModSetting = Ipc.TrySetModSetting.Subscriber(_pluginInterface); - _setModSettings = Ipc.TrySetModSettings.Subscriber(_pluginInterface); - _openModPage = Ipc.OpenMainWindow.Subscriber(_pluginInterface); - Available = true; + _collectionByIdentifier = new global::Penumbra.Api.IpcSubscribers.GetCollectionsByIdentifier(_pluginInterface); + _collections = new global::Penumbra.Api.IpcSubscribers.GetCollections(_pluginInterface); + _redraw = new global::Penumbra.Api.IpcSubscribers.RedrawObject(_pluginInterface); + _checkCutsceneParent = new global::Penumbra.Api.IpcSubscribers.GetCutsceneParentIndexFunc(_pluginInterface).Invoke(); + _getGameObject = new global::Penumbra.Api.IpcSubscribers.GetGameObjectFromDrawObjectFunc(_pluginInterface).Invoke(); + _objectCollection = new global::Penumbra.Api.IpcSubscribers.GetCollectionForObject(_pluginInterface); + _getMods = new global::Penumbra.Api.IpcSubscribers.GetModList(_pluginInterface); + _currentCollection = new global::Penumbra.Api.IpcSubscribers.GetCollection(_pluginInterface); + _getCurrentSettings = new global::Penumbra.Api.IpcSubscribers.GetCurrentModSettings(_pluginInterface); + _inheritMod = new global::Penumbra.Api.IpcSubscribers.TryInheritMod(_pluginInterface); + _setMod = new global::Penumbra.Api.IpcSubscribers.TrySetMod(_pluginInterface); + _setModPriority = new global::Penumbra.Api.IpcSubscribers.TrySetModPriority(_pluginInterface); + _setModSetting = new global::Penumbra.Api.IpcSubscribers.TrySetModSetting(_pluginInterface); + _setModSettings = new global::Penumbra.Api.IpcSubscribers.TrySetModSettings(_pluginInterface); + _openModPage = new global::Penumbra.Api.IpcSubscribers.OpenMainWindow(_pluginInterface); + _getChangedItems = new global::Penumbra.Api.IpcSubscribers.GetChangedItems(_pluginInterface); + _setTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.SetTemporaryModSettings(_pluginInterface); + _setTemporaryModSettingsPlayer = new global::Penumbra.Api.IpcSubscribers.SetTemporaryModSettingsPlayer(_pluginInterface); + _removeTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.RemoveTemporaryModSettings(_pluginInterface); + _removeTemporaryModSettingsPlayer = new global::Penumbra.Api.IpcSubscribers.RemoveTemporaryModSettingsPlayer(_pluginInterface); + _removeAllTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.RemoveAllTemporaryModSettings(_pluginInterface); + _removeAllTemporaryModSettingsPlayer = + new global::Penumbra.Api.IpcSubscribers.RemoveAllTemporaryModSettingsPlayer(_pluginInterface); + _queryTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.QueryTemporaryModSettings(_pluginInterface); + _queryTemporaryModSettingsPlayer = + new global::Penumbra.Api.IpcSubscribers.QueryTemporaryModSettingsPlayer(_pluginInterface); + _getCurrentSettingsWithTemp = new global::Penumbra.Api.IpcSubscribers.GetCurrentModSettingsWithTemp(_pluginInterface); + _getAllSettings = new global::Penumbra.Api.IpcSubscribers.GetAllModSettings(_pluginInterface); + _changedItems = new global::Penumbra.Api.IpcSubscribers.GetChangedItemAdapterList(_pluginInterface).Invoke(); + _checkCurrentChangedItems = + new global::Penumbra.Api.IpcSubscribers.CheckCurrentChangedItemFunc(_pluginInterface).Invoke(); + _registerSettingsSection = new global::Penumbra.Api.IpcSubscribers.RegisterSettingsSection(_pluginInterface); + _unregisterSettingsSection = new global::Penumbra.Api.IpcSubscribers.UnregisterSettingsSection(_pluginInterface); + + _registerSettingsSection.Invoke(InvokeDrawSettingsSection); + + Available = true; _penumbraReloaded.Invoke(); Glamourer.Log.Debug("Glamourer attached to Penumbra."); } catch (Exception e) { + Unattach(); Glamourer.Log.Debug($"Could not attach to Penumbra:\n{e}"); } } @@ -300,15 +597,57 @@ public unsafe class PenumbraService : IDisposable _creatingCharacterBase.Disable(); _createdCharacterBase.Disable(); _modSettingChanged.Disable(); + _pcpCreated.Disable(); + _pcpParsed.Disable(); + try + { + _unregisterSettingsSection?.Invoke(InvokeDrawSettingsSection); + } + catch (IpcNotReadyError) + { + // Ignore. + } + if (Available) { - Available = false; + _collectionByIdentifier = null; + _collections = null; + _redraw = null; + _getGameObject = null; + _checkCutsceneParent = null; + _objectCollection = null; + _getMods = null; + _currentCollection = null; + _getCurrentSettings = null; + _getCurrentSettingsWithTemp = null; + _getAllSettings = null; + _inheritMod = null; + _setMod = null; + _setModPriority = null; + _setModSetting = null; + _setModSettings = null; + _openModPage = null; + _setTemporaryModSettings = null; + _setTemporaryModSettingsPlayer = null; + _removeTemporaryModSettings = null; + _removeTemporaryModSettingsPlayer = null; + _removeAllTemporaryModSettings = null; + _removeAllTemporaryModSettingsPlayer = null; + _queryTemporaryModSettings = null; + _queryTemporaryModSettingsPlayer = null; + _getChangedItems = null; + _changedItems = null; + _checkCurrentChangedItems = null; + _registerSettingsSection = null; + _unregisterSettingsSection = null; + Available = false; Glamourer.Log.Debug("Glamourer detached from Penumbra."); } } public void Dispose() { + ClearAllTemporarySettings(true, true); Unattach(); _tooltipSubscriber.Dispose(); _clickSubscriber.Dispose(); @@ -317,5 +656,7 @@ public unsafe class PenumbraService : IDisposable _initializedEvent.Dispose(); _disposedEvent.Dispose(); _modSettingChanged.Dispose(); + _pcpCreated.Dispose(); + _pcpParsed.Dispose(); } } diff --git a/Glamourer/Interop/ScalingService.cs b/Glamourer/Interop/ScalingService.cs index ea71cad..2a89a25 100644 --- a/Glamourer/Interop/ScalingService.cs +++ b/Glamourer/Interop/ScalingService.cs @@ -1,28 +1,38 @@ -using System; -using System.Runtime.CompilerServices; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Hooking; +using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; -using Glamourer.Interop.Structs; +using Penumbra.GameData; +using Penumbra.GameData.Interop; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Glamourer.State; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; +using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; namespace Glamourer.Interop; public unsafe class ScalingService : IDisposable { - public ScalingService(IGameInteropProvider interop) + private readonly ActorManager _actors; + private readonly StateManager _state; + + public ScalingService(IGameInteropProvider interop, StateManager state, ActorManager actors) { + _state = state; + _actors = actors; interop.InitializeFromAttributes(this); _setupMountHook = - interop.HookFromAddress((nint)Character.MountContainer.MemberFunctionPointers.SetupMount, SetupMountDetour); - _setupOrnamentHook = interop.HookFromAddress((nint)Ornament.MemberFunctionPointers.SetupOrnament, SetupOrnamentDetour); + interop.HookFromAddress((nint)MountContainer.MemberFunctionPointers.SetupMount, SetupMountDetour); _calculateHeightHook = - interop.HookFromAddress((nint)Character.MemberFunctionPointers.CalculateHeight, CalculateHeightDetour); + interop.HookFromAddress((nint)ModelContainer.MemberFunctionPointers.CalculateHeight, CalculateHeightDetour); + _placeMinionHook = interop.HookFromAddress((nint)Companion.MemberFunctionPointers.PlaceCompanion, PlaceMinionDetour); + //_updateOrnamentHook = + // interop.HookFromAddress((nint)Ornament.MemberFunctionPointers.UpdateOrnament, UpdateOrnamentDetour); _setupMountHook.Enable(); - _setupOrnamentHook.Enable(); + _updateOrnamentHook.Enable(); _placeMinionHook.Enable(); _calculateHeightHook.Enable(); } @@ -30,53 +40,45 @@ public unsafe class ScalingService : IDisposable public void Dispose() { _setupMountHook.Dispose(); - _setupOrnamentHook.Dispose(); + _updateOrnamentHook.Dispose(); _placeMinionHook.Dispose(); _calculateHeightHook.Dispose(); } - private delegate void SetupMount(Character.MountContainer* container, short mountId, uint unk1, uint unk2, uint unk3, byte unk4); - private delegate void SetupOrnament(Ornament* ornament, uint* unk1, float* unk2); + private delegate void SetupMount(MountContainer* container, short mountId, uint unk1, uint unk2, uint unk3, byte unk4); + private delegate void UpdateOrnament(OrnamentContainer* ornament); private delegate void PlaceMinion(Companion* character); - private delegate float CalculateHeight(Character* character); + private delegate float CalculateHeight(ModelContainer* character); private readonly Hook _setupMountHook; - private readonly Hook _setupOrnamentHook; + // TODO: Use client structs sig. + [Signature(Sigs.UpdateOrnament, DetourName = nameof(UpdateOrnamentDetour))] + private readonly Hook _updateOrnamentHook = null!; private readonly Hook _calculateHeightHook; - // TODO: Use client structs sig. - [Signature("48 89 5C 24 ?? 55 57 41 57 48 8D 6C 24", DetourName = nameof(PlaceMinionDetour))] private readonly Hook _placeMinionHook = null!; - - private void SetupMountDetour(Character.MountContainer* container, short mountId, uint unk1, uint unk2, uint unk3, byte unk4) + private void SetupMountDetour(MountContainer* container, short mountId, uint unk1, uint unk2, uint unk3, byte unk4) { - var (race, clan, gender) = GetScaleRelevantCustomize(&container->OwnerObject->Character); - SetScaleCustomize(&container->OwnerObject->Character, container->OwnerObject->Character.GameObject.DrawObject); + var (race, clan, gender) = GetScaleRelevantCustomize(container->OwnerObject); + SetScaleCustomize(container->OwnerObject, container->OwnerObject->DrawObject); _setupMountHook.Original(container, mountId, unk1, unk2, unk3, unk4); - SetScaleCustomize(&container->OwnerObject->Character, race, clan, gender); + SetScaleCustomize(container->OwnerObject, race, clan, gender); } - private void SetupOrnamentDetour(Ornament* ornament, uint* unk1, float* unk2) + private void UpdateOrnamentDetour(OrnamentContainer* container) { - var character = ornament->Character.GetParentCharacter(); - if (character == null) - { - _setupOrnamentHook.Original(ornament, unk1, unk2); - return; - } - - var (race, clan, gender) = GetScaleRelevantCustomize(character); - SetScaleCustomize(character, character->GameObject.DrawObject); - _setupOrnamentHook.Original(ornament, unk1, unk2); - SetScaleCustomize(character, race, clan, gender); + var (race, clan, gender) = GetScaleRelevantCustomize(container->OwnerObject); + SetScaleCustomize(container->OwnerObject, container->OwnerObject->DrawObject); + _updateOrnamentHook.Original(container); + SetScaleCustomize(container->OwnerObject, race, clan, gender); } private void PlaceMinionDetour(Companion* companion) { - var owner = (Actor)((nint*)companion)[0x386]; + var owner = (Actor)(GameObject*)companion->Owner; if (!owner.IsCharacter) { _placeMinionHook.Original(companion); @@ -86,62 +88,88 @@ public unsafe class ScalingService : IDisposable var mdl = owner.Model; var oldRace = owner.AsCharacter->DrawData.CustomizeData.Race; if (mdl.IsHuman) + { owner.AsCharacter->DrawData.CustomizeData.Race = mdl.AsHuman->Customize.Race; + } + else + { + var actor = _actors.FromObject(owner, out _, true, false, true); + if (_state.TryGetValue(actor, out var state)) + owner.AsCharacter->DrawData.CustomizeData.Race = (byte)state.ModelData.Customize.Race; + } + _placeMinionHook.Original(companion); owner.AsCharacter->DrawData.CustomizeData.Race = oldRace; } } - private float CalculateHeightDetour(Character* character) + private float CalculateHeightDetour(ModelContainer* container) { - var (gender, bodyType, clan, height) = GetHeightRelevantCustomize(character); - SetHeightCustomize(character, character->GameObject.DrawObject); - var ret = _calculateHeightHook.Original(character); - SetHeightCustomize(character, gender, bodyType, clan, height); + var (gender, bodyType, clan, height) = GetHeightRelevantCustomize(container->OwnerObject); + SetHeightCustomize(container->OwnerObject, container->OwnerObject->DrawObject); + var ret = _calculateHeightHook.Original(container); + SetHeightCustomize(container->OwnerObject, gender, bodyType, clan, height); return ret; } /// We do not change the Customize gender because the functions use the GetGender() vfunc, which uses the game objects gender value. private static (byte Race, byte Clan, byte Gender) GetScaleRelevantCustomize(Character* character) - => (character->DrawData.CustomizeData.Race, character->DrawData.CustomizeData.Clan, character->GameObject.Gender); + => (character->DrawData.CustomizeData.Race, character->DrawData.CustomizeData.Tribe, character->GameObject.Sex); private static (byte Gender, byte BodyType, byte Clan, byte Height) GetHeightRelevantCustomize(Character* character) => (character->DrawData.CustomizeData.Sex, character->DrawData.CustomizeData.BodyType, - character->DrawData.CustomizeData.Clan, character->DrawData.CustomizeData[(int)CustomizeIndex.Height]); + character->DrawData.CustomizeData.Tribe, character->DrawData.CustomizeData[(int)CustomizeIndex.Height]); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static void SetScaleCustomize(Character* character, Model model) + private void SetScaleCustomize(Character* character, Model model) { - if (!model.IsHuman) + if (model.IsHuman) + { + SetScaleCustomize(character, model.AsHuman->Customize.Race, model.AsHuman->Customize.Tribe, model.AsHuman->Customize.Sex); + return; + } + + var actor = _actors.FromObject(character, out _, true, false, true); + if (!_state.TryGetValue(actor, out var state)) return; - SetScaleCustomize(character, model.AsHuman->Customize.Race, model.AsHuman->Customize.Clan, model.AsHuman->Customize.Sex); + ref var customize = ref state.ModelData.Customize; + SetScaleCustomize(character, (byte)customize.Race, (byte)customize.Clan, customize.Gender.ToGameByte()); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static void SetScaleCustomize(Character* character, byte race, byte clan, byte gender) { - character->DrawData.CustomizeData.Race = race; - character->DrawData.CustomizeData.Clan = clan; - character->GameObject.Gender = gender; + character->DrawData.CustomizeData.Race = race; + character->DrawData.CustomizeData.Tribe = clan; + character->GameObject.Sex = gender; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static void SetHeightCustomize(Character* character, Model model) + private void SetHeightCustomize(Character* character, Model model) { - if (!model.IsHuman) + if (model.IsHuman) + { + SetHeightCustomize(character, model.AsHuman->Customize.Sex, model.AsHuman->Customize.BodyType, model.AsHuman->Customize.Tribe, + model.AsHuman->Customize[(int)CustomizeIndex.Height]); + return; + } + + var actor = _actors.FromObject(character, out _, true, false, true); + if (!_state.TryGetValue(actor, out var state)) return; - SetHeightCustomize(character, model.AsHuman->Customize.Sex, model.AsHuman->Customize.BodyType, model.AsHuman->Customize.Clan, - model.AsHuman->Customize[(int)CustomizeIndex.Height]); + ref var customize = ref state.ModelData.Customize; + SetHeightCustomize(character, customize.Gender.ToGameByte(), customize.BodyType.Value, (byte)customize.Clan, + customize[global::Penumbra.GameData.Enums.CustomizeIndex.Height].Value); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static void SetHeightCustomize(Character* character, byte gender, byte bodyType, byte clan, byte height) { - character->DrawData.CustomizeData.Sex = gender; - character->DrawData.CustomizeData.BodyType = bodyType; - character->DrawData.CustomizeData.Clan = clan; - character->DrawData.CustomizeData.Data[(int)CustomizeIndex.Height] = height; + character->DrawData.CustomizeData.Sex = gender; + character->DrawData.CustomizeData.BodyType = bodyType; + character->DrawData.CustomizeData.Tribe = clan; + character->DrawData.CustomizeData.Height = height; } } diff --git a/Glamourer/Interop/Structs/Actor.cs b/Glamourer/Interop/Structs/Actor.cs deleted file mode 100644 index 644d904..0000000 --- a/Glamourer/Interop/Structs/Actor.cs +++ /dev/null @@ -1,117 +0,0 @@ -using Penumbra.GameData.Actors; -using System; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Glamourer.Customization; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.String; - -namespace Glamourer.Interop.Structs; - -public readonly unsafe struct Actor : IEquatable -{ - private Actor(nint address) - => Address = address; - - public static readonly Actor Null = new(nint.Zero); - - public readonly nint Address; - - public GameObject* AsObject - => (GameObject*)Address; - - public Character* AsCharacter - => (Character*)Address; - - public bool Valid - => Address != nint.Zero; - - public bool IsCharacter - => Valid && AsObject->IsCharacter(); - - public static implicit operator Actor(nint? pointer) - => new(pointer ?? nint.Zero); - - public static implicit operator Actor(GameObject* pointer) - => new((nint)pointer); - - public static implicit operator Actor(Character* pointer) - => new((nint)pointer); - - public static implicit operator nint(Actor actor) - => actor.Address; - - public bool IsGPoseOrCutscene - => Index.Index is >= (int)ScreenActor.CutsceneStart and < (int)ScreenActor.CutsceneEnd; - - public ActorIdentifier GetIdentifier(ActorManager actors) - => actors.FromObject(AsObject, out _, true, true, false); - - public ByteString Utf8Name - => Valid ? new ByteString(AsObject->Name) : ByteString.Empty; - - public bool Identifier(ActorManager actors, out ActorIdentifier ident) - { - if (Valid) - { - ident = GetIdentifier(actors); - return ident.IsValid; - } - - ident = ActorIdentifier.Invalid; - return false; - } - - public ObjectIndex Index - => Valid ? AsObject->ObjectIndex : ObjectIndex.AnyIndex; - - public Model Model - => Valid ? AsObject->DrawObject : null; - - public byte Job - => IsCharacter ? AsCharacter->CharacterData.ClassJob : (byte)0; - - public static implicit operator bool(Actor actor) - => actor.Address != nint.Zero; - - public static bool operator true(Actor actor) - => actor.Address != nint.Zero; - - public static bool operator false(Actor actor) - => actor.Address == nint.Zero; - - public static bool operator !(Actor actor) - => actor.Address == nint.Zero; - - public bool Equals(Actor other) - => Address == other.Address; - - public override bool Equals(object? obj) - => obj is Actor other && Equals(other); - - public override int GetHashCode() - => Address.GetHashCode(); - - public static bool operator ==(Actor lhs, Actor rhs) - => lhs.Address == rhs.Address; - - public static bool operator !=(Actor lhs, Actor rhs) - => lhs.Address != rhs.Address; - - /// Only valid for characters. - public CharacterArmor GetArmor(EquipSlot slot) - => ((CharacterArmor*)&AsCharacter->DrawData.Head)[slot.ToIndex()]; - - public CharacterWeapon GetMainhand() - => new(AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).ModelId.Value); - - public CharacterWeapon GetOffhand() - => new(AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).ModelId.Value); - - public Customize GetCustomize() - => *(Customize*)&AsCharacter->DrawData.CustomizeData; - - public override string ToString() - => $"0x{Address:X}"; -} diff --git a/Glamourer/Interop/Structs/ActorData.cs b/Glamourer/Interop/Structs/ActorData.cs deleted file mode 100644 index ce11e34..0000000 --- a/Glamourer/Interop/Structs/ActorData.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using OtterGui.Log; -using Penumbra.GameData.Actors; - -namespace Glamourer.Interop.Structs; - -/// -/// A single actor with its label and the list of associated game objects. -/// -public readonly struct ActorData -{ - public readonly List Objects; - public readonly string Label; - - public bool Valid - => Objects.Count > 0; - - public ActorData(Actor actor, string label) - { - Objects = new List { actor }; - Label = label; - } - - public static readonly ActorData Invalid = new(false); - - private ActorData(bool _) - { - Objects = new List(0); - Label = string.Empty; - } - - public LazyString ToLazyString(string invalid) - { - var objects = Objects; - return Valid - ? new LazyString(() => string.Join(", ", objects.Select(o => o.ToString()))) - : new LazyString(() => invalid); - } - - private ActorData(List objects, string label) - { - Objects = objects; - Label = label; - } - - public ActorData OnlyGPose() - => new(Objects.Where(o => o.IsGPoseOrCutscene).ToList(), Label); -} diff --git a/Glamourer/Interop/Structs/Model.cs b/Glamourer/Interop/Structs/Model.cs deleted file mode 100644 index 77bf24e..0000000 --- a/Glamourer/Interop/Structs/Model.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Glamourer.Customization; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; -using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; - -namespace Glamourer.Interop.Structs; - -public readonly unsafe struct Model : IEquatable -{ - private Model(nint address) - => Address = address; - - public readonly nint Address; - - public static readonly Model Null = new(0); - - public DrawObject* AsDrawObject - => (DrawObject*)Address; - - public CharacterBase* AsCharacterBase - => (CharacterBase*)Address; - - public Weapon* AsWeapon - => (Weapon*)Address; - - public Human* AsHuman - => (Human*)Address; - - public static implicit operator Model(nint? pointer) - => new(pointer ?? nint.Zero); - - public static implicit operator Model(Object* pointer) - => new((nint)pointer); - - public static implicit operator Model(DrawObject* pointer) - => new((nint)pointer); - - public static implicit operator Model(Human* pointer) - => new((nint)pointer); - - public static implicit operator Model(CharacterBase* pointer) - => new((nint)pointer); - - public static implicit operator nint(Model model) - => model.Address; - - public bool Valid - => Address != nint.Zero; - - public bool IsCharacterBase - => Valid && AsDrawObject->Object.GetObjectType() == ObjectType.CharacterBase; - - public bool IsHuman - => IsCharacterBase && AsCharacterBase->GetModelType() == CharacterBase.ModelType.Human; - - public bool IsWeapon - => IsCharacterBase && AsCharacterBase->GetModelType() == CharacterBase.ModelType.Weapon; - - public static implicit operator bool(Model actor) - => actor.Address != nint.Zero; - - public static bool operator true(Model actor) - => actor.Address != nint.Zero; - - public static bool operator false(Model actor) - => actor.Address == nint.Zero; - - public static bool operator !(Model actor) - => actor.Address == nint.Zero; - - public bool Equals(Model other) - => Address == other.Address; - - public override bool Equals(object? obj) - => obj is Model other && Equals(other); - - public override int GetHashCode() - => Address.GetHashCode(); - - public static bool operator ==(Model lhs, Model rhs) - => lhs.Address == rhs.Address; - - public static bool operator !=(Model lhs, Model rhs) - => lhs.Address != rhs.Address; - - /// Only valid for humans. - public CharacterArmor GetArmor(EquipSlot slot) - => ((CharacterArmor*)&AsHuman->Head)[slot.ToIndex()]; - - public Customize GetCustomize() - => *(Customize*)&AsHuman->Customize; - - public (Model Address, CharacterWeapon Data) GetMainhand() - { - Model weapon = AsDrawObject->Object.ChildObject; - return !weapon.IsWeapon - ? (Null, CharacterWeapon.Empty) - : (weapon, new CharacterWeapon(weapon.AsWeapon->ModelSetId, weapon.AsWeapon->SecondaryId, (Variant)weapon.AsWeapon->Variant, - (StainId)weapon.AsWeapon->ModelUnknown)); - } - - public (Model Address, CharacterWeapon Data) GetOffhand() - { - var mainhand = AsDrawObject->Object.ChildObject; - if (mainhand == null) - return (Null, CharacterWeapon.Empty); - - Model offhand = mainhand->NextSiblingObject; - if (offhand == mainhand || !offhand.IsWeapon) - return (Null, CharacterWeapon.Empty); - - return (offhand, new CharacterWeapon(offhand.AsWeapon->ModelSetId, offhand.AsWeapon->SecondaryId, (Variant)offhand.AsWeapon->Variant, - (StainId)offhand.AsWeapon->ModelUnknown)); - } - - /// Obtain the mainhand and offhand and their data by guesstimating which child object is which. - public (Model Mainhand, Model Offhand, CharacterWeapon MainData, CharacterWeapon OffData) GetWeapons() - { - var (first, second, count) = GetChildrenWeapons(); - switch (count) - { - case 0: return (Null, Null, CharacterWeapon.Empty, CharacterWeapon.Empty); - case 1: - return (first, Null, new CharacterWeapon(first.AsWeapon->ModelSetId, first.AsWeapon->SecondaryId, - (Variant)first.AsWeapon->Variant, - (StainId)first.AsWeapon->ModelUnknown), CharacterWeapon.Empty); - default: - var (main, off) = DetermineMainhand(first, second); - var mainData = new CharacterWeapon(main.AsWeapon->ModelSetId, main.AsWeapon->SecondaryId, (Variant)main.AsWeapon->Variant, - (StainId)main.AsWeapon->ModelUnknown); - var offData = new CharacterWeapon(off.AsWeapon->ModelSetId, off.AsWeapon->SecondaryId, (Variant)off.AsWeapon->Variant, - (StainId)off.AsWeapon->ModelUnknown); - return (main, off, mainData, offData); - } - } - - /// Obtain the mainhand and offhand and their data by using the drawdata container from the corresponding actor. - public (Model Mainhand, Model Offhand, CharacterWeapon MainData, CharacterWeapon OffData) GetWeapons(Actor actor) - { - if (!Valid || !actor.IsCharacter || actor.Model.Address != Address) - return (Null, Null, CharacterWeapon.Empty, CharacterWeapon.Empty); - - Model main = actor.AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).DrawObject; - var mainData = CharacterWeapon.Empty; - if (main.IsWeapon) - mainData = new CharacterWeapon(main.AsWeapon->ModelSetId, main.AsWeapon->SecondaryId, (Variant)main.AsWeapon->Variant, - (StainId)main.AsWeapon->ModelUnknown); - else - main = Null; - Model off = actor.AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).DrawObject; - var offData = CharacterWeapon.Empty; - if (off.IsWeapon) - offData = new CharacterWeapon(off.AsWeapon->ModelSetId, off.AsWeapon->SecondaryId, (Variant)off.AsWeapon->Variant, - (StainId)off.AsWeapon->ModelUnknown); - else - off = Null; - return (main, off, mainData, offData); - } - - private (Model, Model, int) GetChildrenWeapons() - { - Span weapons = stackalloc Model[2]; - weapons[0] = Null; - weapons[1] = Null; - var count = 0; - - if (!Valid || AsDrawObject->Object.ChildObject == null) - return (weapons[0], weapons[1], count); - - Model starter = AsDrawObject->Object.ChildObject; - var iterator = starter; - do - { - if (iterator.IsWeapon) - weapons[count++] = iterator; - if (count == 2) - return (weapons[0], weapons[1], count); - - iterator = iterator.AsDrawObject->Object.NextSiblingObject; - } while (iterator.Address != starter.Address); - - return (weapons[0], weapons[1], count); - } - - /// I don't know a safe way to do this but in experiments this worked. - /// The first uint at +0x8 was set to non-zero for the mainhand and zero for the offhand. - private static (Model Mainhand, Model Offhand) DetermineMainhand(Model first, Model second) - { - var discriminator1 = *(ulong*)(first.Address + 0x10); - var discriminator2 = *(ulong*)(second.Address + 0x10); - return discriminator1 == 0 && discriminator2 != 0 ? (second, first) : (first, second); - } - - public override string ToString() - => $"0x{Address:X}"; -} diff --git a/Glamourer/Interop/Structs/ModelExtensions.cs b/Glamourer/Interop/Structs/ModelExtensions.cs new file mode 100644 index 0000000..207c72c --- /dev/null +++ b/Glamourer/Interop/Structs/ModelExtensions.cs @@ -0,0 +1,67 @@ +using FFXIVClientStructs.FFXIV.Shader; +using Glamourer.GameData; +using Penumbra.GameData.Interop; + +namespace Glamourer.Interop.Structs; + +public static unsafe class ModelExtensions +{ + public static CustomizeParameterData GetParameterData(this Model model) + { + if (!model.IsHuman) + return default; + + var cBuffer1 = model.AsHuman->CustomizeParameterCBuffer; + var cBuffer2 = model.AsHuman->DecalColorCBuffer; + var ptr1 = (CustomizeParameter*)(cBuffer1 == null ? null : cBuffer1->UnsafeSourcePointer); + var ptr2 = (DecalParameters*)(cBuffer2 == null ? null : cBuffer2->UnsafeSourcePointer); + return CustomizeParameterData.FromParameters(ptr1 != null ? *ptr1 : default, ptr2 != null ? *ptr2 : default); + } + + public static void ApplyParameterData(this Model model, CustomizeParameterFlag flags, in CustomizeParameterData data) + { + if (!model.IsHuman) + return; + + if (flags.HasFlag(CustomizeParameterFlag.DecalColor)) + { + var cBufferDecal = model.AsHuman->DecalColorCBuffer; + var ptrDecal = (DecalParameters*)(cBufferDecal == null ? null : cBufferDecal->UnsafeSourcePointer); + if (ptrDecal != null) + data.Apply(ref *ptrDecal); + } + + flags &= ~CustomizeParameterFlag.DecalColor; + var cBuffer = model.AsHuman->CustomizeParameterCBuffer; + var ptr = (CustomizeParameter*)(cBuffer == null ? null : cBuffer->UnsafeSourcePointer); + if (ptr != null) + data.Apply(ref *ptr, flags); + } + + public static bool ApplySingleParameterData(this Model model, CustomizeParameterFlag flag, in CustomizeParameterData data) + { + if (!model.IsHuman) + return false; + + if (flag is CustomizeParameterFlag.DecalColor) + { + var cBuffer = model.AsHuman->DecalColorCBuffer; + var ptr = (DecalParameters*)(cBuffer == null ? null : cBuffer->UnsafeSourcePointer); + if (ptr == null) + return false; + + data.Apply(ref *ptr); + return true; + } + else + { + var cBuffer = model.AsHuman->CustomizeParameterCBuffer; + var ptr = (CustomizeParameter*)(cBuffer == null ? null : cBuffer->UnsafeSourcePointer); + if (ptr == null) + return false; + + data.ApplySingle(ref *ptr, flag); + return true; + } + } +} diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index f336f5a..3ef99d9 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -1,31 +1,47 @@ -using System; -using Dalamud.Hooking; +using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Network; using Glamourer.Events; -using Glamourer.Interop.Structs; +using Penumbra.GameData; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.Interop; public unsafe class UpdateSlotService : IDisposable { - public readonly SlotUpdating SlotUpdatingEvent; + public readonly EquipSlotUpdating EquipSlotUpdatingEvent; + public readonly BonusSlotUpdating BonusSlotUpdatingEvent; + public readonly GearsetDataLoaded GearsetDataLoadedEvent; + private readonly DictBonusItems _bonusItems; - public UpdateSlotService(SlotUpdating slotUpdating, IGameInteropProvider interop) + public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, GearsetDataLoaded gearsetDataLoaded, + IGameInteropProvider interop, DictBonusItems bonusItems) { - SlotUpdatingEvent = slotUpdating; + EquipSlotUpdatingEvent = equipSlotUpdating; + BonusSlotUpdatingEvent = bonusSlotUpdating; + GearsetDataLoadedEvent = gearsetDataLoaded; + _bonusItems = bonusItems; + interop.InitializeFromAttributes(this); + _loadGearsetDataHook = interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadGearsetData, LoadGearsetDataDetour); _flagSlotForUpdateHook.Enable(); + _flagBonusSlotForUpdateHook.Enable(); + _loadGearsetDataHook.Enable(); } public void Dispose() { _flagSlotForUpdateHook.Dispose(); + _flagBonusSlotForUpdateHook.Dispose(); + _loadGearsetDataHook.Dispose(); } - public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data) + public void UpdateEquipSlot(Model drawObject, EquipSlot slot, CharacterArmor data) { if (!drawObject.IsCharacterBase) return; @@ -33,29 +49,98 @@ public unsafe class UpdateSlotService : IDisposable FlagSlotForUpdateInterop(drawObject, slot, data); } - public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor, StainId stain) - => UpdateSlot(drawObject, slot, armor.With(stain)); + public void UpdateBonusSlot(Model drawObject, BonusItemFlag slot, CharacterArmor data) + { + if (!drawObject.IsCharacterBase) + return; + + var index = slot.ToIndex(); + if (index == uint.MaxValue) + return; + + _flagBonusSlotForUpdateHook.Original(drawObject.Address, index, &data); + } + + public void UpdateGlasses(Model drawObject, BonusItemId id) + { + if (!_bonusItems.TryGetValue(id, out var glasses)) + return; + + var armor = new CharacterArmor(glasses.PrimaryId, glasses.Variant, StainIds.None); + _flagBonusSlotForUpdateHook.Original(drawObject.Address, BonusItemFlag.Glasses.ToIndex(), &armor); + } + + public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor, StainIds stains) + => UpdateEquipSlot(drawObject, slot, armor.With(stains)); public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor) - => UpdateArmor(drawObject, slot, armor, drawObject.GetArmor(slot).Stain); + => UpdateArmor(drawObject, slot, armor, drawObject.GetArmor(slot).Stains); - public void UpdateStain(Model drawObject, EquipSlot slot, StainId stain) - => UpdateArmor(drawObject, slot, drawObject.GetArmor(slot), stain); + public void UpdateStain(Model drawObject, EquipSlot slot, StainIds stains) + => UpdateArmor(drawObject, slot, drawObject.GetArmor(slot), stains); private delegate ulong FlagSlotForUpdateDelegateIntern(nint drawObject, uint slot, CharacterArmor* data); [Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))] private readonly Hook _flagSlotForUpdateHook = null!; + [Signature(Sigs.FlagBonusSlotForUpdate, DetourName = nameof(FlagBonusSlotForUpdateDetour))] + private readonly Hook _flagBonusSlotForUpdateHook = null!; + + /// Detours the func that makes all FlagSlotForUpdate calls on a gearset change or initial render of a given actor (Only Cases this is Called). + /// Logic done after returning the original hook executes After all equipment/weapon/crest data is loaded into the Actors BaseData. + /// + private delegate ulong LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData); + private readonly Hook _loadGearsetDataHook; + private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { var slot = slotIdx.ToEquipSlot(); var returnValue = ulong.MaxValue; - SlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); + EquipSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); Glamourer.Log.Excessive($"[FlagSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); return returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; } + private ulong FlagBonusSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) + { + var slot = slotIdx.ToBonusSlot(); + var returnValue = ulong.MaxValue; + BonusSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); + Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); + return returnValue == ulong.MaxValue ? _flagBonusSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; + } + private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor) - => _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); + { + Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Glamourer-Invoked on 0x{drawObject.Address:X} on {slot} with item data {armor}."); + return _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); + } + private ulong LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData) + { + var ret = _loadGearsetDataHook.Original(drawDataContainer, gearsetData); + var drawObject = drawDataContainer->OwnerObject->DrawObject; + GearsetDataLoadedEvent.Invoke(drawDataContainer->OwnerObject, drawObject); + Glamourer.Log.Excessive($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); + return ret; + } + + + private static string FormatGearsetItemDataStruct(PacketPlayerGearsetData gearsetData) + { + var ret = + $"\nMainhandWeaponData: Id: {gearsetData.MainhandWeaponData.Id}, Type: {gearsetData.MainhandWeaponData.Type}, " + + $"Variant: {gearsetData.MainhandWeaponData.Variant}, Stain0: {gearsetData.MainhandWeaponData.Stain0}, Stain1: {gearsetData.MainhandWeaponData.Stain1}" + + $"\nOffhandWeaponData: Id: {gearsetData.OffhandWeaponData.Id}, Type: {gearsetData.OffhandWeaponData.Type}, " + + $"Variant: {gearsetData.OffhandWeaponData.Variant}, Stain0: {gearsetData.OffhandWeaponData.Stain0}, Stain1: {gearsetData.OffhandWeaponData.Stain1}" + + $"\nCrestBitField: {gearsetData.CrestBitField} | JobId: {gearsetData.JobId}"; + for (var offset = 20; offset <= 56; offset += sizeof(LegacyCharacterArmor)) + { + var equipSlotPtr = (LegacyCharacterArmor*)((byte*)&gearsetData + offset); + var dyeOffset = (offset - 20) / sizeof(LegacyCharacterArmor) + 60; // Calculate the corresponding dye offset + var dyePtr = (byte*)&gearsetData + dyeOffset; + ret += $"\nEquipSlot {(EquipSlot)(dyeOffset - 60)}:: Id: {(*equipSlotPtr).Set}, Variant: {(*equipSlotPtr).Variant}, Stain0: {(*equipSlotPtr).Stain.Id}, Stain1: {*dyePtr}"; + } + return ret; + } } diff --git a/Glamourer/Interop/VieraEarService.cs b/Glamourer/Interop/VieraEarService.cs new file mode 100644 index 0000000..a6afd1d --- /dev/null +++ b/Glamourer/Interop/VieraEarService.cs @@ -0,0 +1,83 @@ +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using Glamourer.Events; +using Penumbra.GameData; +using Penumbra.GameData.Interop; + +namespace Glamourer.Interop; + +public unsafe class VieraEarService : IDisposable +{ + private readonly PenumbraReloaded _penumbra; + private readonly IGameInteropProvider _interop; + public readonly VieraEarStateChanged Event; + + public VieraEarService(VieraEarStateChanged visorStateChanged, IGameInteropProvider interop, PenumbraReloaded penumbra) + { + _interop = interop; + _penumbra = penumbra; + Event = visorStateChanged; + _setupVieraEarHook = Create(); + _penumbra.Subscribe(Restore, PenumbraReloaded.Priority.VieraEarService); + } + + public void Dispose() + { + _setupVieraEarHook.Dispose(); + _penumbra.Unsubscribe(Restore); + } + + /// Obtain the current state of viera ears for the given draw object (true: toggled). + public static unsafe bool GetVieraEarState(Model characterBase) + => characterBase is { IsCharacterBase: true, VieraEarsVisible: true }; + + /// Manually set the state of the Visor for the given draw object. + /// The draw object. + /// The desired state (true: toggled). + /// Whether the state was changed. + public bool SetVieraEarState(Model human, bool on) + { + if (!human.IsHuman) + return false; + + var oldState = GetVieraEarState(human); + Glamourer.Log.Verbose($"[SetVieraEarState] Invoked manually on 0x{human.Address:X} switching from {oldState} to {on}."); + if (oldState == on) + return false; + + human.VieraEarsVisible = on; + return true; + } + + private delegate void UpdateVieraEarDelegateInternal(DrawDataContainer* drawData, byte on); + + private Hook _setupVieraEarHook; + + private void SetupVieraEarDetour(DrawDataContainer* drawData, byte value) + { + Actor actor = drawData->OwnerObject; + var originalOn = value != 0; + var on = originalOn; + // Invoke an event that can change the requested value + Event.Invoke(actor, ref on); + + Glamourer.Log.Verbose( + $"[SetVieraEarState] Invoked from game on 0x{actor.Address:X} switching to {on} (original {originalOn} from {value})."); + + _setupVieraEarHook.Original(drawData, on ? (byte)1 : (byte)0); + } + + private unsafe Hook Create() + { + var hook = _interop.HookFromSignature(Sigs.SetupVieraEars, SetupVieraEarDetour); + hook.Enable(); + return hook; + } + + private void Restore() + { + _setupVieraEarHook.Dispose(); + _setupVieraEarHook = Create(); + } +} diff --git a/Glamourer/Interop/VisorService.cs b/Glamourer/Interop/VisorService.cs index 2b49eba..83262e4 100644 --- a/Glamourer/Interop/VisorService.cs +++ b/Glamourer/Interop/VisorService.cs @@ -1,29 +1,32 @@ -using System; -using System.Runtime.CompilerServices; -using Dalamud.Hooking; +using Dalamud.Hooking; using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Events; -using Glamourer.Interop.Structs; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; namespace Glamourer.Interop; public class VisorService : IDisposable { - public readonly VisorStateChanged Event; + private readonly PenumbraReloaded _penumbra; + private readonly IGameInteropProvider _interop; + public readonly VisorStateChanged Event; - public unsafe VisorService(VisorStateChanged visorStateChanged, IGameInteropProvider interop) + public VisorService(VisorStateChanged visorStateChanged, IGameInteropProvider interop, PenumbraReloaded penumbra) { + _interop = interop; + _penumbra = penumbra; Event = visorStateChanged; - _setupVisorHook = interop.HookFromAddress((nint)Human.MemberFunctionPointers.SetupVisor, SetupVisorDetour); - interop.InitializeFromAttributes(this); - _setupVisorHook.Enable(); + _setupVisorHook = Create(); + _penumbra.Subscribe(Restore, PenumbraReloaded.Priority.VisorService); } public void Dispose() - => _setupVisorHook.Dispose(); + { + _setupVisorHook.Dispose(); + _penumbra.Unsubscribe(Restore); + } /// Obtain the current state of the Visor for the given draw object (true: toggled). public static unsafe bool GetVisorState(Model characterBase) @@ -33,7 +36,7 @@ public class VisorService : IDisposable /// The draw object. /// The desired state (true: toggled). /// Whether the state was changed. - public bool SetVisorState(Model human, bool on) + public unsafe bool SetVisorState(Model human, bool on) { if (!human.IsHuman) return false; @@ -43,25 +46,28 @@ public class VisorService : IDisposable if (oldState == on) return false; + // No clue what this flag does, but it's necessary for toggling static visors on or off, e.g. Alternate Cap (6229-1). + human.AsHuman->StateFlags |= (CharacterBase.StateFlag)0x40000000; SetupVisorDetour(human, human.GetArmor(EquipSlot.Head).Set.Id, on); return true; } - private delegate void UpdateVisorDelegateInternal(nint humanPtr, ushort modelId, bool on); + private delegate void UpdateVisorDelegateInternal(nint humanPtr, ushort modelId, byte on); - private readonly Hook _setupVisorHook; + private Hook _setupVisorHook; - private void SetupVisorDetour(nint human, ushort modelId, bool on) + private void SetupVisorDetour(nint human, ushort modelId, byte value) { - var originalOn = on; + var originalOn = value != 0; + var on = originalOn; // Invoke an event that can change the requested value // and also control whether the function should be called at all. - Event.Invoke(human, ref on); + Event.Invoke(human, false, ref on); - Glamourer.Log.Excessive( - $"[SetVisorState] Invoked from game on 0x{human:X} switching to {on} (original {originalOn})."); + Glamourer.Log.Verbose( + $"[SetVisorState] Invoked from game on 0x{human:X} switching to {on} (original {originalOn} from {value} with {modelId})."); - SetupVisorDetour((Model)human, modelId, on); + SetupVisorDetour(human, modelId, on); } /// @@ -73,6 +79,19 @@ public class VisorService : IDisposable private unsafe void SetupVisorDetour(Model human, ushort modelId, bool on) { human.AsCharacterBase->VisorToggled = on; - _setupVisorHook.Original(human.Address, modelId, on); + _setupVisorHook.Original(human.Address, modelId, on ? (byte)1 : (byte)0); + } + + private unsafe Hook Create() + { + var hook = _interop.HookFromAddress((nint)Human.MemberFunctionPointers.SetupVisor, SetupVisorDetour); + hook.Enable(); + return hook; + } + + private void Restore() + { + _setupVisorHook.Dispose(); + _setupVisorHook = Create(); } } diff --git a/Glamourer/Interop/WeaponService.cs b/Glamourer/Interop/WeaponService.cs index 708377b..54f318b 100644 --- a/Glamourer/Interop/WeaponService.cs +++ b/Glamourer/Interop/WeaponService.cs @@ -1,31 +1,34 @@ -using System; -using Dalamud.Hooking; +using Dalamud.Hooking; using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using Glamourer.Events; -using Glamourer.Interop.Structs; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.Interop; public unsafe class WeaponService : IDisposable { - private readonly WeaponLoading _event; + private readonly WeaponLoading _event; + private readonly ThreadLocal _inUpdate = new(() => false); + + private readonly delegate* unmanaged[Stdcall] + _original; public WeaponService(WeaponLoading @event, IGameInteropProvider interop) { _event = @event; _loadWeaponHook = interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour); + _original = + (delegate* unmanaged[Stdcall] < DrawDataContainer*, uint, ulong, byte, byte, byte, byte, int, void >) + DrawDataContainer.MemberFunctionPointers.LoadWeapon; _loadWeaponHook.Enable(); } public void Dispose() - { - _loadWeaponHook.Dispose(); - } + => _loadWeaponHook.Dispose(); // Weapons for a specific character are reloaded with this function. // slot is 0 for main hand, 1 for offhand, 2 for combat effects. @@ -33,39 +36,51 @@ public unsafe class WeaponService : IDisposable // redrawOnEquality controls whether the game does anything if the new weapon is identical to the old one. // skipGameObject seems to control whether the new weapons are written to the game object or just influence the draw object. (1 = skip, 0 = change) // unk4 seemed to be the same as unk1. + // unk5 is new in 7.30 and is checked at the beginning of the function to call some timeline related function. private delegate void LoadWeaponDelegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, - byte skipGameObject, byte unk4); + byte skipGameObject, byte unk4, byte unk5); private readonly Hook _loadWeaponHook; - private void LoadWeaponDetour(DrawDataContainer* drawData, uint slot, ulong weaponValue, byte redrawOnEquality, byte unk2, - byte skipGameObject, byte unk4) + byte skipGameObject, byte unk4, byte unk5) { - var actor = (Actor)((nint*)drawData)[1]; - var weapon = new CharacterWeapon(weaponValue); - var equipSlot = slot switch + if (!_inUpdate.Value) { - 0 => EquipSlot.MainHand, - 1 => EquipSlot.OffHand, - _ => EquipSlot.Unknown, - }; + var actor = (Actor)((nint*)drawData)[1]; + var weapon = new CharacterWeapon(weaponValue); + var equipSlot = slot switch + { + 0 => EquipSlot.MainHand, + 1 => EquipSlot.OffHand, + _ => EquipSlot.Unknown, + }; - var tmpWeapon = weapon; - // First call the regular function. - if (equipSlot is not EquipSlot.Unknown) - _event.Invoke(actor, equipSlot, ref tmpWeapon); + var tmpWeapon = weapon; + // First call the regular function. + if (equipSlot is not EquipSlot.Unknown) + _event.Invoke(actor, equipSlot, ref tmpWeapon); + // Sage hack for weapons appearing in animations? + // Check for weapon value 0 for certain cases (e.g. carbuncles transforming to humans) because that breaks some stuff (weapon hiding?) otherwise. + else if (weaponValue == actor.GetMainhand().Value && weaponValue != 0) + _event.Invoke(actor, EquipSlot.MainHand, ref tmpWeapon); - _loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4); - if (tmpWeapon.Value != weapon.Value) - { - if (tmpWeapon.Set.Id == 0) - tmpWeapon.Stain = 0; - _loadWeaponHook.Original(drawData, slot, tmpWeapon.Value, 1, unk2, 1, unk4); + _loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4, unk5); + + if (tmpWeapon.Value != weapon.Value) + { + if (tmpWeapon.Skeleton.Id == 0) + tmpWeapon.Stains = StainIds.None; + _loadWeaponHook.Original(drawData, slot, tmpWeapon.Value, 1, unk2, 1, unk4, unk5); + } + + Glamourer.Log.Excessive( + $"Weapon reloaded for 0x{actor.Address:X} ({actor.Utf8Name}) with attributes {slot} {weapon.Value:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}, {unk5}"); + } + else + { + _loadWeaponHook.Original(drawData, slot, weaponValue, redrawOnEquality, unk2, skipGameObject, unk4, unk5); } - - Glamourer.Log.Excessive( - $"Weapon reloaded for 0x{actor.Address:X} ({actor.Utf8Name}) with attributes {slot} {weapon.Value:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}"); } // Load a specific weapon for a character by its data and slot. @@ -74,25 +89,30 @@ public unsafe class WeaponService : IDisposable switch (slot) { case EquipSlot.MainHand: - _loadWeaponHook.Original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0); + _inUpdate.Value = true; + _original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0, 0); + _inUpdate.Value = false; return; case EquipSlot.OffHand: - _loadWeaponHook.Original(&character.AsCharacter->DrawData, 1, weapon.Value, 1, 0, 1, 0); + _inUpdate.Value = true; + _original(&character.AsCharacter->DrawData, 1, weapon.Value, 1, 0, 1, 0, 0); + _inUpdate.Value = false; return; case EquipSlot.BothHand: - _loadWeaponHook.Original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0); - _loadWeaponHook.Original(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 1, 0, 1, 0); + _inUpdate.Value = true; + _original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0, 0); + _original(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 1, 0, 1, 0, 0); + _inUpdate.Value = false; return; - // function can also be called with '2', but does not seem to ever be. } } - public void LoadStain(Actor character, EquipSlot slot, StainId stain) + public void LoadStain(Actor character, EquipSlot slot, StainIds stains) { var mdl = character.Model; var (_, _, mh, oh) = mdl.GetWeapons(character); var value = slot == EquipSlot.OffHand ? oh : mh; - var weapon = value.With(value.Set.Id == 0 ? 0 : stain); + var weapon = value.With(value.Skeleton.Id == 0 ? StainIds.None : stains); LoadWeapon(character, slot, weapon); } } diff --git a/Glamourer/Services/BackupService.cs b/Glamourer/Services/BackupService.cs index dfccb2a..511cca6 100644 --- a/Glamourer/Services/BackupService.cs +++ b/Glamourer/Services/BackupService.cs @@ -1,11 +1,10 @@ -using System.Collections.Generic; -using System.IO; using OtterGui.Classes; using OtterGui.Log; +using OtterGui.Services; namespace Glamourer.Services; -public class BackupService +public class BackupService : IAsyncService { private readonly Logger _logger; private readonly DirectoryInfo _configDirectory; @@ -16,7 +15,7 @@ public class BackupService _logger = logger; _fileNames = GlamourerFiles(fileNames); _configDirectory = new DirectoryInfo(fileNames.ConfigDirectory); - Backup.CreateAutomaticBackup(logger, _configDirectory, _fileNames); + Awaiter = Task.Run(() => Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), _fileNames)); } /// Create a permanent backup with a given name for migrations. @@ -35,10 +34,16 @@ public class BackupService new(fileNames.UnlockFileCustomize), new(fileNames.UnlockFileItems), new(fileNames.FavoriteFile), + new(fileNames.DesignColorFile), }; list.AddRange(fileNames.Designs()); return list; } + + public Task Awaiter { get; } + + public bool Finished + => Awaiter.IsCompletedSuccessfully; } diff --git a/Glamourer/Services/CodeService.cs b/Glamourer/Services/CodeService.cs index f065e50..4a82f0e 100644 --- a/Glamourer/Services/CodeService.cs +++ b/Glamourer/Services/CodeService.cs @@ -1,8 +1,3 @@ -using System; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Text; using Penumbra.GameData.Enums; namespace Glamourer.Services; @@ -12,23 +7,83 @@ public class CodeService private readonly Configuration _config; private readonly SHA256 _hasher = SHA256.Create(); - public enum Sizing + [Flags] + public enum CodeFlag : ulong { - None, - Dwarf, - Giant, + Clown = 0x000001, + Emperor = 0x000002, + Individual = 0x000004, + Dwarf = 0x000008, + Giant = 0x000010, + OopsHyur = 0x000020, + OopsElezen = 0x000040, + OopsLalafell = 0x000080, + OopsMiqote = 0x000100, + OopsRoegadyn = 0x000200, + OopsAuRa = 0x000400, + OopsHrothgar = 0x000800, + OopsViera = 0x001000, + //Artisan = 0x002000, + SixtyThree = 0x004000, + Shirts = 0x008000, + World = 0x010000, + Elephants = 0x020000, + Crown = 0x040000, + Dolphins = 0x080000, + Face = 0x100000, + Manderville = 0x200000, + Smiles = 0x400000, } - public bool EnabledClown { get; private set; } - public bool EnabledEmperor { get; private set; } - public bool EnabledIndividual { get; private set; } - public Sizing EnabledSizing { get; private set; } - public Race EnabledOops { get; private set; } - public bool EnabledArtisan { get; private set; } - public bool EnabledCaptain { get; private set; } - public bool Enabled63 { get; private set; } - public bool EnabledShirts { get; private set; } - public bool EnabledWorld { get; private set; } + public static readonly CodeFlag AllHintCodes = + Enum.GetValues().Where(f => GetData(f).Display).Aggregate((CodeFlag)0, (f1, f2) => f1 | f2); + + public const CodeFlag DyeCodes = + CodeFlag.Clown | CodeFlag.World | CodeFlag.Elephants | CodeFlag.Dolphins; + + public const CodeFlag GearCodes = + CodeFlag.Emperor | CodeFlag.World | CodeFlag.Elephants | CodeFlag.Dolphins; + + public const CodeFlag RaceCodes = CodeFlag.OopsHyur + | CodeFlag.OopsElezen + | CodeFlag.OopsLalafell + | CodeFlag.OopsMiqote + | CodeFlag.OopsRoegadyn + | CodeFlag.OopsAuRa + | CodeFlag.OopsHrothgar + | CodeFlag.OopsViera; + + public const CodeFlag FullCodes = CodeFlag.Face | CodeFlag.Manderville | CodeFlag.Smiles; + + public const CodeFlag SizeCodes = CodeFlag.Dwarf | CodeFlag.Giant; + + private CodeFlag _enabled; + + public CodeFlag AllEnabled + => _enabled; + + public bool Enabled(CodeFlag flag) + => _enabled.HasFlag(flag); + + public bool AnyEnabled(CodeFlag flag) + => (_enabled & flag) != 0; + + public CodeFlag Masked(CodeFlag mask) + => _enabled & mask; + + public Race GetRace() + => (_enabled & RaceCodes) switch + { + CodeFlag.OopsHyur => Race.Hyur, + CodeFlag.OopsElezen => Race.Elezen, + CodeFlag.OopsLalafell => Race.Lalafell, + CodeFlag.OopsMiqote => Race.Miqote, + CodeFlag.OopsRoegadyn => Race.Roegadyn, + CodeFlag.OopsAuRa => Race.AuRa, + CodeFlag.OopsHrothgar => Race.Hrothgar, + CodeFlag.OopsViera => Race.Viera, + _ => Race.Unknown, + }; public CodeService(Configuration config) { @@ -41,7 +96,7 @@ public class CodeService var changes = false; for (var i = 0; i < _config.Codes.Count; ++i) { - var enabled = CheckCode(_config.Codes[i].Code); + var enabled = CheckCode(_config.Codes[i].Code).Item1; if (enabled == null) { _config.Codes.RemoveAt(i--); @@ -60,7 +115,7 @@ public class CodeService public bool AddCode(string name) { - if (CheckCode(name) == null || _config.Codes.Any(p => p.Code == name)) + if (CheckCode(name).Item1 is null || _config.Codes.Any(p => p.Code == name)) return false; _config.Codes.Add((name, false)); @@ -68,93 +123,132 @@ public class CodeService return true; } - public Action? CheckCode(string name) + public (Action?, CodeFlag) CheckCode(string name) + { + var flag = GetCode(name); + if (flag == 0) + return (null, 0); + + var badFlags = ~GetMutuallyExclusive(flag); + return (v => _enabled = v ? (_enabled | flag) & badFlags : _enabled & ~flag, flag); + } + + public CodeFlag GetCode(string name) { using var stream = new MemoryStream(Encoding.UTF8.GetBytes(name)); var sha = (ReadOnlySpan)_hasher.ComputeHash(stream); - return sha switch - { - _ when CodeClown.SequenceEqual(sha) => v => EnabledClown = v, - _ when CodeEmperor.SequenceEqual(sha) => v => EnabledEmperor = v, - _ when CodeIndividual.SequenceEqual(sha) => v => EnabledIndividual = v, - _ when CodeCaptain.SequenceEqual(sha) => v => EnabledCaptain = v, - _ when Code63.SequenceEqual(sha) => v => Enabled63 = v, - _ when CodeDwarf.SequenceEqual(sha) => v => EnabledSizing = v ? Sizing.Dwarf : Sizing.None, - _ when CodeGiant.SequenceEqual(sha) => v => EnabledSizing = v ? Sizing.Giant : Sizing.None, - _ when CodeOops1.SequenceEqual(sha) => v => EnabledOops = v ? Race.Hyur : Race.Unknown, - _ when CodeOops2.SequenceEqual(sha) => v => EnabledOops = v ? Race.Elezen : Race.Unknown, - _ when CodeOops3.SequenceEqual(sha) => v => EnabledOops = v ? Race.Lalafell : Race.Unknown, - _ when CodeOops4.SequenceEqual(sha) => v => EnabledOops = v ? Race.Miqote : Race.Unknown, - _ when CodeOops5.SequenceEqual(sha) => v => EnabledOops = v ? Race.Roegadyn : Race.Unknown, - _ when CodeOops6.SequenceEqual(sha) => v => EnabledOops = v ? Race.AuRa : Race.Unknown, - _ when CodeOops7.SequenceEqual(sha) => v => EnabledOops = v ? Race.Hrothgar : Race.Unknown, - _ when CodeOops8.SequenceEqual(sha) => v => EnabledOops = v ? Race.Viera : Race.Unknown, - _ when CodeArtisan.SequenceEqual(sha) => v => EnabledArtisan = v, - _ when CodeShirts.SequenceEqual(sha) => v => EnabledShirts = v, - _ when CodeWorld.SequenceEqual(sha) => v => EnabledWorld = v, - _ => null, - }; + foreach (var flag in Enum.GetValues()) + { + if (sha.SequenceEqual(GetSha(flag))) + return flag; + } + + return 0; } - public void VerifyState() + /// Update all enabled states in the config. + public void SaveState() { - if (EnabledSizing == Sizing.None && EnabledOops == Race.Unknown) - return; - for (var i = 0; i < _config.Codes.Count; ++i) { - var (code, enabled) = _config.Codes[i]; - if (!enabled) - continue; - - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(code)); - var sha = (ReadOnlySpan)_hasher.ComputeHash(stream); - var _ = EnabledSizing switch + var name = _config.Codes[i].Code; + var flag = GetCode(name); + if (flag == 0) { - Sizing.Dwarf when sha.SequenceEqual(CodeGiant) => _config.Codes[i] = (code, false), - Sizing.Giant when sha.SequenceEqual(CodeDwarf) => _config.Codes[i] = (code, false), - _ => (string.Empty, false), - }; + _config.Codes.RemoveAt(i--); + continue; + } - var race = OopsRace(sha); - if (race is not Race.Unknown && race != EnabledOops) - _config.Codes[i] = (code, false); + _config.Codes[i] = (name, Enabled(flag)); } + + _config.Save(); } - private static Race OopsRace(ReadOnlySpan sha) - => sha switch + // @formatter:off + private static CodeFlag GetMutuallyExclusive(CodeFlag flag) + => flag switch { - _ when CodeOops1.SequenceEqual(sha) => Race.Hyur, - _ when CodeOops2.SequenceEqual(sha) => Race.Elezen, - _ when CodeOops3.SequenceEqual(sha) => Race.Lalafell, - _ when CodeOops4.SequenceEqual(sha) => Race.Miqote, - _ when CodeOops5.SequenceEqual(sha) => Race.Roegadyn, - _ when CodeOops6.SequenceEqual(sha) => Race.AuRa, - _ when CodeOops7.SequenceEqual(sha) => Race.Hrothgar, - _ when CodeOops8.SequenceEqual(sha) => Race.Viera, - _ => Race.Unknown, + CodeFlag.Clown => (FullCodes | DyeCodes) & ~CodeFlag.Clown, + CodeFlag.Emperor => (FullCodes | GearCodes) & ~CodeFlag.Emperor, + CodeFlag.Individual => FullCodes, + CodeFlag.Dwarf => (FullCodes | SizeCodes) & ~CodeFlag.Dwarf, + CodeFlag.Giant => (FullCodes | SizeCodes) & ~CodeFlag.Giant, + CodeFlag.OopsHyur => (FullCodes | RaceCodes) & ~CodeFlag.OopsHyur, + CodeFlag.OopsElezen => (FullCodes | RaceCodes) & ~CodeFlag.OopsElezen, + CodeFlag.OopsLalafell => (FullCodes | RaceCodes) & ~CodeFlag.OopsLalafell, + CodeFlag.OopsMiqote => (FullCodes | RaceCodes) & ~CodeFlag.OopsMiqote, + CodeFlag.OopsRoegadyn => (FullCodes | RaceCodes) & ~CodeFlag.OopsRoegadyn, + CodeFlag.OopsAuRa => (FullCodes | RaceCodes) & ~CodeFlag.OopsAuRa, + CodeFlag.OopsHrothgar => (FullCodes | RaceCodes) & ~CodeFlag.OopsHrothgar, + CodeFlag.OopsViera => (FullCodes | RaceCodes) & ~CodeFlag.OopsViera, + CodeFlag.SixtyThree => FullCodes, + CodeFlag.Shirts => 0, + CodeFlag.World => (FullCodes | DyeCodes | GearCodes) & ~CodeFlag.World, + CodeFlag.Elephants => (FullCodes | DyeCodes | GearCodes) & ~CodeFlag.Elephants, + CodeFlag.Crown => FullCodes, + CodeFlag.Dolphins => (FullCodes | DyeCodes | GearCodes) & ~CodeFlag.Dolphins, + CodeFlag.Face => (FullCodes | RaceCodes | SizeCodes | GearCodes | DyeCodes | CodeFlag.Crown | CodeFlag.SixtyThree) & ~CodeFlag.Face, + CodeFlag.Manderville => (FullCodes | RaceCodes | SizeCodes | GearCodes | DyeCodes | CodeFlag.Crown | CodeFlag.SixtyThree) & ~CodeFlag.Manderville, + CodeFlag.Smiles => (FullCodes | RaceCodes | SizeCodes | GearCodes | DyeCodes | CodeFlag.Crown | CodeFlag.SixtyThree) & ~CodeFlag.Smiles, + _ => 0, + }; + + private static ReadOnlySpan GetSha(CodeFlag flag) + => flag switch + { + CodeFlag.Clown => [ 0xC4, 0xEE, 0x1D, 0x6F, 0xC5, 0x5D, 0x47, 0xBE, 0x78, 0x63, 0x66, 0x86, 0x81, 0x15, 0xEB, 0xFA, 0xF6, 0x4A, 0x90, 0xEA, 0xC0, 0xE4, 0xEE, 0x86, 0x69, 0x01, 0x8E, 0xDB, 0xCC, 0x69, 0xD1, 0xBD ], + CodeFlag.Emperor => [ 0xE2, 0x2D, 0x3E, 0x57, 0x16, 0x82, 0x65, 0x98, 0x7E, 0xE6, 0x8F, 0x45, 0x14, 0x7D, 0x65, 0x31, 0xE9, 0xD8, 0xDB, 0xEA, 0xDC, 0xBF, 0xEE, 0x2A, 0xBA, 0xD5, 0x69, 0x96, 0x78, 0x34, 0x3B, 0x57 ], + CodeFlag.Individual => [ 0x95, 0xA4, 0x71, 0xAC, 0xA3, 0xC2, 0x34, 0x94, 0xC1, 0x65, 0x07, 0xF3, 0x7F, 0x93, 0x57, 0xEE, 0xE3, 0x04, 0xC0, 0xE8, 0x1B, 0xA0, 0xE2, 0x08, 0x68, 0x02, 0x8D, 0xAD, 0x76, 0x03, 0x9B, 0xC5 ], + CodeFlag.Dwarf => [ 0x55, 0x97, 0xFE, 0xE9, 0x78, 0x64, 0xE8, 0x2F, 0xCD, 0x25, 0xD1, 0xAE, 0xDF, 0x35, 0xE6, 0xED, 0x03, 0x78, 0x54, 0x1D, 0x56, 0x22, 0x34, 0x75, 0x4B, 0x96, 0x6F, 0xBA, 0xAC, 0xEC, 0x00, 0x46 ], + CodeFlag.Giant => [ 0x6E, 0xBB, 0x91, 0x1D, 0x67, 0xE3, 0x00, 0x07, 0xA1, 0x0F, 0x2A, 0xF0, 0x26, 0x91, 0x38, 0x63, 0xD3, 0x52, 0x82, 0xF7, 0x5D, 0x93, 0xE8, 0x83, 0xB1, 0xF6, 0xB9, 0x69, 0x78, 0x20, 0xC4, 0xCE ], + CodeFlag.OopsHyur => [ 0x4C, 0x51, 0xE2, 0x38, 0xEF, 0xAD, 0x84, 0x0E, 0x4E, 0x11, 0x0F, 0x5E, 0xDE, 0x45, 0x41, 0x9F, 0x6A, 0xF6, 0x5F, 0x5B, 0xA8, 0x91, 0x64, 0x22, 0xEE, 0x62, 0x97, 0x3C, 0x78, 0x18, 0xCD, 0xAF ], + CodeFlag.OopsElezen => [ 0x3D, 0x5B, 0xA9, 0x62, 0xCE, 0xBE, 0x52, 0xF5, 0x94, 0x2A, 0xF9, 0xB7, 0xCF, 0xD9, 0x24, 0x2B, 0x38, 0xC7, 0x4F, 0x28, 0x97, 0x29, 0x1D, 0x01, 0x13, 0x53, 0x44, 0x11, 0x15, 0x6F, 0x9B, 0x56 ], + CodeFlag.OopsLalafell => [ 0x85, 0x8D, 0x5B, 0xC2, 0x66, 0x53, 0x2E, 0xB9, 0xE9, 0x85, 0xE5, 0xF8, 0xD3, 0x75, 0x18, 0x7C, 0x58, 0x55, 0xD4, 0x8C, 0x8E, 0x5F, 0x58, 0x2E, 0xF3, 0xF1, 0xAE, 0xA8, 0xA0, 0x81, 0xC6, 0x0E ], + CodeFlag.OopsMiqote => [ 0x44, 0x73, 0x8C, 0x39, 0x5A, 0xF1, 0xDB, 0x5F, 0x62, 0xA1, 0x6E, 0x5F, 0xE6, 0x97, 0x9E, 0x90, 0xD7, 0x5C, 0x97, 0x67, 0xB6, 0xC7, 0x99, 0x61, 0x36, 0xCA, 0x34, 0x7E, 0xB9, 0xAC, 0xC3, 0x76 ], + CodeFlag.OopsRoegadyn => [ 0xB7, 0x25, 0x73, 0xDB, 0xBE, 0xD0, 0x49, 0xFB, 0xFF, 0x9C, 0x32, 0x21, 0xB0, 0x8A, 0x2C, 0x0C, 0x77, 0x46, 0xD5, 0xCF, 0x0E, 0x63, 0x2F, 0x91, 0x85, 0x8B, 0x55, 0x5C, 0x4D, 0xD2, 0xB9, 0xB8 ], + CodeFlag.OopsAuRa => [ 0x69, 0x93, 0xAF, 0xE4, 0xB8, 0xEC, 0x5F, 0x40, 0xEB, 0x8A, 0x6F, 0xD1, 0x9B, 0xD9, 0x56, 0x0B, 0xEA, 0x64, 0x79, 0x9B, 0x54, 0xA1, 0x41, 0xED, 0xBC, 0x3E, 0x6E, 0x5C, 0xF1, 0x23, 0x60, 0xF8 ], + CodeFlag.OopsHrothgar => [ 0x41, 0xEC, 0x65, 0x05, 0x8D, 0x20, 0x68, 0x5A, 0xB7, 0xEB, 0x92, 0x15, 0x43, 0xCF, 0x15, 0x05, 0x27, 0x51, 0xFE, 0x20, 0xC9, 0xB6, 0x2B, 0x84, 0xD9, 0x6A, 0x49, 0x5A, 0x5B, 0x7F, 0x2E, 0xE7 ], + CodeFlag.OopsViera => [ 0x16, 0xFF, 0x63, 0x85, 0x1C, 0xF5, 0x34, 0x33, 0x67, 0x8C, 0x46, 0x8E, 0x3E, 0xE3, 0xA6, 0x94, 0xF9, 0x74, 0x47, 0xAA, 0xC7, 0x29, 0x59, 0x1F, 0x6C, 0x6E, 0xF2, 0xF5, 0x87, 0x24, 0x9E, 0x2B ], + CodeFlag.SixtyThree => [ 0xA1, 0x65, 0x60, 0x99, 0xB0, 0x9F, 0xBF, 0xD7, 0x20, 0xC8, 0x29, 0xF6, 0x7B, 0x86, 0x27, 0xF5, 0xBE, 0xA9, 0x5B, 0xB0, 0x20, 0x5E, 0x57, 0x7B, 0xFF, 0xBC, 0x1E, 0x8C, 0x04, 0xF9, 0x35, 0xD3 ], + CodeFlag.Shirts => [ 0xD1, 0x35, 0xD7, 0x18, 0xBE, 0x45, 0x42, 0xBD, 0x88, 0x77, 0x7E, 0xC4, 0x41, 0x06, 0x34, 0x4D, 0x71, 0x3A, 0xC5, 0xCC, 0xA4, 0x1B, 0x7D, 0x3F, 0x3B, 0x86, 0x07, 0xCB, 0x63, 0xD7, 0xF9, 0xDB ], + CodeFlag.World => [ 0xFD, 0xA2, 0xD2, 0xBC, 0xD9, 0x8A, 0x7E, 0x2B, 0x52, 0xCB, 0x57, 0x6E, 0x3A, 0x2E, 0x30, 0xBA, 0x4E, 0xAE, 0x42, 0xEA, 0x5C, 0x57, 0xDF, 0x17, 0x37, 0x3C, 0xCE, 0x17, 0x42, 0x43, 0xAE, 0xD0 ], + CodeFlag.Elephants => [ 0x9F, 0x4C, 0xCF, 0x6D, 0xC4, 0x01, 0x31, 0x46, 0x02, 0x05, 0x31, 0xED, 0xED, 0xB2, 0x66, 0x29, 0x31, 0x09, 0x1E, 0xE7, 0x47, 0xDE, 0x7B, 0x03, 0xB0, 0x3C, 0x06, 0x76, 0x26, 0x91, 0xDF, 0xB2 ], + CodeFlag.Crown => [ 0x43, 0x8E, 0x34, 0x56, 0x24, 0xC9, 0xC6, 0xDE, 0x2A, 0x68, 0x3A, 0x5D, 0xF5, 0x8E, 0xCB, 0xEF, 0x0D, 0x4D, 0x5B, 0xDC, 0x23, 0xF9, 0xF9, 0xBD, 0xD9, 0x60, 0xAD, 0x53, 0xC5, 0xA0, 0x33, 0xC4 ], + CodeFlag.Dolphins => [ 0x64, 0xC6, 0x2E, 0x7C, 0x22, 0x3A, 0x42, 0xF5, 0xC3, 0x93, 0x4F, 0x70, 0x1F, 0xFD, 0xFA, 0x3C, 0x98, 0xD2, 0x7C, 0xD8, 0x88, 0xA7, 0x3D, 0x1D, 0x0D, 0xD6, 0x70, 0x15, 0x28, 0x2E, 0x79, 0xE7 ], + CodeFlag.Face => [ 0xCA, 0x97, 0x81, 0x12, 0xCA, 0x1B, 0xBD, 0xCA, 0xFA, 0xC2, 0x31, 0xB3, 0x9B, 0x23, 0xDC, 0x4D, 0xA7, 0x86, 0xEF, 0xF8, 0x14, 0x7C, 0x4E, 0x72, 0xB9, 0x80, 0x77, 0x85, 0xAF, 0xEE, 0x48, 0xBB ], + CodeFlag.Manderville => [ 0x3E, 0x23, 0xE8, 0x16, 0x00, 0x39, 0x59, 0x4A, 0x33, 0x89, 0x4F, 0x65, 0x65, 0xE1, 0xB1, 0x34, 0x8B, 0xBD, 0x7A, 0x00, 0x88, 0xD4, 0x2C, 0x4A, 0xCB, 0x73, 0xEE, 0xAE, 0xD5, 0x9C, 0x00, 0x9D ], + CodeFlag.Smiles => [ 0x2E, 0x7D, 0x2C, 0x03, 0xA9, 0x50, 0x7A, 0xE2, 0x65, 0xEC, 0xF5, 0xB5, 0x36, 0x68, 0x85, 0xA5, 0x33, 0x93, 0xA2, 0x02, 0x9D, 0x24, 0x13, 0x94, 0x99, 0x72, 0x65, 0xA1, 0xA2, 0x5A, 0xEF, 0xC6 ], + _ => [], }; - // @formatter:off - private static ReadOnlySpan CodeClown => new byte[] { 0xC4, 0xEE, 0x1D, 0x6F, 0xC5, 0x5D, 0x47, 0xBE, 0x78, 0x63, 0x66, 0x86, 0x81, 0x15, 0xEB, 0xFA, 0xF6, 0x4A, 0x90, 0xEA, 0xC0, 0xE4, 0xEE, 0x86, 0x69, 0x01, 0x8E, 0xDB, 0xCC, 0x69, 0xD1, 0xBD }; - private static ReadOnlySpan CodeEmperor => new byte[] { 0xE2, 0x2D, 0x3E, 0x57, 0x16, 0x82, 0x65, 0x98, 0x7E, 0xE6, 0x8F, 0x45, 0x14, 0x7D, 0x65, 0x31, 0xE9, 0xD8, 0xDB, 0xEA, 0xDC, 0xBF, 0xEE, 0x2A, 0xBA, 0xD5, 0x69, 0x96, 0x78, 0x34, 0x3B, 0x57 }; - private static ReadOnlySpan CodeIndividual => new byte[] { 0x95, 0xA4, 0x71, 0xAC, 0xA3, 0xC2, 0x34, 0x94, 0xC1, 0x65, 0x07, 0xF3, 0x7F, 0x93, 0x57, 0xEE, 0xE3, 0x04, 0xC0, 0xE8, 0x1B, 0xA0, 0xE2, 0x08, 0x68, 0x02, 0x8D, 0xAD, 0x76, 0x03, 0x9B, 0xC5 }; - private static ReadOnlySpan CodeDwarf => new byte[] { 0x55, 0x97, 0xFE, 0xE9, 0x78, 0x64, 0xE8, 0x2F, 0xCD, 0x25, 0xD1, 0xAE, 0xDF, 0x35, 0xE6, 0xED, 0x03, 0x78, 0x54, 0x1D, 0x56, 0x22, 0x34, 0x75, 0x4B, 0x96, 0x6F, 0xBA, 0xAC, 0xEC, 0x00, 0x46 }; - private static ReadOnlySpan CodeGiant => new byte[] { 0x6E, 0xBB, 0x91, 0x1D, 0x67, 0xE3, 0x00, 0x07, 0xA1, 0x0F, 0x2A, 0xF0, 0x26, 0x91, 0x38, 0x63, 0xD3, 0x52, 0x82, 0xF7, 0x5D, 0x93, 0xE8, 0x83, 0xB1, 0xF6, 0xB9, 0x69, 0x78, 0x20, 0xC4, 0xCE }; - private static ReadOnlySpan CodeOops1 => new byte[] { 0x4C, 0x51, 0xE2, 0x38, 0xEF, 0xAD, 0x84, 0x0E, 0x4E, 0x11, 0x0F, 0x5E, 0xDE, 0x45, 0x41, 0x9F, 0x6A, 0xF6, 0x5F, 0x5B, 0xA8, 0x91, 0x64, 0x22, 0xEE, 0x62, 0x97, 0x3C, 0x78, 0x18, 0xCD, 0xAF }; - private static ReadOnlySpan CodeOops2 => new byte[] { 0x3D, 0x5B, 0xA9, 0x62, 0xCE, 0xBE, 0x52, 0xF5, 0x94, 0x2A, 0xF9, 0xB7, 0xCF, 0xD9, 0x24, 0x2B, 0x38, 0xC7, 0x4F, 0x28, 0x97, 0x29, 0x1D, 0x01, 0x13, 0x53, 0x44, 0x11, 0x15, 0x6F, 0x9B, 0x56 }; - private static ReadOnlySpan CodeOops3 => new byte[] { 0x85, 0x8D, 0x5B, 0xC2, 0x66, 0x53, 0x2E, 0xB9, 0xE9, 0x85, 0xE5, 0xF8, 0xD3, 0x75, 0x18, 0x7C, 0x58, 0x55, 0xD4, 0x8C, 0x8E, 0x5F, 0x58, 0x2E, 0xF3, 0xF1, 0xAE, 0xA8, 0xA0, 0x81, 0xC6, 0x0E }; - private static ReadOnlySpan CodeOops4 => new byte[] { 0x44, 0x73, 0x8C, 0x39, 0x5A, 0xF1, 0xDB, 0x5F, 0x62, 0xA1, 0x6E, 0x5F, 0xE6, 0x97, 0x9E, 0x90, 0xD7, 0x5C, 0x97, 0x67, 0xB6, 0xC7, 0x99, 0x61, 0x36, 0xCA, 0x34, 0x7E, 0xB9, 0xAC, 0xC3, 0x76 }; - private static ReadOnlySpan CodeOops5 => new byte[] { 0xB7, 0x25, 0x73, 0xDB, 0xBE, 0xD0, 0x49, 0xFB, 0xFF, 0x9C, 0x32, 0x21, 0xB0, 0x8A, 0x2C, 0x0C, 0x77, 0x46, 0xD5, 0xCF, 0x0E, 0x63, 0x2F, 0x91, 0x85, 0x8B, 0x55, 0x5C, 0x4D, 0xD2, 0xB9, 0xB8 }; - private static ReadOnlySpan CodeOops6 => new byte[] { 0x69, 0x93, 0xAF, 0xE4, 0xB8, 0xEC, 0x5F, 0x40, 0xEB, 0x8A, 0x6F, 0xD1, 0x9B, 0xD9, 0x56, 0x0B, 0xEA, 0x64, 0x79, 0x9B, 0x54, 0xA1, 0x41, 0xED, 0xBC, 0x3E, 0x6E, 0x5C, 0xF1, 0x23, 0x60, 0xF8 }; - private static ReadOnlySpan CodeOops7 => new byte[] { 0x41, 0xEC, 0x65, 0x05, 0x8D, 0x20, 0x68, 0x5A, 0xB7, 0xEB, 0x92, 0x15, 0x43, 0xCF, 0x15, 0x05, 0x27, 0x51, 0xFE, 0x20, 0xC9, 0xB6, 0x2B, 0x84, 0xD9, 0x6A, 0x49, 0x5A, 0x5B, 0x7F, 0x2E, 0xE7 }; - private static ReadOnlySpan CodeOops8 => new byte[] { 0x16, 0xFF, 0x63, 0x85, 0x1C, 0xF5, 0x34, 0x33, 0x67, 0x8C, 0x46, 0x8E, 0x3E, 0xE3, 0xA6, 0x94, 0xF9, 0x74, 0x47, 0xAA, 0xC7, 0x29, 0x59, 0x1F, 0x6C, 0x6E, 0xF2, 0xF5, 0x87, 0x24, 0x9E, 0x2B }; - private static ReadOnlySpan CodeArtisan => new byte[] { 0xDE, 0x01, 0x32, 0x1E, 0x7F, 0x22, 0x80, 0x3D, 0x76, 0xDF, 0x74, 0x0E, 0xEC, 0x33, 0xD3, 0xF4, 0x1A, 0x98, 0x9E, 0x9D, 0x22, 0x5C, 0xAC, 0x3B, 0xFE, 0x0B, 0xC2, 0x13, 0xB9, 0x91, 0x24, 0x61 }; - private static ReadOnlySpan CodeCaptain => new byte[] { 0x5E, 0x0B, 0xDD, 0x86, 0x8F, 0x24, 0xDA, 0x49, 0x1A, 0xD2, 0x59, 0xB9, 0x10, 0x38, 0x29, 0x37, 0x99, 0x9D, 0x53, 0xD9, 0x9B, 0x84, 0x91, 0x5B, 0x6C, 0xCE, 0x3E, 0x2A, 0x38, 0x06, 0x47, 0xE6 }; - private static ReadOnlySpan Code63 => new byte[] { 0xA1, 0x65, 0x60, 0x99, 0xB0, 0x9F, 0xBF, 0xD7, 0x20, 0xC8, 0x29, 0xF6, 0x7B, 0x86, 0x27, 0xF5, 0xBE, 0xA9, 0x5B, 0xB0, 0x20, 0x5E, 0x57, 0x7B, 0xFF, 0xBC, 0x1E, 0x8C, 0x04, 0xF9, 0x35, 0xD3 }; - private static ReadOnlySpan CodeShirts => new byte[] { 0xD1, 0x35, 0xD7, 0x18, 0xBE, 0x45, 0x42, 0xBD, 0x88, 0x77, 0x7E, 0xC4, 0x41, 0x06, 0x34, 0x4D, 0x71, 0x3A, 0xC5, 0xCC, 0xA4, 0x1B, 0x7D, 0x3F, 0x3B, 0x86, 0x07, 0xCB, 0x63, 0xD7, 0xF9, 0xDB }; - private static ReadOnlySpan CodeWorld => new byte[] { 0xFD, 0xA2, 0xD2, 0xBC, 0xD9, 0x8A, 0x7E, 0x2B, 0x52, 0xCB, 0x57, 0x6E, 0x3A, 0x2E, 0x30, 0xBA, 0x4E, 0xAE, 0x42, 0xEA, 0x5C, 0x57, 0xDF, 0x17, 0x37, 0x3C, 0xCE, 0x17, 0x42, 0x43, 0xAE, 0xD0 }; - // @formatter:on + public static (bool Display, int CapitalCount, string Punctuation, string Hint, string Effect) GetData(CodeFlag flag) + => flag switch + { + CodeFlag.Clown => (true, 3, ",.", "A punchline uttered by Rorschach.", "Randomizes dyes for every player."), + CodeFlag.Emperor => (true, 1, ".", "A truth about clothes that only a child will speak.", "Randomizes clothing for every player."), + CodeFlag.Individual => (true, 2, "'!'!", "Something an unwilling prophet tries to convince his followers of.", "Randomizes customizations for every player."), + CodeFlag.Dwarf => (true, 1, "!", "A centuries old metaphor about humility and the progress of science.", "Sets the player character to minimum height and all other players to maximum height."), + CodeFlag.Giant => (true, 2, "!", "A Swift renaming of one of the most famous literary openings of all time.", "Sets the player character to maximum height and all other players to minimum height."), + CodeFlag.OopsHyur => (true, 1, "','.", "An alkaline quote attributed to Marilyn Monroe.", "Turns all players to Hyur."), + CodeFlag.OopsElezen => (true, 1, ".", "A line from a Futurama song about the far future.", "Turns all players to Elezen."), + CodeFlag.OopsLalafell => (true, 2, ",!", "The name of a discontinued plugin.", "Turns all players to Lalafell."), + CodeFlag.OopsMiqote => (true, 3, ".", "A Sandman story.", "Turns all players to Miqo'te."), + CodeFlag.OopsRoegadyn => (true, 2, "!", "A line from a Steven Universe song about his desires.", "Turns all players to Roegadyn."), + CodeFlag.OopsAuRa => (true, 1, "',.", "Something a plumber hates to hear, made to something a scaly hates to hear and initial Au Ra designs.", "Turns all players to Au Ra."), + CodeFlag.OopsHrothgar => (true, 1, "',...", "A meme about the attractiveness of anthropomorphic animals.", "Turns all players to Hrothgar."), + CodeFlag.OopsViera => (true, 2, "!'!", "A panicked exclamation about bunny arithmetics.", "Turns all players to Viera."), + CodeFlag.SixtyThree => (true, 2, "", "The title of a famous LGBTQ-related french play and movie.", "Inverts the gender of every player."), + CodeFlag.Shirts => (true, 2, "-.", "A pre-internet meme about disappointing rewards for an adventure, adapted to this specific cheat code.", "Highlights all items in the Unlocks tab as if they were unlocked."), + CodeFlag.World => (true, 1, ",.", "A quote about being more important than other people.", "Sets every player except the player character themselves to job-appropriate gear."), + CodeFlag.Elephants => (true, 1, "!", "Appropriate lyrics that can also be found in Glamourer's changelogs.", "Sets every player to the elephant costume in varying shades of pink."), + CodeFlag.Crown => (true, 1, ".", "A famous Shakespearean line.", "Sets every player with a mentor symbol enabled to the clown's hat."), + CodeFlag.Dolphins => (true, 5, ",", "The farewell of the second smartest species on Earth.", "Sets every player to a Namazu hat with different costume bodies."), + CodeFlag.Face => (false, 3, ",,!", string.Empty, "Enable a debugging mode for the UI. Not really useful."), + CodeFlag.Manderville => (false, 3, ",,!", string.Empty, "Enable a debugging mode for the UI. Not really useful."), + CodeFlag.Smiles => (false, 3, ",,!", string.Empty, "Enable a debugging mode for the UI. Not really useful."), + _ => (false, 0, string.Empty, string.Empty, string.Empty), + }; } + diff --git a/Glamourer/Services/CollectionOverrideService.cs b/Glamourer/Services/CollectionOverrideService.cs new file mode 100644 index 0000000..99635d8 --- /dev/null +++ b/Glamourer/Services/CollectionOverrideService.cs @@ -0,0 +1,221 @@ +using Dalamud.Interface.ImGuiNotification; +using Glamourer.Interop.Penumbra; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Extensions; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; +using Notification = OtterGui.Classes.Notification; + +namespace Glamourer.Services; + +public sealed class CollectionOverrideService : IService, ISavable +{ + public const int Version = 2; + private readonly SaveService _saveService; + private readonly ActorManager _actors; + private readonly PenumbraService _penumbra; + + public CollectionOverrideService(SaveService saveService, ActorManager actors, PenumbraService penumbra) + { + _saveService = saveService; + _actors = actors; + _penumbra = penumbra; + Load(); + } + + public unsafe (Guid CollectionId, string Display, bool Overriden) GetCollection(Actor actor, ActorIdentifier identifier = default) + { + if (!identifier.IsValid) + identifier = _actors.FromObject(actor.AsObject, out _, true, true, true); + + return _overrides.FindFirst(p => p.Actor.Matches(identifier), out var ret) + ? (ret.CollectionId, ret.DisplayName, true) + : (_penumbra.GetActorCollection(actor, out var name), name, false); + } + + private readonly List<(ActorIdentifier Actor, Guid CollectionId, string DisplayName)> _overrides = []; + + public IReadOnlyList<(ActorIdentifier Actor, Guid CollectionId, string DisplayName)> Overrides + => _overrides; + + public string ToFilename(FilenameService fileNames) + => fileNames.CollectionOverrideFile; + + public void AddOverride(IEnumerable identifiers, Guid collectionId, string displayName) + { + if (collectionId == Guid.Empty) + return; + + foreach (var id in identifiers.Where(i => i.IsValid)) + { + _overrides.Add((id, collectionId, displayName)); + Glamourer.Log.Debug($"Added collection override {id.Incognito(null)} -> {collectionId}."); + _saveService.QueueSave(this); + } + } + + public (bool Exists, ActorIdentifier Identifier, Guid CollectionId, string DisplayName) Fetch(int idx) + { + var (identifier, id, name) = _overrides[idx]; + var collection = _penumbra.CollectionByIdentifier(id.ToString()); + if (collection == null) + return (false, identifier, id, name); + + if (collection.Value.Name == name) + return (true, identifier, id, name); + + _overrides[idx] = (identifier, id, collection.Value.Name); + Glamourer.Log.Debug($"Updated display name of collection override {idx + 1} ({id})."); + _saveService.QueueSave(this); + return (true, identifier, id, collection.Value.Name); + } + + public void ChangeOverride(int idx, Guid newCollectionId, string newDisplayName) + { + if (idx < 0 || idx >= _overrides.Count) + return; + + if (newCollectionId == Guid.Empty || newDisplayName.Length == 0) + return; + + var current = _overrides[idx]; + if (current.CollectionId == newCollectionId) + return; + + _overrides[idx] = current with + { + CollectionId = newCollectionId, + DisplayName = newDisplayName, + }; + Glamourer.Log.Debug($"Changed collection override {idx + 1} from {current.CollectionId} to {newCollectionId}."); + _saveService.QueueSave(this); + } + + public void DeleteOverride(int idx) + { + if (idx < 0 || idx >= _overrides.Count) + return; + + _overrides.RemoveAt(idx); + Glamourer.Log.Debug($"Removed collection override {idx + 1}."); + _saveService.QueueSave(this); + } + + public void MoveOverride(int idxFrom, int idxTo) + { + if (!_overrides.Move(idxFrom, idxTo)) + return; + + Glamourer.Log.Debug($"Moved collection override {idxFrom + 1} to {idxTo + 1}."); + _saveService.QueueSave(this); + } + + private void Load() + { + var file = _saveService.FileNames.CollectionOverrideFile; + if (!File.Exists(file)) + return; + + try + { + var text = File.ReadAllText(file); + var jObj = JObject.Parse(text); + var version = jObj["Version"]?.ToObject() ?? 0; + switch (version) + { + case 1: + case 2: + if (jObj["Overrides"] is not JArray array) + { + Glamourer.Log.Error($"Invalid format of collection override file, ignored."); + return; + } + + foreach (var token in array.OfType()) + { + var collectionIdentifier = token["Collection"]?.ToObject() ?? string.Empty; + var identifier = _actors.FromJson(token); + var displayName = token["DisplayName"]?.ToObject() ?? collectionIdentifier; + if (!identifier.IsValid) + { + Glamourer.Log.Warning( + $"Invalid identifier for collection override with collection [{token["Collection"]}], skipped."); + continue; + } + + if (!Guid.TryParse(collectionIdentifier, out var collectionId)) + { + if (collectionIdentifier.Length == 0) + { + Glamourer.Log.Warning($"Empty collection override for identifier {identifier.Incognito(null)}, skipped."); + continue; + } + + if (version >= 2) + { + Glamourer.Log.Warning( + $"Invalid collection override {collectionIdentifier} for identifier {identifier.Incognito(null)}, skipped."); + continue; + } + + var collection = _penumbra.CollectionByIdentifier(collectionIdentifier); + if (collection == null) + { + Glamourer.Messager.AddMessage(new Notification( + $"The overridden collection for identifier {identifier.Incognito(null)} with name {collectionIdentifier} could not be found by Penumbra for migration.", + NotificationType.Warning)); + continue; + } + + Glamourer.Log.Information($"Migrated collection {collectionIdentifier} to {collection.Value.Id}."); + collectionId = collection.Value.Id; + displayName = collection.Value.Name; + } + + _overrides.Add((identifier, collectionId, displayName)); + } + + break; + + default: + Glamourer.Log.Error($"Invalid version {version} of collection override file, ignored."); + return; + } + } + catch (Exception ex) + { + Glamourer.Log.Error($"Error loading collection override file:\n{ex}"); + } + } + + public void Save(StreamWriter writer) + { + var jObj = new JObject() + { + ["Version"] = Version, + ["Overrides"] = SerializeOverrides(), + }; + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + jObj.WriteTo(j); + return; + + JArray SerializeOverrides() + { + var jArray = new JArray(); + foreach (var (actor, collection, displayName) in _overrides) + { + var obj = actor.ToJson(); + obj["Collection"] = collection; + obj["DisplayName"] = displayName; + jArray.Add(obj); + } + + return jArray; + } + } +} diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index 924d091..d2feac0 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -1,44 +1,53 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Dalamud.Game.Command; +using Dalamud.Game.Command; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using Glamourer.Automation; -using Glamourer.Customization; using Glamourer.Designs; -using Glamourer.Events; +using Glamourer.Designs.Special; +using Glamourer.GameData; using Glamourer.Gui; -using Glamourer.Interop; +using Glamourer.Gui.Tabs.DesignTab; +using Glamourer.Interop.Penumbra; using Glamourer.State; -using Glamourer.Structs; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; +using OtterGui.Services; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; namespace Glamourer.Services; -public class CommandService : IDisposable +public class CommandService : IDisposable, IApiService { private const string MainCommandString = "/glamourer"; private const string ApplyCommandString = "/glamour"; - private readonly ICommandManager _commands; - private readonly MainWindow _mainWindow; - private readonly IChatGui _chat; - private readonly ActorService _actors; - private readonly ObjectManager _objects; - private readonly StateManager _stateManager; - private readonly AutoDesignApplier _autoDesignApplier; - private readonly AutoDesignManager _autoDesignManager; - private readonly DesignManager _designManager; - private readonly DesignConverter _converter; - private readonly DesignFileSystem _designFileSystem; + private readonly ICommandManager _commands; + private readonly MainWindow _mainWindow; + private readonly IChatGui _chat; + private readonly ActorManager _actors; + private readonly ActorObjectManager _objects; + private readonly StateManager _stateManager; + private readonly AutoDesignApplier _autoDesignApplier; + private readonly AutoDesignManager _autoDesignManager; + private readonly Configuration _config; + private readonly ModSettingApplier _modApplier; + private readonly ItemManager _items; + private readonly CustomizeService _customizeService; + private readonly DesignManager _designManager; + private readonly DesignConverter _converter; + private readonly DesignResolver _resolver; + private readonly PenumbraService _penumbra; - public CommandService(ICommandManager commands, MainWindow mainWindow, IChatGui chat, ActorService actors, ObjectManager objects, + public CommandService(ICommandManager commands, MainWindow mainWindow, IChatGui chat, ActorManager actors, ActorObjectManager objects, AutoDesignApplier autoDesignApplier, StateManager stateManager, DesignManager designManager, DesignConverter converter, - DesignFileSystem designFileSystem, AutoDesignManager autoDesignManager) + DesignFileSystem designFileSystem, AutoDesignManager autoDesignManager, Configuration config, ModSettingApplier modApplier, + ItemManager items, RandomDesignGenerator randomDesign, CustomizeService customizeService, DesignFileSystemSelector designSelector, + QuickDesignCombo quickDesignCombo, DesignResolver resolver, PenumbraService penumbra) { _commands = commands; _mainWindow = mainWindow; @@ -49,8 +58,13 @@ public class CommandService : IDisposable _stateManager = stateManager; _designManager = designManager; _converter = converter; - _designFileSystem = designFileSystem; _autoDesignManager = autoDesignManager; + _config = config; + _modApplier = modApplier; + _items = items; + _customizeService = customizeService; + _resolver = resolver; + _penumbra = penumbra; _commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." }); _commands.AddHandler(ApplyCommandString, @@ -64,7 +78,41 @@ public class CommandService : IDisposable } private void OnGlamourer(string command, string arguments) - => _mainWindow.Toggle(); + { + if (arguments.Length > 0) + switch (arguments) + { + case "qdb": + case "quick": + case "bar": + case "designs": + case "design": + case "design bar": + _config.Ephemeral.ShowDesignQuickBar = !_config.Ephemeral.ShowDesignQuickBar; + _config.Ephemeral.Save(); + return; + case "lock": + case "unlock": + _config.Ephemeral.LockMainWindow = !_config.Ephemeral.LockMainWindow; + _config.Ephemeral.Save(); + return; + case "automation": + var newValue = !_config.EnableAutoDesigns; + _config.EnableAutoDesigns = newValue; + _autoDesignApplier.OnEnableAutoDesignsChanged(newValue); + _config.Save(); + return; + default: + _chat.Print("Use without argument to toggle the main window."); + _chat.Print(new SeStringBuilder().AddText("Use ").AddPurple("/glamour").AddText(" instead of ").AddRed("/glamourer") + .AddText(" for application commands.").BuiltString); + _chat.Print(new SeStringBuilder().AddCommand("qdb", "Toggles the quick design bar on or off.").BuiltString); + _chat.Print(new SeStringBuilder().AddCommand("lock", "Toggles the lock of the main window on or off.").BuiltString); + return; + } + + _mainWindow.Toggle(); + } private void OnGlamour(string command, string arguments) { @@ -78,14 +126,20 @@ public class CommandService : IDisposable var argument = argumentList.Length == 2 ? argumentList[1] : string.Empty; var _ = argumentList[0].ToLowerInvariant() switch { - "apply" => Apply(argument), - "reapply" => ReapplyState(argument), - "revert" => Revert(argument), - "reapplyautomation" => ReapplyAutomation(argument), - "automation" => SetAutomation(argument), - "copy" => CopyState(argument), - "save" => SaveState(argument), - _ => PrintHelp(argumentList[0]), + "apply" => Apply(argument), + "reapply" => ReapplyState(argument), + "revert" => Revert(argument), + "reapplyautomation" => ReapplyAutomation(argument, "reapplyautomation", false, false), + "reverttoautomation" => ReapplyAutomation(argument, "reverttoautomation", true, false), + "resetdesign" => ReapplyAutomation(argument, "resetdesign", false, true), + "clearsettings" => ClearSettings(argument), + "automation" => SetAutomation(argument), + "copy" => CopyState(argument), + "save" => SaveState(argument), + "delete" => Delete(argument), + "applyitem" => ApplyItem(argument), + "applycustomization" => ApplyCustomization(argument), + _ => PrintHelp(argumentList[0]), }; } @@ -104,13 +158,82 @@ public class CommandService : IDisposable _chat.Print(new SeStringBuilder().AddCommand("revert", "Reverts a given character to its game state. Use without arguments for help.") .BuiltString); _chat.Print(new SeStringBuilder().AddCommand("reapplyautomation", + "Reapplies the current automation state on top of the characters current state.. Use without arguments for help.").BuiltString); + _chat.Print(new SeStringBuilder().AddCommand("reverttoautomation", "Reverts a given character to its supposed state using automated designs. Use without arguments for help.").BuiltString); + _chat.Print(new SeStringBuilder().AddCommand("resetdesign", + "Reapplies the current automation and resets the random design. Use without arguments for help.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("clearsettings", "Clears all temporary settings applied by Glamourer. Use without arguments for help.").BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("copy", "Copy the current state of a character to clipboard. Use without arguments for help.").BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("save", "Save the current state of a character to a named design. Use without arguments for help.").BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("automation", "Change the state of automated design sets. Use without arguments for help.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("applyitem", "Apply a specific item to a character. Use without arguments for help.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("applycustomization", "Apply a specific customization value to a character. Use without arguments for help.") + .BuiltString); + return true; + } + + private bool ClearSettings(string argument) + { + var argumentList = argument.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (argumentList.Length < 1) + { + _chat.Print(new SeStringBuilder().AddText("Use with /glamour clearsettings ").AddGreen("[Character Identifier]").AddText(" | ") + .AddPurple("").AddText(" | ").AddBlue("").BuiltString); + PlayerIdentifierHelp(false, true); + _chat.Print(new SeStringBuilder() + .AddText(" 》 The character identifier specifies the collection to clear settings from. It also accepts '").AddGreen("all") + .AddText("' to clear all collections.").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》 The booleans are optional and default to 'true', the ").AddPurple("first") + .AddText(" determines whether ").AddPurple("manually").AddText(" applied settings are cleared, the ").AddBlue("second") + .AddText(" determines whether ").AddBlue("automatically").AddText(" applied settings are cleared.").BuiltString); + return false; + } + + var clearManual = true; + var clearAutomatic = true; + if (argumentList.Length > 1 && bool.TryParse(argumentList[1], out var m)) + clearManual = m; + if (argumentList.Length > 2 && bool.TryParse(argumentList[2], out var a)) + clearAutomatic = a; + + if (!clearManual && !clearAutomatic) + return true; + + if (argumentList[0].ToLowerInvariant() is "all") + { + _penumbra.ClearAllTemporarySettings(clearAutomatic, clearManual); + return true; + } + + if (!IdentifierHandling(argumentList[0], out var identifiers, false, true)) + return false; + + var set = new HashSet(); + foreach (var id in identifiers) + { + if (!_objects.TryGetValue(id, out var data) || !data.Valid) + continue; + + foreach (var obj in data.Objects) + { + var guid = _penumbra.GetActorCollection(obj, out _); + if (!set.Add(guid)) + continue; + + if (clearManual) + _penumbra.RemoveAllTemporarySettings(guid, StateSource.Manual); + if (clearAutomatic) + _penumbra.RemoveAllTemporarySettings(guid, StateSource.Fixed); + } + } + return true; } @@ -136,7 +259,7 @@ public class CommandService : IDisposable .AddInitialPurple("Customizations, ") .AddInitialPurple("Equipment, ") .AddInitialPurple("Accessories, ") - .AddInitialPurple("Dyes and ") + .AddInitialPurple("Dyes & Crests and ") .AddInitialPurple("Weapons, where ").AddPurple("CEADW") .AddText(" means everything should be toggled on, and no value means nothing should be toggled on.") .BuiltString); @@ -223,27 +346,17 @@ public class CommandService : IDisposable } --designIdx; - AutoDesign.Type applicationFlags = 0; + ApplicationType applicationFlags = 0; if (split2.Length == 2) foreach (var character in split2[1]) { switch (char.ToLowerInvariant(character)) { - case 'c': - applicationFlags |= AutoDesign.Type.Customizations; - break; - case 'e': - applicationFlags |= AutoDesign.Type.Armor; - break; - case 'a': - applicationFlags |= AutoDesign.Type.Accessories; - break; - case 'd': - applicationFlags |= AutoDesign.Type.Stains; - break; - case 'w': - applicationFlags |= AutoDesign.Type.Weapons; - break; + case 'c': applicationFlags |= ApplicationType.Customizations; break; + case 'e': applicationFlags |= ApplicationType.Armor; break; + case 'a': applicationFlags |= ApplicationType.Accessories; break; + case 'd': applicationFlags |= ApplicationType.GearCustomization; break; + case 'w': applicationFlags |= ApplicationType.Weapons; break; default: _chat.Print(new SeStringBuilder().AddText("The value ").AddPurple(split2[1], true) .AddText(" is not a valid set of application flags.").BuiltString); @@ -255,11 +368,11 @@ public class CommandService : IDisposable return true; } - private bool ReapplyAutomation(string argument) + private bool ReapplyAutomation(string argument, string command, bool revert, bool forcedNew) { if (argument.Length == 0) { - _chat.Print(new SeStringBuilder().AddText("Use with /glamour reapplyautomation ").AddGreen("[Character Identifier]").BuiltString); + _chat.Print(new SeStringBuilder().AddText($"Use with /glamour {command} ").AddGreen("[Character Identifier]").BuiltString); PlayerIdentifierHelp(false, true); return true; } @@ -267,7 +380,6 @@ public class CommandService : IDisposable if (!IdentifierHandling(argument, out var identifiers, false, true)) return false; - _objects.Update(); foreach (var identifier in identifiers) { if (!_objects.TryGetValue(identifier, out var data)) @@ -277,8 +389,8 @@ public class CommandService : IDisposable { if (_stateManager.GetOrCreate(identifier, actor, out var state)) { - _autoDesignApplier.ReapplyAutomation(actor, identifier, state); - _stateManager.ReapplyState(actor); + _autoDesignApplier.ReapplyAutomation(actor, identifier, state, revert, forcedNew, out var forcedRedraw); + _stateManager.ReapplyAutomationState(actor, forcedRedraw, revert, StateSource.Manual); } } } @@ -301,7 +413,7 @@ public class CommandService : IDisposable foreach (var identifier in identifiers) { if (_stateManager.TryGetValue(identifier, out var state)) - _stateManager.ResetState(state, StateChanged.Source.Manual); + _stateManager.ResetState(state, StateSource.Manual, isFinal: true); } @@ -320,28 +432,248 @@ public class CommandService : IDisposable if (!IdentifierHandling(argument, out var identifiers, false, true)) return false; - _objects.Update(); foreach (var identifier in identifiers) { if (!_objects.TryGetValue(identifier, out var data)) return true; foreach (var actor in data.Objects) - _stateManager.ReapplyState(actor); + _stateManager.ReapplyState(actor, false, StateSource.Manual, true); } return true; } + private bool ApplyItem(string arguments) + { + var split = arguments.Split('|', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length is not 2) + { + _chat.Print(new SeStringBuilder().AddText("Use with /glamour applyitem ").AddYellow("[Item ID or Item Name]") + .AddText(" | ") + .AddGreen("[Character Identifier]") + .BuiltString); + _chat.Print(new SeStringBuilder() + .AddText( + " 》 The item name is case-insensitive. Numeric IDs are preferred before item names.") + .BuiltString); + PlayerIdentifierHelp(false, true); + return true; + } + + var items = new EquipItem[3]; + if (uint.TryParse(split[0], out var id)) + { + if (_items.ItemData.Primary.TryGetValue(id, out var main)) + items[0] = main; + } + else if (_items.ItemData.Primary.FindFirst(pair => string.Equals(pair.Value.Name, split[0], StringComparison.OrdinalIgnoreCase), + out var i)) + { + items[0] = i.Value; + } + + if (!items[0].Valid) + { + _chat.Print(new SeStringBuilder().AddText("The item ").AddYellow(split[0], true) + .AddText(" could not be identified as a valid item.").BuiltString); + return false; + } + + if (_items.ItemData.Secondary.TryGetValue(items[0].ItemId, out var off)) + { + items[1] = off; + if (_items.ItemData.Tertiary.TryGetValue(items[0].ItemId, out var gauntlet)) + items[2] = gauntlet; + } + + if (!IdentifierHandling(split[1], out var identifiers, false, true)) + return false; + + foreach (var identifier in identifiers) + { + if (!_objects.TryGetValue(identifier, out var actors)) + { + if (!_stateManager.TryGetValue(identifier, out var state)) + continue; + + foreach (var item in items.Where(i => i.Valid)) + _stateManager.ChangeItem(state, item.Type.ToSlot(), item, ApplySettings.Manual); + } + else + { + foreach (var actor in actors.Objects) + { + if (!_stateManager.GetOrCreate(actor.GetIdentifier(_actors), actor, out var state)) + continue; + + foreach (var item in items.Where(i => i.Valid)) + _stateManager.ChangeItem(state, item.Type.ToSlot(), item, ApplySettings.Manual); + } + } + } + + return true; + } + + private bool ApplyCustomization(string arguments) + { + var split = arguments.Split('|', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length is not 2) + return PrintCustomizationHelp(); + + var customizationSplit = split[0].Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (customizationSplit.Length < 2) + return PrintCustomizationHelp(); + + if (!Enum.TryParse(customizationSplit[0], true, out CustomizeIndex customizeIndex) + || !CustomizationExtensions.AllBasic.Contains(customizeIndex)) + { + if (!int.TryParse(customizationSplit[0], out var customizeInt) + || customizeInt < 0 + || customizeInt >= CustomizationExtensions.AllBasic.Length) + { + _chat.Print(new SeStringBuilder().AddText("The customization type ").AddYellow(customizationSplit[0], true) + .AddText(" could not be identified as a valid type.").BuiltString); + return false; + } + + customizeIndex = CustomizationExtensions.AllBasic[customizeInt]; + } + + var valueString = customizationSplit[1].ToLowerInvariant(); + var (wrapAround, offset) = valueString switch + { + "next" => (true, (sbyte)1), + "previous" => (true, (sbyte)-1), + "plus" => (false, (sbyte)1), + "minus" => (false, (sbyte)-1), + _ => (false, (sbyte)0), + }; + byte? baseValue = null; + if (offset == 0) + { + if (byte.TryParse(valueString, out var b)) + { + baseValue = b; + } + else + { + _chat.Print(new SeStringBuilder().AddText("The customization value ").AddPurple(valueString, true) + .AddText(" could not be parsed.") + .BuiltString); + return false; + } + } + + if (customizationSplit.Length < 3 || !byte.TryParse(customizationSplit[2], out var multiplier)) + multiplier = 1; + + if (!IdentifierHandling(split[1], out var identifiers, false, true)) + return false; + + foreach (var identifier in identifiers) + { + if (!_objects.TryGetValue(identifier, out var actors)) + { + if (_stateManager.TryGetValue(identifier, out var state)) + ApplyToState(state); + } + else + { + foreach (var actor in actors.Objects) + { + if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors), actor, out var state)) + ApplyToState(state); + } + } + } + + return true; + + void ApplyToState(ActorState state) + { + var customize = state.ModelData.Customize; + if (!state.ModelData.IsHuman) + return; + + var set = _customizeService.Manager.GetSet(customize.Clan, customize.Gender); + if (!set.IsAvailable(customizeIndex)) + return; + + if (baseValue != null) + { + var v = baseValue.Value; + if (set.Type(customizeIndex) is MenuType.ListSelector) + --v; + set.DataByValue(customizeIndex, new CustomizeValue(v), out var data, customize.Face); + if (data != null) + _stateManager.ChangeCustomize(state, customizeIndex, data.Value.Value, ApplySettings.Manual); + } + else + { + var idx = set.DataByValue(customizeIndex, customize[customizeIndex], out var data, customize.Face); + var count = set.Count(customizeIndex, customize.Face); + var m = multiplier % count; + var newIdx = offset is 1 + ? idx >= count - m + ? wrapAround + ? m + idx - count + : count - 1 + : idx + m + : idx < m + ? wrapAround + ? count - m + idx + : 0 + : idx - m; + data = set.Data(customizeIndex, newIdx, customize.Face); + _stateManager.ChangeCustomize(state, customizeIndex, data.Value.Value, ApplySettings.Manual); + } + } + + bool PrintCustomizationHelp() + { + _chat.Print(new SeStringBuilder().AddText("Use with /glamour applycustomization ").AddYellow("[Customization Type]") + .AddPurple(" [Value, Next, Previous, Minus, or Plus] ") + .AddBlue("") + .AddText(" | ") + .AddGreen("[Character Identifier]") + .BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》 Valid ").AddPurple("values") + .AddText(" depend on the the character's gender, clan, and the customization type.").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》 ").AddPurple("Plus").AddText(" and ").AddPurple("Minus") + .AddText(" are the same as pressing the + and - buttons in the UI, times the optional ").AddBlue(" amount").AddText(".") + .BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》 ").AddPurple("Next").AddText(" and ").AddPurple("Previous") + .AddText(" is similar to Plus and Minus, but with wrap-around on reaching the end.").BuiltString); + var builder = new SeStringBuilder().AddText(" 》 Available ").AddYellow("Customization Types") + .AddText(" are either a number in ") + .AddYellow($"[0, {CustomizationExtensions.AllBasic.Length}]") + .AddText(" or one of "); + foreach (var index in CustomizationExtensions.AllBasic.SkipLast(1)) + builder.AddYellow(index.ToString()).AddText(", "); + _chat.Print(builder.AddYellow(CustomizationExtensions.AllBasic[^1].ToString()).AddText(".").BuiltString); + _chat.Print(new SeStringBuilder() + .AddText(" 》 The item name is case-insensitive. Numeric IDs are preferred before item names.") + .BuiltString); + PlayerIdentifierHelp(false, true); + return true; + } + } + private bool Apply(string arguments) { var split = arguments.Split('|', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - if (split.Length != 2) + if (split.Length is not 2) { - _chat.Print(new SeStringBuilder().AddText("Use with /glamour apply ").AddYellow("[Design Name, Path or Identifier, or Clipboard]") + _chat.Print(new SeStringBuilder().AddText("Use with /glamour apply ") + .AddYellow("[Design Name, Path or Identifier, Quick, Selection, Random, or Clipboard]") .AddText(" | ") - .AddGreen("[Character Identifier]").BuiltString); + .AddGreen("[Character Identifier]") + .AddText("; ") + .AddBlue("") + .BuiltString); _chat.Print(new SeStringBuilder() .AddText( " 》 The design name is case-insensitive. If multiple designs of that name up to case exist, the first one is chosen.") @@ -353,28 +685,57 @@ public class CommandService : IDisposable _chat.Print(new SeStringBuilder() .AddText(" 》 The design path is the folder path in the selector, with '/' as separators. It is also case-insensitive.") .BuiltString); + _chat.Print(new SeStringBuilder() + .AddText(" 》 Quick will use the design currently selected in the Quick Design Bar, if any.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddText(" 》 Selection will use the design currently selected in the main interfaces Designs tab, if any.").BuiltString); _chat.Print(new SeStringBuilder() .AddText(" 》 Clipboard as a single word will try to apply a design string currently in your clipboard.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddText(" 》 ").AddYellow("Random") + .AddText( + " supports many restrictions, see the Restriction Builder when adding a Random design to Automations for valid strings.") + .BuiltString); + _chat.Print(new SeStringBuilder() + .AddText(" 》 ").AddBlue("").AddText(" is optional and can be omitted (together with the ;), ").AddBlue("true") + .AddText(" or ").AddBlue("false").AddText(".").BuiltString); + _chat.Print(new SeStringBuilder().AddText("If ").AddBlue("true") + .AddText(", it will try to apply mod associations to the collection assigned to the identified character.").BuiltString); PlayerIdentifierHelp(false, true); + return true; } - if (!GetDesign(split[0], out var design, true) || !IdentifierHandling(split[1], out var identifiers, false, true)) + var split2 = split[1].Split(';', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + var applyMods = split2.Length == 2 + && split2[1].ToLowerInvariant() switch + { + "true" => true, + "1" => true, + "t" => true, + "yes" => true, + "y" => true, + _ => false, + }; + if (!_resolver.GetDesign(split[0], out var design, true) || !IdentifierHandling(split2[0], out var identifiers, false, true)) return false; - _objects.Update(); foreach (var identifier in identifiers) { if (!_objects.TryGetValue(identifier, out var actors)) { if (_stateManager.TryGetValue(identifier, out var state)) - _stateManager.ApplyDesign(design, state, StateChanged.Source.Manual); + _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { IsFinal = true }); } else { foreach (var actor in actors.Objects) { - if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors.AwaitedService), actor, out var state)) - _stateManager.ApplyDesign(design, state, StateChanged.Source.Manual); + if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors), actor, out var state)) + { + ApplyModSettings(design, actor, applyMods); + _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { IsFinal = true }); + } } } } @@ -382,6 +743,49 @@ public class CommandService : IDisposable return true; } + private void ApplyModSettings(DesignBase design, Actor actor, bool applyMods) + { + if (!applyMods || design is not Design d) + return; + + var (messages, appliedMods, _, name, overridden) = + _modApplier.ApplyModSettings(d.AssociatedMods, actor, StateSource.Manual, d.ResetTemporarySettings); + + foreach (var message in messages) + Glamourer.Messager.Chat.Print($"Error applying mod settings: {message}"); + + if (appliedMods > 0) + Glamourer.Messager.Chat.Print( + $"Applied {appliedMods} mod settings to {name}{(overridden ? " (overridden by settings)" : string.Empty)}."); + } + + private bool Delete(string argument) + { + if (argument.Length == 0) + { + _chat.Print(new SeStringBuilder().AddText("Use with /glamour delete ").AddYellow("[Design Name, Path or Identifier]").BuiltString); + _chat.Print(new SeStringBuilder() + .AddText( + " 》 The design name is case-insensitive. If multiple designs of that name up to case exist, the first one is chosen.") + .BuiltString); + _chat.Print(new SeStringBuilder() + .AddText( + " 》 If using the design identifier, you need to specify at least 4 characters for it, and the first one starting with the provided characters is chosen.") + .BuiltString); + _chat.Print(new SeStringBuilder() + .AddText(" 》 The design path is the folder path in the selector, with '/' as separators. It is also case-insensitive.") + .BuiltString); + return false; + } + + if (!_resolver.GetDesign(argument, out var designBase, false) || designBase is not Design d) + return false; + + _designManager.Delete(d); + + return true; + } + private bool CopyState(string argument) { if (argument.Length == 0) @@ -393,7 +797,6 @@ public class CommandService : IDisposable if (!IdentifierHandling(argument, out var identifiers, false, true)) return false; - _objects.Update(); foreach (var identifier in identifiers) { if (!_stateManager.TryGetValue(identifier, out var state) @@ -404,7 +807,7 @@ public class CommandService : IDisposable try { - var text = _converter.ShareBase64(state); + var text = _converter.ShareBase64(state, ApplicationRules.AllButParameters(state)); ImGui.SetClipboardText(text); return true; } @@ -434,7 +837,6 @@ public class CommandService : IDisposable if (!IdentifierHandling(split[1], out var identifiers, false, true)) return false; - _objects.Update(); foreach (var identifier in identifiers) { if (!_stateManager.TryGetValue(identifier, out var state) @@ -443,7 +845,7 @@ public class CommandService : IDisposable && _stateManager.GetOrCreate(identifier, data.Objects[0], out state))) continue; - var design = _converter.Convert(state, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant); + var design = _converter.Convert(state, ApplicationRules.FromModifiers(state)); _designManager.CreateClone(design, split[0], true); return true; } @@ -453,80 +855,31 @@ public class CommandService : IDisposable return false; } - private bool GetDesign(string argument, [NotNullWhen(true)] out DesignBase? design, bool allowClipboard) - { - design = null; - if (argument.Length == 0) - return false; - - if (allowClipboard && string.Equals("clipboard", argument, StringComparison.OrdinalIgnoreCase)) - { - try - { - var clipboardText = ImGui.GetClipboardText(); - if (clipboardText.Length > 0) - design = _converter.FromBase64(clipboardText, true, true, out _); - } - catch - { - // ignored - } - - if (design != null) - return true; - - _chat.Print(new SeStringBuilder().AddText("Your current clipboard did not contain a valid design string.").BuiltString); - return false; - } - - if (Guid.TryParse(argument, out var guid)) - { - design = _designManager.Designs.FirstOrDefault(d => d.Identifier == guid); - } - else - { - var lower = argument.ToLowerInvariant(); - design = _designManager.Designs.FirstOrDefault(d - => d.Name.Lower == lower || lower.Length > 3 && d.Identifier.ToString().StartsWith(lower)); - if (design == null && _designFileSystem.Find(lower, out var child) && child is DesignFileSystem.Leaf leaf) - design = leaf.Value; - } - - if (design == null) - { - _chat.Print(new SeStringBuilder().AddText("The token ").AddYellow(argument, true).AddText(" did not resolve to an existing design.") - .BuiltString); - return false; - } - - return true; - } - private unsafe bool IdentifierHandling(string argument, out ActorIdentifier[] identifiers, bool allowAnyWorld, bool allowIndex) { try { if (_objects.GetName(argument.ToLowerInvariant(), out var obj)) { - var identifier = _actors.AwaitedService.FromObject(obj.AsObject, out _, true, true, true); + var identifier = _actors.FromObject(obj.AsObject, out _, true, true, true); if (!identifier.IsValid) { _chat.Print(new SeStringBuilder().AddText("The placeholder ").AddGreen(argument) .AddText(" did not resolve to a game object with a valid identifier.").BuiltString); - identifiers = Array.Empty(); + identifiers = []; return false; } if (allowIndex && identifier.Type is IdentifierType.Npc) - identifier = _actors.AwaitedService.CreateNpc(identifier.Kind, identifier.DataId, obj.Index); - identifiers = new[] - { + identifier = _actors.CreateNpc(identifier.Kind, identifier.DataId, obj.Index); + identifiers = + [ identifier, - }; + ]; } else { - identifiers = _actors.AwaitedService.FromUserString(argument, allowIndex); + identifiers = _actors.FromUserString(argument, allowIndex); if (!allowAnyWorld && identifiers[0].Type is IdentifierType.Player or IdentifierType.Owned && identifiers[0].HomeWorld == ushort.MaxValue) @@ -539,12 +892,12 @@ public class CommandService : IDisposable return true; } - catch (ActorManager.IdentifierParseError e) + catch (ActorIdentifierFactory.IdentifierParseError e) { _chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(argument, true) .AddText($" could not be converted to an identifier. {e.Message}") .BuiltString); - identifiers = Array.Empty(); + identifiers = []; return false; } } diff --git a/Glamourer/Services/ConfigMigrationService.cs b/Glamourer/Services/ConfigMigrationService.cs index bc75a17..ef39f1a 100644 --- a/Glamourer/Services/ConfigMigrationService.cs +++ b/Glamourer/Services/ConfigMigrationService.cs @@ -1,50 +1,88 @@ -using System; -using System.IO; -using System.Linq; -using Glamourer.Automation; +using Glamourer.Automation; using Glamourer.Gui; using Newtonsoft.Json.Linq; namespace Glamourer.Services; -public class ConfigMigrationService +public class ConfigMigrationService(SaveService saveService, FixedDesignMigrator fixedDesignMigrator, BackupService backupService) { - private readonly SaveService _saveService; - private readonly FixedDesignMigrator _fixedDesignMigrator; - private readonly BackupService _backupService; - private Configuration _config = null!; private JObject _data = null!; - public ConfigMigrationService(SaveService saveService, FixedDesignMigrator fixedDesignMigrator, BackupService backupService) - { - _saveService = saveService; - _fixedDesignMigrator = fixedDesignMigrator; - _backupService = backupService; - } - public void Migrate(Configuration config) { - _config = config; - if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(_saveService.FileNames.ConfigFile)) + _config = config; + if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(saveService.FileNames.ConfigFile)) { AddColors(config, false); return; } - _data = JObject.Parse(File.ReadAllText(_saveService.FileNames.ConfigFile)); + _data = JObject.Parse(File.ReadAllText(saveService.FileNames.ConfigFile)); MigrateV1To2(); MigrateV2To4(); + MigrateV4To5(); + MigrateV5To6(); + MigrateV6To7(); + MigrateV7To8(); AddColors(config, true); } + private void MigrateV7To8() + { + if (_config.Version > 7) + return; + + if (_config.QdbButtons.HasFlag(QdbButtons.RevertAdvancedDyes)) + _config.QdbButtons |= QdbButtons.RevertAdvancedCustomization; + _config.Version = 8; + } + + private void MigrateV6To7() + { + if (_config.Version > 6) + return; + + // Do not actually change anything in the config, just create a backup before designs are migrated. + backupService.CreateMigrationBackup("pre_gloss_specular_migration"); + _config.Version = 7; + } + + private void MigrateV5To6() + { + if (_config.Version > 5) + return; + + if (_data["ShowRevertAdvancedParametersButton"]?.ToObject() ?? true) + _config.QdbButtons |= QdbButtons.RevertAdvancedCustomization; + _config.Version = 6; + } + + // Ephemeral Config. + private void MigrateV4To5() + { + if (_config.Version > 4) + return; + + _config.Ephemeral.IncognitoMode = _data["IncognitoMode"]?.ToObject() ?? _config.Ephemeral.IncognitoMode; + _config.Ephemeral.UnlockDetailMode = _data["UnlockDetailMode"]?.ToObject() ?? _config.Ephemeral.UnlockDetailMode; + _config.Ephemeral.ShowDesignQuickBar = _data["ShowDesignQuickBar"]?.ToObject() ?? _config.Ephemeral.ShowDesignQuickBar; + _config.Ephemeral.LockDesignQuickBar = _data["LockDesignQuickBar"]?.ToObject() ?? _config.Ephemeral.LockDesignQuickBar; + _config.Ephemeral.LockMainWindow = _data["LockMainWindow"]?.ToObject() ?? _config.Ephemeral.LockMainWindow; + _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject() ?? _config.Ephemeral.SelectedTab; + _config.Ephemeral.LastSeenVersion = _data["LastSeenVersion"]?.ToObject() ?? _config.Ephemeral.LastSeenVersion; + _config.Version = 5; + _config.Ephemeral.Version = 5; + _config.Ephemeral.Save(); + } + private void MigrateV1To2() { if (_config.Version > 1) return; - _backupService.CreateMigrationBackup("pre_v1_to_v2_migration"); - _fixedDesignMigrator.Migrate(_data["FixedDesigns"]); + backupService.CreateMigrationBackup("pre_v1_to_v2_migration"); + fixedDesignMigrator.Migrate(_data["FixedDesigns"]); _config.Version = 2; var customizationColor = _data["CustomizationColor"]?.ToObject() ?? ColorId.CustomizationDesign.Data().DefaultColor; _config.Colors[ColorId.CustomizationDesign] = customizationColor; @@ -58,7 +96,7 @@ public class ConfigMigrationService { if (_config.Version > 4) return; - + _config.Version = 4; _config.Codes = _config.Codes.DistinctBy(c => c.Code).ToList(); } diff --git a/Glamourer/Services/CustomizationService.cs b/Glamourer/Services/CustomizeService.cs similarity index 53% rename from Glamourer/Services/CustomizationService.cs rename to Glamourer/Services/CustomizeService.cs index 1b3f6e2..74f0b5b 100644 --- a/Glamourer/Services/CustomizationService.cs +++ b/Glamourer/Services/CustomizeService.cs @@ -1,27 +1,33 @@ -using System.Linq; -using System.Runtime.CompilerServices; -using Dalamud.Plugin.Services; -using Glamourer.Customization; -using Penumbra.GameData.Data; +using Glamourer.GameData; +using OtterGui.Services; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Glamourer.Services; -public sealed class CustomizationService : AsyncServiceWrapper +public sealed class CustomizeService( + HumanModelList humanModels, + NpcCustomizeSet npcCustomizeSet, + CustomizeManager manager) + : IAsyncService { - public readonly HumanModelList HumanModels; + public readonly HumanModelList HumanModels = humanModels; + public readonly CustomizeManager Manager = manager; + public readonly NpcCustomizeSet NpcCustomizeSet = npcCustomizeSet; - public CustomizationService(ITextureProvider textures, IDataManager gameData, HumanModelList humanModels, IPluginLog log) - : base(nameof(CustomizationService), () => CustomizationManager.Create(textures, gameData, log)) - => HumanModels = humanModels; + public Task Awaiter { get; } + = Task.WhenAll(humanModels.Awaiter, manager.Awaiter, npcCustomizeSet.Awaiter); - public (Customize NewValue, CustomizeFlag Applied, CustomizeFlag Changed) Combine(Customize oldValues, Customize newValues, + public bool Finished + => Awaiter.IsCompletedSuccessfully; + + public (CustomizeArray NewValue, CustomizeFlag Applied, CustomizeFlag Changed) Combine(CustomizeArray oldValues, CustomizeArray newValues, CustomizeFlag applyWhich, bool allowUnknown) { CustomizeFlag applied = 0; CustomizeFlag changed = 0; - Customize ret = default; - ret.Load(oldValues); + var ret = oldValues; if (applyWhich.HasFlag(CustomizeFlag.Clan)) { changed |= ChangeClan(ref ret, newValues.Clan); @@ -29,14 +35,23 @@ public sealed class CustomizationService : AsyncServiceWrapper AwaitedService.GetName(CustomName.MidlanderM), - (Gender.Male, SubRace.Highlander) => AwaitedService.GetName(CustomName.HighlanderM), - (Gender.Male, SubRace.Wildwood) => AwaitedService.GetName(CustomName.WildwoodM), - (Gender.Male, SubRace.Duskwight) => AwaitedService.GetName(CustomName.DuskwightM), - (Gender.Male, SubRace.Plainsfolk) => AwaitedService.GetName(CustomName.PlainsfolkM), - (Gender.Male, SubRace.Dunesfolk) => AwaitedService.GetName(CustomName.DunesfolkM), - (Gender.Male, SubRace.SeekerOfTheSun) => AwaitedService.GetName(CustomName.SeekerOfTheSunM), - (Gender.Male, SubRace.KeeperOfTheMoon) => AwaitedService.GetName(CustomName.KeeperOfTheMoonM), - (Gender.Male, SubRace.Seawolf) => AwaitedService.GetName(CustomName.SeawolfM), - (Gender.Male, SubRace.Hellsguard) => AwaitedService.GetName(CustomName.HellsguardM), - (Gender.Male, SubRace.Raen) => AwaitedService.GetName(CustomName.RaenM), - (Gender.Male, SubRace.Xaela) => AwaitedService.GetName(CustomName.XaelaM), - (Gender.Male, SubRace.Helion) => AwaitedService.GetName(CustomName.HelionM), - (Gender.Male, SubRace.Lost) => AwaitedService.GetName(CustomName.LostM), - (Gender.Male, SubRace.Rava) => AwaitedService.GetName(CustomName.RavaM), - (Gender.Male, SubRace.Veena) => AwaitedService.GetName(CustomName.VeenaM), - (Gender.Female, SubRace.Midlander) => AwaitedService.GetName(CustomName.MidlanderF), - (Gender.Female, SubRace.Highlander) => AwaitedService.GetName(CustomName.HighlanderF), - (Gender.Female, SubRace.Wildwood) => AwaitedService.GetName(CustomName.WildwoodF), - (Gender.Female, SubRace.Duskwight) => AwaitedService.GetName(CustomName.DuskwightF), - (Gender.Female, SubRace.Plainsfolk) => AwaitedService.GetName(CustomName.PlainsfolkF), - (Gender.Female, SubRace.Dunesfolk) => AwaitedService.GetName(CustomName.DunesfolkF), - (Gender.Female, SubRace.SeekerOfTheSun) => AwaitedService.GetName(CustomName.SeekerOfTheSunF), - (Gender.Female, SubRace.KeeperOfTheMoon) => AwaitedService.GetName(CustomName.KeeperOfTheMoonF), - (Gender.Female, SubRace.Seawolf) => AwaitedService.GetName(CustomName.SeawolfF), - (Gender.Female, SubRace.Hellsguard) => AwaitedService.GetName(CustomName.HellsguardF), - (Gender.Female, SubRace.Raen) => AwaitedService.GetName(CustomName.RaenF), - (Gender.Female, SubRace.Xaela) => AwaitedService.GetName(CustomName.XaelaF), - (Gender.Female, SubRace.Helion) => AwaitedService.GetName(CustomName.HelionM), - (Gender.Female, SubRace.Lost) => AwaitedService.GetName(CustomName.LostM), - (Gender.Female, SubRace.Rava) => AwaitedService.GetName(CustomName.RavaF), - (Gender.Female, SubRace.Veena) => AwaitedService.GetName(CustomName.VeenaF), - _ => "Unknown", - }; + return Manager.GetSet(race, gender).Name; } /// Returns whether a clan is valid. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public bool IsClanValid(SubRace clan) - => AwaitedService.Clans.Contains(clan); + => CustomizeManager.Clans.Contains(clan); /// Returns whether a gender is valid for the given race. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public bool IsGenderValid(Race race, Gender gender) - => race is Race.Hrothgar ? gender == Gender.Male : AwaitedService.Genders.Contains(gender); + => CustomizeManager.Genders.Contains(gender); - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static bool IsCustomizationValid(CustomizationSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value) + public static bool IsCustomizationValid(CustomizeSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value) => IsCustomizationValid(set, face, type, value, out _); /// Returns whether a customization value is valid for a given clan/gender set and face. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static bool IsCustomizationValid(CustomizationSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value, + public static bool IsCustomizationValid(CustomizeSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value, out CustomizeData? data) => set.Validate(type, value, out data, face); /// Returns whether a customization value is valid for a given clan, gender and face. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public bool IsCustomizationValid(SubRace race, Gender gender, CustomizeValue face, CustomizeIndex type, CustomizeValue value) - => IsCustomizationValid(AwaitedService.GetList(race, gender), face, type, value); + => IsCustomizationValid(Manager.GetSet(race, gender), face, type, value); /// /// Check that the given race and clan are valid. @@ -145,10 +125,10 @@ public sealed class CustomizationService : AsyncServiceWrapper c.ToRace() == race, SubRace.Unknown); + actualClan = CustomizeManager.Clans.FirstOrDefault(c => c.ToRace() == race, SubRace.Unknown); // This should not happen. if (actualClan == SubRace.Unknown) { @@ -174,19 +154,12 @@ public sealed class CustomizationService : AsyncServiceWrapper public string ValidateGender(Race race, Gender gender, out Gender actualGender) { - if (!AwaitedService.Genders.Contains(gender)) + if (!CustomizeManager.Genders.Contains(gender)) { actualGender = Gender.Male; return $"The gender {gender.ToName()} is unknown, reset to {Gender.Male.ToName()}."; } - // TODO: Female Hrothgar - if (gender is Gender.Female && race is Race.Hrothgar) - { - actualGender = Gender.Male; - return $"{Race.Hrothgar.ToName()} do not currently support {Gender.Female.ToName()} characters, reset to {Gender.Male.ToName()}."; - } - actualGender = gender; return string.Empty; } @@ -215,7 +188,7 @@ public sealed class CustomizationService : AsyncServiceWrapper - public static string ValidateCustomizeValue(CustomizationSet set, CustomizeValue face, CustomizeIndex index, CustomizeValue value, + public static string ValidateCustomizeValue(CustomizeSet set, CustomizeValue face, CustomizeIndex index, CustomizeValue value, out CustomizeValue actualValue, bool allowUnknown) { if (allowUnknown || IsCustomizationValid(set, face, index, value)) @@ -232,7 +205,7 @@ public sealed class CustomizationService : AsyncServiceWrapper Change a clan while keeping all other customizations valid. - public CustomizeFlag ChangeClan(ref Customize customize, SubRace newClan) + public CustomizeFlag ChangeClan(ref CustomizeArray customize, SubRace newClan) { if (customize.Clan == newClan) return 0; @@ -244,39 +217,50 @@ public sealed class CustomizationService : AsyncServiceWrapper Change a gender while keeping all other customizations valid. - public CustomizeFlag ChangeGender(ref Customize customize, Gender newGender) + public CustomizeFlag ChangeGender(ref CustomizeArray customize, Gender newGender) { if (customize.Gender == newGender) return 0; - // TODO Female Hrothgar - if (customize.Race is Race.Hrothgar) - return 0; - if (ValidateGender(customize.Race, newGender, out newGender).Length > 0) return 0; customize.Gender = newGender; - var set = AwaitedService.GetList(customize.Clan, customize.Gender); + var set = Manager.GetSet(customize.Clan, customize.Gender); return FixValues(set, ref customize) | CustomizeFlag.Gender; } - private static CustomizeFlag FixValues(CustomizationSet set, ref Customize customize) + private static CustomizeFlag FixValues(CustomizeSet set, ref CustomizeArray customize) { CustomizeFlag flags = 0; - foreach (var idx in CustomizationExtensions.AllBasic) + + // Hrothgar face hack. + if (customize.Race is Race.Hrothgar) + { + if (customize.Face.Value is < 5) + { + customize.Face += 4; + flags |= CustomizeFlag.Face; + } + } + else if (customize.Face.Value is > 4 and < 9) + { + customize.Face -= 4; + flags |= CustomizeFlag.Face; + } + + if (ValidateCustomizeValue(set, customize.Face, CustomizeIndex.Face, customize.Face, out var fixedFace, false).Length > 0) + { + customize.Face = fixedFace; + flags |= CustomizeFlag.Face; + } + + foreach (var idx in CustomizationExtensions.AllBasicWithoutFace) { if (set.IsAvailable(idx)) { diff --git a/Glamourer/Services/DalamudServices.cs b/Glamourer/Services/DalamudServices.cs index 30cf9c8..e8a9f55 100644 --- a/Glamourer/Services/DalamudServices.cs +++ b/Glamourer/Services/DalamudServices.cs @@ -1,55 +1,35 @@ -using Dalamud.Game.ClientState.Objects; using Dalamud.Interface.DragDrop; -using Dalamud.IoC; using Dalamud.Plugin; using Dalamud.Plugin.Services; -using Microsoft.Extensions.DependencyInjection; +using OtterGui.Services; namespace Glamourer.Services; +#pragma warning disable SeStringEvaluator + public class DalamudServices { - public DalamudServices(DalamudPluginInterface pi) + public static void AddServices(ServiceManager services, IDalamudPluginInterface pi) { - pi.Inject(this); + services.AddExistingService(pi); + services.AddExistingService(pi.UiBuilder); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); + services.AddDalamudService(pi); } - - public void AddServices(IServiceCollection services) - { - services.AddSingleton(PluginInterface); - services.AddSingleton(Commands); - services.AddSingleton(GameData); - services.AddSingleton(ClientState); - services.AddSingleton(Condition); - services.AddSingleton(GameGui); - services.AddSingleton(Chat); - services.AddSingleton(Framework); - services.AddSingleton(Targets); - services.AddSingleton(Objects); - services.AddSingleton(KeyState); - services.AddSingleton(this); - services.AddSingleton(PluginInterface.UiBuilder); - services.AddSingleton(DragDropManager); - services.AddSingleton(TextureProvider); - services.AddSingleton(Log); - services.AddSingleton(Interop); - } - - // @formatter:off - [PluginService][RequiredVersion("1.0")] public DalamudPluginInterface PluginInterface { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ICommandManager Commands { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IDataManager GameData { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IClientState ClientState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ICondition Condition { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IGameGui GameGui { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IChatGui Chat { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IFramework Framework { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ITargetManager Targets { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IObjectTable Objects { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IKeyState KeyState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IDragDropManager DragDropManager { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public ITextureProvider TextureProvider { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IPluginLog Log { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public IGameInteropProvider Interop { get; private set; } = null!; - // @formatter:on } diff --git a/Glamourer/Services/DesignApplier.cs b/Glamourer/Services/DesignApplier.cs new file mode 100644 index 0000000..f0a9ba4 --- /dev/null +++ b/Glamourer/Services/DesignApplier.cs @@ -0,0 +1,54 @@ +using Glamourer.Designs; +using Glamourer.State; +using OtterGui.Services; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; + +namespace Glamourer.Services; + +public sealed class DesignApplier(StateManager stateManager, ActorObjectManager objects) : IService +{ + public void ApplyToPlayer(DesignBase design) + { + var (player, data) = objects.PlayerData; + if (!data.Valid) + return; + + if (!stateManager.GetOrCreate(player, data.Objects[0], out var state)) + return; + + stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks); + } + + public void ApplyToTarget(DesignBase design) + { + var (player, data) = objects.TargetData; + if (!data.Valid) + return; + + if (!stateManager.GetOrCreate(player, data.Objects[0], out var state)) + return; + + stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks); + } + + public void Apply(ActorIdentifier actor, DesignBase design) + => Apply(actor, objects.TryGetValue(actor, out var d) ? d : ActorData.Invalid, design, ApplySettings.ManualWithLinks); + + public void Apply(ActorIdentifier actor, DesignBase design, ApplySettings settings) + => Apply(actor, objects.TryGetValue(actor, out var d) ? d : ActorData.Invalid, design, settings); + + public void Apply(ActorIdentifier actor, ActorData data, DesignBase design) + => Apply(actor, data, design, ApplySettings.ManualWithLinks); + + public void Apply(ActorIdentifier actor, ActorData data, DesignBase design, ApplySettings settings) + { + if (!actor.IsValid || !data.Valid) + return; + + if (!stateManager.GetOrCreate(actor, data.Objects[0], out var state)) + return; + + stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks); + } +} diff --git a/Glamourer/Services/DesignResolver.cs b/Glamourer/Services/DesignResolver.cs new file mode 100644 index 0000000..8bb5cd2 --- /dev/null +++ b/Glamourer/Services/DesignResolver.cs @@ -0,0 +1,173 @@ +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Plugin.Services; +using Glamourer.Designs; +using Glamourer.Designs.Special; +using Glamourer.Gui; +using Glamourer.Gui.Tabs.DesignTab; +using Dalamud.Bindings.ImGui; +using OtterGui.Services; +using OtterGui.Classes; + +namespace Glamourer.Services; + +public class DesignResolver( + DesignFileSystemSelector designSelector, + QuickDesignCombo quickDesignCombo, + DesignConverter converter, + DesignManager manager, + DesignFileSystem designFileSystem, + RandomDesignGenerator randomDesign, + IChatGui chat) : IService +{ + private const string RandomString = "random"; + + public bool GetDesign(string argument, [NotNullWhen(true)] out DesignBase? design, bool allowSpecial) + { + if (GetDesign(argument, out design, out var error, out var message, allowSpecial)) + { + if (message != null) + chat.Print(message); + return true; + } + + if (error != null) + chat.Print(error); + return false; + } + + public bool GetDesign(string argument, [NotNullWhen(true)] out DesignBase? design, out SeString? error, out SeString? message, + bool allowSpecial) + { + design = null; + error = null; + message = null; + + if (argument.Length == 0) + return false; + + if (allowSpecial) + { + if (string.Equals("selection", argument, StringComparison.OrdinalIgnoreCase)) + return GetSelectedDesign(ref design, ref error); + + if (string.Equals("quick", argument, StringComparison.OrdinalIgnoreCase)) + return GetQuickDesign(ref design, ref error); + + if (string.Equals("clipboard", argument, StringComparison.OrdinalIgnoreCase)) + return GetClipboardDesign(ref design, ref error); + + if (argument.StartsWith(RandomString, StringComparison.OrdinalIgnoreCase)) + return GetRandomDesign(argument, ref design, ref error, ref message); + } + + return GetStandardDesign(argument, ref design, ref error); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetSelectedDesign(ref DesignBase? design, ref SeString? error) + { + design = designSelector.Selected; + if (design != null) + return true; + + error = "You do not have selected any design in the Designs Tab."; + return false; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetQuickDesign(ref DesignBase? design, ref SeString? error) + { + design = quickDesignCombo.Design as Design; + if (design != null) + return true; + + error = "You do not have selected any design in the Quick Design Bar."; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetClipboardDesign(ref DesignBase? design, ref SeString? error) + { + try + { + var clipboardText = ImGui.GetClipboardText(); + if (clipboardText.Length > 0) + design = converter.FromBase64(clipboardText, true, true, out _); + } + catch + { + // ignored + } + + if (design != null) + return true; + + error = "Your current clipboard did not contain a valid design string."; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetRandomDesign(string argument, ref DesignBase? design, ref SeString? error, ref SeString? message) + { + try + { + if (argument.Length == RandomString.Length) + design = randomDesign.Design(); + else if (argument[RandomString.Length] == ':') + design = randomDesign.Design(argument[(RandomString.Length + 1)..]); + if (design == null) + { + error = "No design matched your restrictions."; + return false; + } + + message = $"Chose random design {((Design)design).Name}."; + } + catch (Exception ex) + { + error = $"Error in the restriction string: {ex.Message}"; + return false; + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetStandardDesign(string argument, ref DesignBase? design, ref SeString? error) + { + // As Guid + if (Guid.TryParse(argument, out var guid)) + { + design = manager.Designs.ByIdentifier(guid); + } + else + { + var lower = argument.ToLowerInvariant(); + // Search for design by name and partial identifier. + design = manager.Designs.FirstOrDefault(MatchNameAndIdentifier(lower)); + // Search for design by path, if nothing was found. + if (design == null && designFileSystem.Find(lower, out var child) && child is DesignFileSystem.Leaf leaf) + design = leaf.Value; + } + + if (design != null) + return true; + + error = new SeStringBuilder().AddText("The token ").AddYellow(argument, true).AddText(" did not resolve to an existing design.") + .BuiltString; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Func MatchNameAndIdentifier(string lower) + { + // Check for names and identifiers, prefer names + if (lower.Length > 3) + return d => d.Name.Lower == lower || d.Identifier.ToString().StartsWith(lower); + + // Check only for names. + return d => d.Name.Lower == lower; + } +} diff --git a/Glamourer/Services/FilenameService.cs b/Glamourer/Services/FilenameService.cs index 7299d32..cd25c64 100644 --- a/Glamourer/Services/FilenameService.cs +++ b/Glamourer/Services/FilenameService.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.IO; using Dalamud.Plugin; using Glamourer.Designs; @@ -16,21 +14,28 @@ public class FilenameService public readonly string UnlockFileCustomize; public readonly string UnlockFileItems; public readonly string FavoriteFile; + public readonly string DesignColorFile; + public readonly string EphemeralConfigFile; + public readonly string NpcAppearanceFile; + public readonly string CollectionOverrideFile; - public FilenameService(DalamudPluginInterface pi) + public FilenameService(IDalamudPluginInterface pi) { - ConfigDirectory = pi.ConfigDirectory.FullName; - ConfigFile = pi.ConfigFile.FullName; - AutomationFile = Path.Combine(ConfigDirectory, "automation.json"); - DesignFileSystem = Path.Combine(ConfigDirectory, "sort_order.json"); - MigrationDesignFile = Path.Combine(ConfigDirectory, "Designs.json"); - UnlockFileCustomize = Path.Combine(ConfigDirectory, "unlocks_customize.json"); - UnlockFileItems = Path.Combine(ConfigDirectory, "unlocks_items.json"); - DesignDirectory = Path.Combine(ConfigDirectory, "designs"); - FavoriteFile = Path.Combine(ConfigDirectory, "favorites.json"); + ConfigDirectory = pi.ConfigDirectory.FullName; + ConfigFile = pi.ConfigFile.FullName; + AutomationFile = Path.Combine(ConfigDirectory, "automation.json"); + DesignFileSystem = Path.Combine(ConfigDirectory, "sort_order.json"); + MigrationDesignFile = Path.Combine(ConfigDirectory, "Designs.json"); + UnlockFileCustomize = Path.Combine(ConfigDirectory, "unlocks_customize.json"); + UnlockFileItems = Path.Combine(ConfigDirectory, "unlocks_items.json"); + DesignDirectory = Path.Combine(ConfigDirectory, "designs"); + FavoriteFile = Path.Combine(ConfigDirectory, "favorites.json"); + DesignColorFile = Path.Combine(ConfigDirectory, "design_colors.json"); + EphemeralConfigFile = Path.Combine(ConfigDirectory, "ephemeral_config.json"); + NpcAppearanceFile = Path.Combine(ConfigDirectory, "npc_appearance_data.json"); + CollectionOverrideFile = Path.Combine(ConfigDirectory, "collection_overrides.json"); } - public IEnumerable Designs() { if (!Directory.Exists(DesignDirectory)) diff --git a/Glamourer/Services/HeightService.cs b/Glamourer/Services/HeightService.cs new file mode 100644 index 0000000..0a6c7bb --- /dev/null +++ b/Glamourer/Services/HeightService.cs @@ -0,0 +1,24 @@ +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Services; + +public unsafe class HeightService : IService +{ + [Signature(Sigs.CalculateHeight)] + private readonly delegate* unmanaged[Stdcall] _calculateHeight = null!; + + public HeightService(IGameInteropProvider interop) + => interop.InitializeFromAttributes(this); + + public float Height(CustomizeValue height, SubRace clan, Gender gender, CustomizeValue bodyType) + => _calculateHeight(CharacterUtility.Instance(), height.Value, (byte)clan, (byte)((byte)gender - 1), bodyType.Value); + + public float Height(in CustomizeArray customize) + => Height(customize[CustomizeIndex.Height], customize.Clan, customize.Gender, customize.BodyType); +} diff --git a/Glamourer/Services/ItemManager.cs b/Glamourer/Services/ItemManager.cs index 59e8228..a885b54 100644 --- a/Glamourer/Services/ItemManager.cs +++ b/Glamourer/Services/ItemManager.cs @@ -1,48 +1,42 @@ -using System; -using System.Linq; -using System.Runtime.CompilerServices; -using Dalamud.Plugin; using Dalamud.Plugin.Services; using Lumina.Excel; +using Lumina.Excel.Sheets; using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Race = Penumbra.GameData.Enums.Race; namespace Glamourer.Services; -public class ItemManager : IDisposable +public class ItemManager { - public const string Nothing = "Nothing"; + public const string Nothing = EquipItem.Nothing; public const string SmallClothesNpc = "Smallclothes (NPC)"; public const ushort SmallClothesNpcModel = 9903; private readonly Configuration _config; - public readonly IdentifierService IdentifierService; - public readonly ExcelSheet ItemSheet; - public readonly StainData Stains; - public readonly ItemService ItemService; - public readonly RestrictedGear RestrictedGear; + public readonly ObjectIdentification ObjectIdentification; + public readonly ExcelSheet ItemSheet; + public readonly DictStain Stains; + public readonly ItemData ItemData; + public readonly DictBonusItems DictBonusItems; + public readonly RestrictedGear RestrictedGear; public readonly EquipItem DefaultSword; - public ItemManager(Configuration config, DalamudPluginInterface pi, IDataManager gameData, IdentifierService identifierService, - ItemService itemService, IPluginLog log) + public ItemManager(Configuration config, IDataManager gameData, ObjectIdentification objectIdentification, + ItemData itemData, DictStain stains, RestrictedGear restrictedGear, DictBonusItems dictBonusItems) { - _config = config; - ItemSheet = gameData.GetExcelSheet()!; - IdentifierService = identifierService; - Stains = new StainData(pi, gameData, gameData.Language, log); - ItemService = itemService; - RestrictedGear = new RestrictedGear(pi, gameData.Language, gameData, log); - DefaultSword = EquipItem.FromMainhand(ItemSheet.GetRow(1601)!); // Weathered Shortsword - } - - public void Dispose() - { - Stains.Dispose(); - RestrictedGear.Dispose(); + _config = config; + ItemSheet = gameData.GetExcelSheet(); + ObjectIdentification = objectIdentification; + ItemData = itemData; + Stains = stains; + RestrictedGear = restrictedGear; + DictBonusItems = dictBonusItems; + DefaultSword = EquipItem.FromMainhand(ItemSheet.GetRow(1601)); // Weathered Shortsword } public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender) @@ -66,7 +60,7 @@ public class ItemManager : IDisposable public static EquipItem SmallClothesItem(EquipSlot slot) => new(SmallClothesNpc, SmallclothesId(slot), 0, SmallClothesNpcModel, 0, 1, slot.ToEquipType(), 0, 0, 0); - public EquipItem Resolve(EquipSlot slot, ItemId itemId) + public EquipItem Resolve(EquipSlot slot, CustomItemId itemId) { slot = slot.ToSlot(); if (itemId == NothingId(slot)) @@ -74,14 +68,26 @@ public class ItemManager : IDisposable if (itemId == SmallclothesId(slot)) return SmallClothesItem(slot); - if (!ItemService.AwaitedService.TryGetValue(itemId, slot, out var item)) + if (!itemId.IsItem) + { + var item = EquipItem.FromId(itemId); + item = slot is EquipSlot.MainHand or EquipSlot.OffHand + ? Identify(slot, item.PrimaryId, item.SecondaryId, item.Variant) + : Identify(slot, item.PrimaryId, item.Variant); + return item; + } + else if (!ItemData.TryGetValue(itemId.Item, slot, out var item)) + { return EquipItem.FromId(itemId); + } + else + { + if (item.Type.ToSlot() != slot) + return new EquipItem(string.Intern($"Invalid #{itemId}"), itemId, item.IconId, item.PrimaryId, item.SecondaryId, item.Variant, + 0, 0, 0, 0); - if (item.Type.ToSlot() != slot) - return new EquipItem(string.Intern($"Invalid #{itemId}"), itemId, item.IconId, item.ModelId, item.WeaponType, item.Variant, 0, 0, 0, - 0); - - return item; + return item; + } } public EquipItem Resolve(FullEquipType type, ItemId itemId) @@ -89,13 +95,13 @@ public class ItemManager : IDisposable if (itemId == NothingId(type)) return NothingItem(type); - if (!ItemService.AwaitedService.TryGetValue(itemId, type is FullEquipType.Shield ? EquipSlot.MainHand : EquipSlot.OffHand, + if (!ItemData.TryGetValue(itemId, type is FullEquipType.Shield ? EquipSlot.MainHand : EquipSlot.OffHand, out var item)) return EquipItem.FromId(itemId); if (item.Type != type) - return new EquipItem(string.Intern($"Invalid #{itemId}"), itemId, item.IconId, item.ModelId, item.WeaponType, item.Variant, 0, 0, 0, - 0); + return new EquipItem(string.Intern($"Invalid #{itemId}"), itemId, item.IconId, item.PrimaryId, item.SecondaryId, item.Variant, + 0, 0, 0, 0); return item; } @@ -103,7 +109,7 @@ public class ItemManager : IDisposable public EquipItem Resolve(FullEquipType type, CustomItemId id) => id.IsItem ? Resolve(type, id.Item) : EquipItem.FromId(id); - public EquipItem Identify(EquipSlot slot, SetId id, Variant variant) + public EquipItem Identify(EquipSlot slot, PrimaryId id, Variant variant) { slot = slot.ToSlot(); if (slot.ToIndex() == uint.MaxValue) @@ -114,13 +120,52 @@ public class ItemManager : IDisposable case 0: return NothingItem(slot); case SmallClothesNpcModel: return SmallClothesItem(slot); default: - var item = IdentifierService.AwaitedService.Identify(id, variant, slot).FirstOrDefault(); + var item = ObjectIdentification.Identify(id, 0, variant, slot).FirstOrDefault(); return item.Valid ? item : EquipItem.FromIds(0, 0, id, 0, variant, slot.ToEquipType()); } } + public EquipItem Identify(BonusItemFlag slot, PrimaryId id, Variant variant) + { + var index = slot.ToIndex(); + if (index == uint.MaxValue) + return new EquipItem($"Invalid ({id.Id}-{variant})", 0, 0, id, 0, variant, slot.ToEquipType(), 0, 0, 0); + + return ObjectIdentification.Identify(id, variant, slot) + .FirstOrDefault(new EquipItem($"Invalid ({id.Id}-{variant})", 0, 0, id, 0, variant, slot.ToEquipType(), 0, 0, 0)); + } + + public EquipItem Resolve(BonusItemFlag slot, BonusItemId id) + => IsBonusItemValid(slot, id, out var item) ? item : new EquipItem($"Invalid ({id.Id})", id, 0, 0, 0, 0, slot.ToEquipType(), 0, 0, 0); + + public EquipItem Resolve(BonusItemFlag slot, CustomItemId id) + { + // Only from early designs as migration. + if (!id.IsBonusItem || id.Id == 0) + { + if (IsBonusItemValid(slot, (BonusItemId)id.Id, out var item)) + return item; + + return EquipItem.BonusItemNothing(slot); + } + + if (!id.IsCustom) + { + if (IsBonusItemValid(slot, id.BonusItem, out var item)) + return item; + + return EquipItem.BonusItemNothing(slot); + } + + var (model, variant, slot2) = id.SplitBonus; + if (slot != slot2) + return EquipItem.BonusItemNothing(slot); + + return Identify(slot, model, variant); + } + /// Return the default offhand for a given mainhand, that is for both handed weapons, return the correct offhand part, and for everything else Nothing. public EquipItem GetDefaultOffhand(EquipItem mainhand) { @@ -131,7 +176,38 @@ public class ItemManager : IDisposable return NothingItem(offhandType); } - public EquipItem Identify(EquipSlot slot, SetId id, WeaponType type, Variant variant, FullEquipType mainhandType = FullEquipType.Unknown) + public bool FindClosestShield(ItemId id, out EquipItem item) + { + var list = ItemData.ByType[FullEquipType.Shield]; + try + { + item = list.Where(i => i.ItemId.Id > id.Id && i.ItemId.Id - id.Id < 50).MinBy(i => i.ItemId.Id); + return true; + } + catch + { + item = default; + return false; + } + } + + public bool FindClosestSword(ItemId id, out EquipItem item) + { + var list = ItemData.ByType[FullEquipType.Sword]; + try + { + item = list.Where(i => i.ItemId.Id < id.Id && id.Id - i.ItemId.Id < 50).MaxBy(i => i.ItemId.Id); + return true; + } + catch + { + item = default; + return false; + } + } + + public EquipItem Identify(EquipSlot slot, PrimaryId id, SecondaryId type, Variant variant, + FullEquipType mainhandType = FullEquipType.Unknown) { if (slot is EquipSlot.OffHand) { @@ -143,7 +219,7 @@ public class ItemManager : IDisposable if (slot is not EquipSlot.MainHand and not EquipSlot.OffHand) return new EquipItem($"Invalid ({id.Id}-{type.Id}-{variant})", 0, 0, id, type, variant, 0, 0, 0, 0); - var item = IdentifierService.AwaitedService.Identify(id, type, variant, slot).FirstOrDefault(i => i.Type.ToSlot() == slot); + var item = ObjectIdentification.Identify(id, type, variant, slot).FirstOrDefault(i => i.Type.ToSlot() == slot); return item.Valid ? item : EquipItem.FromIds(0, 0, id, type, variant, slot.ToEquipType()); @@ -151,12 +227,24 @@ public class ItemManager : IDisposable /// Returns whether an item id represents a valid item for a slot and gives the item. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public bool IsItemValid(EquipSlot slot, ItemId itemId, out EquipItem item) + public bool IsItemValid(EquipSlot slot, CustomItemId itemId, out EquipItem item) { item = Resolve(slot, itemId); return item.Valid; } + /// Returns whether a bonus item id represents a valid item for a slot and gives the item. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsBonusItemValid(BonusItemFlag slot, BonusItemId itemId, out EquipItem item) + { + if (itemId.Id != 0) + return DictBonusItems.TryGetValue(itemId, out item) && slot == item.Type.ToBonus(); + + item = EquipItem.BonusItemNothing(slot); + return true; + } + + /// /// Check whether an item id resolves to an existing item of the correct slot (which should not be weapons.) /// The returned item is either the resolved correct item, or the Nothing item for that slot. @@ -191,16 +279,16 @@ public class ItemManager : IDisposable /// The returned stain id is either the input or 0. /// The return value is an empty string if there was no problem and a warning otherwise. /// - public string ValidateStain(StainId stain, out StainId ret, bool allowUnknown) + public string ValidateStain(StainIds stains, out StainIds ret, bool allowUnknown) { - if (allowUnknown || IsStainValid(stain)) + if (allowUnknown || stains.All(IsStainValid)) { - ret = stain; + ret = stains; return string.Empty; } - ret = 0; - return $"The Stain {stain} does not exist, reset to unstained."; + ret = StainIds.None; + return $"The Stain {stains} does not exist, reset to unstained."; } /// Returns whether an offhand is valid given the required offhand type. diff --git a/Glamourer/Services/PcpService.cs b/Glamourer/Services/PcpService.cs new file mode 100644 index 0000000..3363172 --- /dev/null +++ b/Glamourer/Services/PcpService.cs @@ -0,0 +1,119 @@ +using Glamourer.Designs; +using Glamourer.Interop.Penumbra; +using Glamourer.State; +using Newtonsoft.Json.Linq; +using OtterGui.Services; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; + +namespace Glamourer.Services; + +public class PcpService : IRequiredService +{ + private readonly Configuration _config; + private readonly PenumbraService _penumbra; + private readonly ActorObjectManager _objects; + private readonly StateManager _state; + private readonly DesignConverter _designConverter; + private readonly DesignManager _designManager; + + public PcpService(Configuration config, PenumbraService penumbra, ActorObjectManager objects, StateManager state, + DesignConverter designConverter, DesignManager designManager) + { + _config = config; + _penumbra = penumbra; + _objects = objects; + _state = state; + _designConverter = designConverter; + _designManager = designManager; + + _config.AttachToPcp = !_config.AttachToPcp; + Set(!_config.AttachToPcp); + } + + public void CleanPcpDesigns() + { + var designs = _designManager.Designs.Where(d => d.Tags.Contains("PCP")).ToList(); + Glamourer.Log.Information($"[PCPService] Deleting {designs.Count} designs containing the tag PCP."); + foreach (var design in designs) + _designManager.Delete(design); + } + + public void Set(bool value) + { + if (value == _config.AttachToPcp) + return; + + _config.AttachToPcp = value; + _config.Save(); + if (value) + { + Glamourer.Log.Information("[PCPService] Attached to PCP handling."); + _penumbra.PcpCreated += OnPcpCreation; + _penumbra.PcpParsed += OnPcpParse; + } + else + { + Glamourer.Log.Information("[PCPService] Detached from PCP handling."); + _penumbra.PcpCreated -= OnPcpCreation; + _penumbra.PcpParsed -= OnPcpParse; + } + } + + private void OnPcpParse(JObject jObj, string modDirectory, Guid collection) + { + Glamourer.Log.Debug("[PCPService] Parsing PCP file."); + if (jObj["Glamourer"] is not JObject glamourer) + return; + + if (glamourer["Version"]!.ToObject() is not 1) + return; + + if (_designConverter.FromJObject(glamourer["Design"] as JObject, true, true) is not { } designBase) + return; + + var actorIdentifier = _objects.Actors.FromJson(jObj["Actor"] as JObject); + if (!actorIdentifier.IsValid) + return; + + var time = new DateTimeOffset(jObj["Time"]?.ToObject() ?? DateTime.UtcNow); + var design = _designManager.CreateClone(designBase, + $"{_config.PcpFolder}/{actorIdentifier} - {jObj["Note"]?.ToObject() ?? string.Empty}", true); + _designManager.AddTag(design, "PCP"); + _designManager.SetWriteProtection(design, true); + _designManager.AddMod(design, new Mod(modDirectory, modDirectory), new ModSettings([], 0, true, false, false)); + _designManager.ChangeDescription(design, $"PCP design created for {actorIdentifier} on {time}."); + _designManager.ChangeResetAdvancedDyes(design, true); + _designManager.SetQuickDesign(design, false); + _designManager.ChangeColor(design, _config.PcpColor); + + Glamourer.Log.Debug("[PCPService] Created PCP design."); + if (_state.GetOrCreate(actorIdentifier, _objects.TryGetValue(actorIdentifier, out var data) ? data.Objects[0] : Actor.Null, + out var state)) + { + _state.ApplyDesign(state!, design, ApplySettings.Manual); + Glamourer.Log.Debug($"[PCPService] Applied PCP design to {actorIdentifier.Incognito(null)}"); + } + } + + private void OnPcpCreation(JObject jObj, ushort index, string path) + { + Glamourer.Log.Debug("[PCPService] Adding Glamourer data to PCP file."); + var actorIdentifier = _objects.Actors.FromJson(jObj["Actor"] as JObject); + if (!actorIdentifier.IsValid) + return; + + if (!_state.GetOrCreate(actorIdentifier, _objects.Objects[(int)index], out var state)) + { + Glamourer.Log.Debug($"[PCPService] Could not get or create state for actor {index}."); + return; + } + + var design = _designConverter.Convert(state, ApplicationRules.All); + jObj["Glamourer"] = new JObject + { + ["Version"] = 1, + ["Design"] = design.JsonSerialize(), + }; + } +} diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index a848f96..6cfb4b6 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin; using Glamourer.Api; +using Glamourer.Api.Api; using Glamourer.Automation; using Glamourer.Designs; using Glamourer.Events; @@ -9,7 +10,10 @@ using Glamourer.Gui.Equipment; using Glamourer.Gui.Tabs; using Glamourer.Gui.Tabs.ActorTab; using Glamourer.Gui.Tabs.AutomationTab; +using Glamourer.Gui.Tabs.DebugTab; using Glamourer.Gui.Tabs.DesignTab; +using Glamourer.Gui.Tabs.NpcTab; +using Glamourer.Gui.Tabs.SettingsTab; using Glamourer.Gui.Tabs.UnlocksTab; using Glamourer.Interop; using Glamourer.Interop.Penumbra; @@ -18,18 +22,23 @@ using Glamourer.Unlocks; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; using OtterGui.Log; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.GameData.Actors; using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; namespace Glamourer.Services; -public static class ServiceManager +public static class StaticServiceManager { - public static ServiceProvider CreateProvider(DalamudPluginInterface pi, Logger log) + public static ServiceManager CreateProvider(IDalamudPluginInterface pi, Logger log, Glamourer glamourer) { - EventWrapper.ChangeLogger(log); - var services = new ServiceCollection() - .AddSingleton(log) - .AddDalamud(pi) + EventWrapperBase.ChangeLogger(log); + var services = new ServiceManager(log) + .AddExistingService(log) .AddMeta() .AddInterop() .AddEvents() @@ -37,18 +46,17 @@ public static class ServiceManager .AddDesigns() .AddState() .AddUi() - .AddApi(); - - return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); - } - - private static IServiceCollection AddDalamud(this IServiceCollection services, DalamudPluginInterface pi) - { - new DalamudServices(pi).AddServices(services); + .AddExistingService(glamourer); + DalamudServices.AddServices(services, pi); + services.AddIServices(typeof(EquipItem).Assembly); + services.AddIServices(typeof(Glamourer).Assembly); + services.AddIServices(typeof(ImRaii).Assembly); + services.AddSingleton(p => p.GetRequiredService()); + services.CreateProvider(); return services; } - private static IServiceCollection AddMeta(this IServiceCollection services) + private static ServiceManager AddMeta(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton() @@ -57,12 +65,14 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); - private static IServiceCollection AddEvents(this IServiceCollection services) + private static ServiceManager AddEvents(this ServiceManager services) => services.AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -72,65 +82,81 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); - private static IServiceCollection AddData(this IServiceCollection services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() + private static ServiceManager AddData(this ServiceManager services) + => services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton(); - private static IServiceCollection AddInterop(this IServiceCollection services) + private static ServiceManager AddInterop(this ServiceManager services) => services.AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton(p => new CutsceneResolver(p.GetRequiredService().CutsceneParent)) .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); - private static IServiceCollection AddDesigns(this IServiceCollection services) + private static ServiceManager AddDesigns(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); - private static IServiceCollection AddState(this IServiceCollection services) + private static ServiceManager AddState(this ServiceManager services) => services.AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); - private static IServiceCollection AddUi(this IServiceCollection services) + private static ServiceManager AddUi(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -142,9 +168,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); - - private static IServiceCollection AddApi(this IServiceCollection services) - => services.AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); } diff --git a/Glamourer/Services/ServiceWrapper.cs b/Glamourer/Services/ServiceWrapper.cs deleted file mode 100644 index ded8ef0..0000000 --- a/Glamourer/Services/ServiceWrapper.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Dalamud.Plugin; -using Penumbra.GameData.Actors; -using System; -using System.Threading.Tasks; -using Dalamud.Plugin.Services; -using Glamourer.Interop.Penumbra; -using Penumbra.GameData.Data; -using Penumbra.GameData; - -namespace Glamourer.Services; - -public abstract class AsyncServiceWrapper : IDisposable -{ - public string Name { get; } - public T? Service { get; private set; } - - public T AwaitedService - { - get - { - _task?.Wait(); - return Service!; - } - } - - public bool Valid - => Service != null && !_isDisposed; - - public event Action? FinishedCreation; - private Task? _task; - - private bool _isDisposed; - - protected AsyncServiceWrapper(string name, Func factory) - { - Name = name; - _task = Task.Run(() => - { - var service = factory(); - if (_isDisposed) - { - if (service is IDisposable d) - d.Dispose(); - } - else - { - Service = service; - Glamourer.Log.Verbose($"[{Name}] Created."); - _task = null; - } - }); - _task.ContinueWith((t, x) => - { - if (!_isDisposed) - FinishedCreation?.Invoke(); - }, null); - } - - public void Dispose() - { - if (_isDisposed) - return; - - _isDisposed = true; - _task = null; - if (Service is IDisposable d) - d.Dispose(); - Glamourer.Log.Verbose($"[{Name}] Disposed."); - } -} - -public sealed class IdentifierService : AsyncServiceWrapper -{ - public IdentifierService(DalamudPluginInterface pi, IDataManager data, ItemService itemService, IPluginLog log) - : base(nameof(IdentifierService), () => Penumbra.GameData.GameData.GetIdentifier(pi, data, itemService.AwaitedService, log)) - { } -} - -public sealed class ItemService : AsyncServiceWrapper -{ - public ItemService(DalamudPluginInterface pi, IDataManager gameData, IPluginLog log) - : base(nameof(ItemService), () => new ItemData(pi, gameData, gameData.Language, log)) - { } -} - -public sealed class ActorService : AsyncServiceWrapper -{ - public ActorService(DalamudPluginInterface pi, IObjectTable objects, IClientState clientState, IFramework framework, IGameInteropProvider interop, IDataManager gameData, - IGameGui gui, PenumbraService penumbra, IPluginLog log) - : base(nameof(ActorService), - () => new ActorManager(pi, objects, clientState, framework, interop, gameData, gui, idx => (short)penumbra.CutsceneParent(idx), log)) - { } -} diff --git a/Glamourer/Services/TextureService.cs b/Glamourer/Services/TextureService.cs index 8f65bfa..a0ec443 100644 --- a/Glamourer/Services/TextureService.cs +++ b/Glamourer/Services/TextureService.cs @@ -1,7 +1,6 @@ -using System; -using System.Numerics; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; using OtterGui.Classes; using Penumbra.GameData.Enums; @@ -9,23 +8,35 @@ using Penumbra.GameData.Structs; namespace Glamourer.Services; -public sealed class TextureService : TextureCache, IDisposable +public sealed class TextureService(IUiBuilder uiBuilder, IDataManager dataManager, ITextureProvider textureProvider) + : TextureCache(dataManager, textureProvider), IDisposable { - public TextureService(UiBuilder uiBuilder, IDataManager dataManager, ITextureProvider textureProvider) - : base(dataManager, textureProvider) - => _slotIcons = CreateSlotIcons(uiBuilder); + private readonly IDalamudTextureWrap?[] _slotIcons = CreateSlotIcons(uiBuilder); - private readonly IDalamudTextureWrap?[] _slotIcons; - - public (nint, Vector2, bool) GetIcon(EquipItem item, EquipSlot slot) + public (ImTextureID, Vector2, bool) GetIcon(EquipItem item, EquipSlot slot) { if (item.IconId.Id != 0 && TryLoadIcon(item.IconId.Id, out var ret)) - return (ret.ImGuiHandle, new Vector2(ret.Width, ret.Height), false); + return (ret.Handle, new Vector2(ret.Width, ret.Height), false); var idx = slot.ToIndex(); return idx < 12 && _slotIcons[idx] != null - ? (_slotIcons[idx]!.ImGuiHandle, new Vector2(_slotIcons[idx]!.Width, _slotIcons[idx]!.Height), true) - : (nint.Zero, Vector2.Zero, true); + ? (_slotIcons[idx]!.Handle, new Vector2(_slotIcons[idx]!.Width, _slotIcons[idx]!.Height), true) + : (default, Vector2.Zero, true); + } + + public (ImTextureID, Vector2, bool) GetIcon(EquipItem item, BonusItemFlag slot) + { + if (item.IconId.Id != 0 && TryLoadIcon(item.IconId.Id, out var ret)) + return (ret.Handle, new Vector2(ret.Width, ret.Height), false); + + var idx = slot.ToIndex(); + if (idx == uint.MaxValue) + return (default, Vector2.Zero, true); + + idx += 12; + return idx < 13 && _slotIcons[idx] != null + ? (_slotIcons[idx]!.Handle, new Vector2(_slotIcons[idx]!.Width, _slotIcons[idx]!.Height), true) + : (default, Vector2.Zero, true); } public void Dispose() @@ -37,11 +48,11 @@ public sealed class TextureService : TextureCache, IDisposable } } - private static IDalamudTextureWrap[] CreateSlotIcons(UiBuilder uiBuilder) + private static IDalamudTextureWrap?[] CreateSlotIcons(IUiBuilder uiBuilder) { - var ret = new IDalamudTextureWrap[12]; + var ret = new IDalamudTextureWrap?[13]; - using var uldWrapper = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld"); + using var uldWrapper = uiBuilder.LoadUld("ui/uld/Character.uld"); if (!uldWrapper.Valid) { @@ -49,20 +60,37 @@ public sealed class TextureService : TextureCache, IDisposable return ret; } - ret[0] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1)!; - ret[1] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2)!; - ret[2] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3)!; - ret[3] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5)!; - ret[4] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6)!; - ret[5] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8)!; - ret[6] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)!; - ret[7] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)!; - ret[8] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)!; - ret[9] = ret[8]; - ret[10] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0)!; - ret[11] = uldWrapper.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7)!; + SetIcon(EquipSlot.Head, 19); + SetIcon(EquipSlot.Body, 20); + SetIcon(EquipSlot.Hands, 21); + SetIcon(EquipSlot.Legs, 23); + SetIcon(EquipSlot.Feet, 24); + SetIcon(EquipSlot.Ears, 25); + SetIcon(EquipSlot.Neck, 26); + SetIcon(EquipSlot.Wrists, 27); + SetIcon(EquipSlot.RFinger, 28); + SetIcon(EquipSlot.MainHand, 17); + SetIcon(EquipSlot.OffHand, 18); + Set(BonusItemFlag.Glasses.ToName(), (int) BonusItemFlag.Glasses.ToIndex() + 12, 55); + ret[EquipSlot.LFinger.ToIndex()] = ret[EquipSlot.RFinger.ToIndex()]; - uldWrapper.Dispose(); return ret; + + void Set(string name, int slot, int index) + { + try + { + ret[slot] = uldWrapper.LoadTexturePart("ui/uld/Character_hr1.tex", index)!; + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not get empty slot texture for {name}, icon will be left empty. " + + $"This may be because of incompatible mods affecting your character screen interface:\n{ex}"); + ret[slot] = null; + } + } + + void SetIcon(EquipSlot slot, int index) + => Set(slot.ToName(), (int)slot.ToIndex(), index); } } diff --git a/Glamourer/State/ActorState.cs b/Glamourer/State/ActorState.cs index 2cb3f2a..5d582f6 100644 --- a/Glamourer/State/ActorState.cs +++ b/Glamourer/State/ActorState.cs @@ -1,27 +1,15 @@ -using Glamourer.Customization; -using Glamourer.Designs; -using Glamourer.Events; -using Glamourer.Structs; +using Glamourer.Designs; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; -using System.Linq; using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; -using CustomizeIndex = Glamourer.Customization.CustomizeIndex; +using Glamourer.GameData; +using Penumbra.GameData.Structs; namespace Glamourer.State; public class ActorState { - public enum MetaIndex - { - Wetness = EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices, - HatState, - VisorState, - WeaponState, - ModelId, - } - public readonly ActorIdentifier Identifier; public bool AllowsRedraw(ICondition condition) @@ -34,7 +22,7 @@ public class ActorState public DesignData ModelData; /// The last seen job. - public byte LastJob; + public JobId LastJob; /// The Lock-Key locking this state. public uint Combination; @@ -42,6 +30,9 @@ public class ActorState /// The territory the draw object was created last. public ushort LastTerritory; + /// State for specific material values. + public readonly StateMaterialManager Materials = new(); + /// Whether the State is locked at all. public bool IsLocked => Combination != 0; @@ -79,29 +70,14 @@ public class ActorState => Unlock(1337); /// This contains whether a change to the base data was made by the game, the user via manual input or through automatic application. - private readonly StateChanged.Source[] _sources = Enumerable - .Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5).ToArray(); + public StateSources Sources = new(); internal ActorState(ActorIdentifier identifier) => Identifier = identifier.CreatePermanent(); - public ref StateChanged.Source this[EquipSlot slot, bool stain] - => ref _sources[slot.ToIndex() + (stain ? EquipFlagExtensions.NumEquipFlags / 2 : 0)]; - - public ref StateChanged.Source this[CustomizeIndex type] - => ref _sources[EquipFlagExtensions.NumEquipFlags + (int)type]; - - public ref StateChanged.Source this[MetaIndex index] - => ref _sources[(int)index]; - - public void RemoveFixedDesignSources() - { - for (var i = 0; i < _sources.Length; ++i) - { - if (_sources[i] is StateChanged.Source.Fixed) - _sources[i] = StateChanged.Source.Manual; - } - } + public CustomizeParameterFlag OnlyChangedParameters() + => CustomizeParameterExtensions.AllFlags.Where(f => Sources[f] is not StateSource.Game) + .Aggregate((CustomizeParameterFlag)0, (a, b) => a | b); public bool UpdateTerritory(ushort territory) { diff --git a/Glamourer/State/FunEquipSet.cs b/Glamourer/State/FunEquipSet.cs index c2618b0..c1ae02e 100644 --- a/Glamourer/State/FunEquipSet.cs +++ b/Glamourer/State/FunEquipSet.cs @@ -1,6 +1,4 @@ -using System; -using Glamourer.Interop.Structs; -using Penumbra.GameData.Structs; +using Penumbra.GameData.Structs; namespace Glamourer.State; @@ -22,8 +20,8 @@ internal class FunEquipSet { public Group(ushort headS, byte headV, ushort bodyS, byte bodyV, ushort handsS, byte handsV, ushort legsS, byte legsV, ushort feetS, byte feetV, StainId[]? stains = null) - : this(new CharacterArmor(headS, headV, 0), new CharacterArmor(bodyS, bodyV, 0), new CharacterArmor(handsS, handsV, 0), - new CharacterArmor(legsS, legsV, 0), new CharacterArmor(feetS, feetV, 0), stains) + : this(new CharacterArmor(headS, headV, StainIds.None), new CharacterArmor(bodyS, bodyV, StainIds.None), new CharacterArmor(handsS, handsV, StainIds.None), + new CharacterArmor(legsS, legsV, StainIds.None), new CharacterArmor(feetS, feetV, StainIds.None), stains) { } public static Group FullSetWithoutHat(ushort modelSet, byte variant, StainId[]? stains = null) @@ -64,13 +62,13 @@ internal class FunEquipSet new Group(6005, 1, 0058, 1, 6005, 1, 0000, 0, 6005, 1), // Reindeer new Group(0231, 1, 0231, 1, 0279, 1, 0231, 1, 0231, 1), // Starlight new Group(0231, 1, 6030, 1, 0279, 1, 0231, 1, 0231, 1), // Starlight - new Group(0053, 1, 0053, 1, 0279, 1, 0279, 1, 0053, 1), // Sweet Dream + new Group(0053, 1, 0053, 1, 0279, 1, 0049, 6, 0053, 1), // Sweet Dream new Group(0136, 1, 0136, 1, 0136, 1, 0000, 0, 0000, 0) // Snowman ); public static readonly FunEquipSet Halloween = new ( - new Group(0316, 1, 0316, 1, 0316, 1, 0279, 1, 0316, 1), // Witch + new Group(0316, 1, 0316, 1, 0316, 1, 0049, 6, 0316, 1), // Witch new Group(6047, 1, 6047, 1, 6047, 1, 6047, 1, 6047, 1), // Werewolf new Group(6148, 1, 6148, 1, 6148, 1, 6148, 1, 6148, 1), // Wake Doctor new Group(6117, 1, 6117, 1, 6117, 1, 6117, 1, 6117, 1), // Clown @@ -78,7 +76,8 @@ internal class FunEquipSet new Group(0000, 0, 0137, 2, 0000, 0, 0000, 0, 0000, 0), // Wailing Spirit new Group(0232, 1, 0232, 1, 0279, 1, 0232, 1, 0232, 1), // Eerie Attire new Group(0232, 1, 6036, 1, 0279, 1, 0232, 1, 0232, 1), // Vampire - new Group(0505, 6, 0505, 6, 0505, 6, 0505, 6, 0505, 6) // Manusya Casting + new Group(0505, 6, 0505, 6, 0505, 6, 0505, 6, 0505, 6), // Manusya Casting + new Group(6147, 1, 6147, 1, 6147, 1, 6147, 1, 6147, 1) // Tonberry ); public static readonly FunEquipSet AprilFirst = new @@ -95,7 +94,13 @@ internal class FunEquipSet new Group(0159, 1, 0000, 0, 0000, 0, 0000, 0, 0000, 0), // Slime Crown new Group(6117, 1, 6117, 1, 6117, 1, 6117, 1, 6117, 1), // Clown new Group(6169, 3, 6169, 3, 0279, 1, 6169, 3, 6169, 3), // Chocobo Pajama - new Group(6169, 2, 6169, 2, 0279, 2, 6169, 2, 6169, 2) // Cactuar Pajama + new Group(6169, 2, 6169, 2, 0279, 2, 6169, 2, 6169, 2), // Cactuar Pajama + new Group(6023, 1, 6023, 1, 0000, 0, 0000, 0, 0000, 0), // Swine + new Group(5040, 1, 0000, 0, 0000, 0, 0000, 0, 0000, 0), // Namazu only + new Group(5040, 1, 6023, 1, 0000, 0, 0000, 0, 0000, 0), // Namazu lean + new Group(5040, 1, 6023, 1, 0000, 0, 0000, 0, 0000, 0), // Namazu chonk + new Group(6182, 1, 6182, 1, 0000, 0, 0000, 0, 0000, 0), // Imp + new Group(6147, 1, 6147, 1, 6147, 1, 6147, 1, 6147, 1) // Tonberry ); private FunEquipSet(params Group[] groups) diff --git a/Glamourer/State/FunModule.cs b/Glamourer/State/FunModule.cs index 06ed9ef..6abb03a 100644 --- a/Glamourer/State/FunModule.cs +++ b/Glamourer/State/FunModule.cs @@ -1,18 +1,17 @@ -using System; -using System.Linq; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Interface.Internal.Notifications; -using Glamourer.Customization; +using Dalamud.Interface.ImGuiNotification; +using FFXIVClientStructs.FFXIV.Client.Game.Object; using Glamourer.Designs; +using Glamourer.GameData; using Glamourer.Gui; -using Glamourer.Interop; -using Glamourer.Interop.Structs; using Glamourer.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; +using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; -using CustomizeIndex = Glamourer.Customization.CustomizeIndex; +using CustomizeIndex = Penumbra.GameData.Enums.CustomizeIndex; namespace Glamourer.State; @@ -26,18 +25,19 @@ public unsafe class FunModule : IDisposable AprilFirst, } - private readonly WorldSets _worldSets = new(); - private readonly ItemManager _items; - private readonly CustomizationService _customizations; - private readonly Configuration _config; - private readonly CodeService _codes; - private readonly Random _rng; - private readonly GenericPopupWindow _popupWindow; - private readonly StateManager _stateManager; - private readonly DesignConverter _designConverter; - private readonly DesignManager _designManager; - private readonly ObjectManager _objects; - private readonly StainId[] _stains; + private readonly WorldSets _worldSets = new(); + private readonly ItemManager _items; + private readonly CustomizeService _customizations; + private readonly Configuration _config; + private readonly CodeService _codes; + private readonly Random _rng; + private readonly GenericPopupWindow _popupWindow; + private readonly StateManager _stateManager; + private readonly DesignConverter _designConverter; + private readonly DesignManager _designManager; + private readonly ActorObjectManager _objects; + private readonly NpcCustomizeSet _npcs; + private readonly StainId[] _stains; public FestivalType CurrentFestival { get; private set; } = FestivalType.None; private FunEquipSet? _festivalSet; @@ -66,11 +66,11 @@ public unsafe class FunModule : IDisposable } internal void ResetFestival() - => OnDayChange(DateTime.UtcNow.Day, DateTime.UtcNow.Month, DateTime.UtcNow.Year); + => OnDayChange(DateTime.Now.Day, DateTime.Now.Month, DateTime.Now.Year); - public FunModule(CodeService codes, CustomizationService customizations, ItemManager items, Configuration config, - GenericPopupWindow popupWindow, StateManager stateManager, ObjectManager objects, DesignConverter designConverter, - DesignManager designManager) + public FunModule(CodeService codes, CustomizeService customizations, ItemManager items, Configuration config, + GenericPopupWindow popupWindow, StateManager stateManager, ActorObjectManager objects, DesignConverter designConverter, + DesignManager designManager, NpcCustomizeSet npcs) { _codes = codes; _customizations = customizations; @@ -81,6 +81,7 @@ public unsafe class FunModule : IDisposable _objects = objects; _designConverter = designConverter; _designManager = designManager; + _npcs = npcs; _rng = new Random(); _stains = _items.Stains.Keys.Prepend((StainId)0).ToArray(); ResetFestival(); @@ -90,148 +91,366 @@ public unsafe class FunModule : IDisposable public void Dispose() => DayChangeTracker.DayChanged -= OnDayChange; - public void ApplyFun(Actor actor, ref CharacterArmor armor, EquipSlot slot) + private bool IsInFestival + => _config.DisableFestivals == 0 && _festivalSet != null; + + public void ApplyFunToSlot(Actor actor, ref CharacterArmor armor, EquipSlot slot) { - if (!actor.IsCharacter || actor.AsObject->ObjectKind is not (byte)ObjectKind.Player) + if (!ValidFunTarget(actor)) return; - if (actor.AsCharacter->CharacterData.ModelCharaId != 0) - return; - - if (_config.DisableFestivals == 0 && _festivalSet != null - || _codes.EnabledWorld && actor.Index != 0) + if (IsInFestival) { - armor = actor.Model.GetArmor(slot); + KeepOldArmor(actor, slot, ref armor); + return; } - else + + if (actor.Index < ObjectIndex.CutsceneStart) + switch (_codes.Masked(CodeService.FullCodes)) + { + case CodeService.CodeFlag.Face when actor.Index != 0: + case CodeService.CodeFlag.Smiles: + case CodeService.CodeFlag.Manderville: + KeepOldArmor(actor, slot, ref armor); + return; + } + + if (_codes.Enabled(CodeService.CodeFlag.Crown) + && actor.OnlineStatus is OnlineStatus.PvEMentor or OnlineStatus.PvPMentor or OnlineStatus.TradeMentor + && slot.IsEquipment()) { - ApplyEmperor(new Span(ref armor), slot); - ApplyClown(new Span(ref armor)); + armor = new CharacterArmor(6117, 1, StainIds.None); + return; + } + + switch (_codes.Masked(CodeService.GearCodes)) + { + case CodeService.CodeFlag.Emperor: SetRandomItem(slot, ref armor); break; + case CodeService.CodeFlag.Elephants: + case CodeService.CodeFlag.Dolphins: + case CodeService.CodeFlag.World when actor.Index != 0: + KeepOldArmor(actor, slot, ref armor); + break; + } + + switch (_codes.Masked(CodeService.DyeCodes)) + { + case CodeService.CodeFlag.Clown: SetRandomDye(ref armor); break; } } - public void ApplyFun(Actor actor, Span armor, ref Customize customize) + private sealed class PrioritizedList : List<(T Item, int Priority)> { - if (!actor.IsCharacter || actor.AsObject->ObjectKind is not (byte)ObjectKind.Player) - return; + private int _cumulative; - if (actor.AsCharacter->CharacterData.ModelCharaId != 0) - return; + public PrioritizedList(params (T Item, int Priority)[] list) + { + if (list.Length == 0) + return; - if (_config.DisableFestivals == 0 && _festivalSet != null) - { - _festivalSet.Apply(_stains, _rng, armor); - } - else if (_codes.EnabledWorld && actor.Index != 0) - { - _worldSets.Apply(actor, _rng, armor); - } - else - { - ApplyEmperor(armor); - ApplyClown(armor); + AddRange(list.Where(p => p.Priority > 0).OrderByDescending(p => p.Priority).Select(p => (p.Item, _cumulative += p.Priority))); } - ApplyOops(ref customize); - Apply63(ref customize); - ApplyIndividual(ref customize); - ApplySizing(actor, ref customize); + public T GetRandom(Random rng) + { + var val = rng.Next(0, _cumulative); + foreach (var (item, priority) in this) + { + if (val < priority) + return item; + } + + // Should never happen. + return this[^1].Item1; + } } - public void ApplyFun(Actor actor, ref CharacterWeapon weapon, EquipSlot slot) + private static readonly PrioritizedList MandervilleMale = new + ( + //(0000000, 400), // Nothing + (1008264, 30), // Hildi + (1008731, 10), // Hildi, slightly damaged + (1011668, 3), // Zombi + (1016617, 5), // Hildi, heavily damaged + (1042518, 1), // Hildi of Light + (1006339, 2), // Godbert, naked + (1008734, 10), // Godbert, shorts + (1015921, 5), // Godbert, ripped + (1041606, 5), // Godbert, only shorts + (1041605, 5), // Godbert, summer + (1024501, 30), // Godbert, fully clothed + (1045184, 3), // Godbrand + (1044749, 1) // Brandihild + ); + + private static readonly PrioritizedList MandervilleFemale = new + ( + //(0000000, 400), // Nothing + (1025669, 5), // Hildi, Geisha + (1025670, 2), // Hildi, makeup, black + (1042477, 2), // Hildi, makeup, white + (1016798, 20), // Julyan, Winter + (1011707, 30), // Julyan + (1005714, 20), // Nashu + (1025668, 5), // Nashu, Kimono + (1025674, 5), // Nashu, fancy + (1042486, 30), // Nashu, inspector + (1017263, 3), // Gigi + (1017263, 1) // Gigi, buff + ); + + private static readonly PrioritizedList Smile = new + ( + (1046504, 75), // Normal + (1046501, 20), // Hat + (1050613, 4), // Armor + (1047625, 1) // Elephant + ); + + private static readonly PrioritizedList FaceMale = new + ( + //(0000000, 700), // Nothing + (1016136, 35), // Gerolt + (1032667, 2), // Gerolt, Suit + (1030519, 35), // Grenoldt + (1030519, 20), // Grenoldt, Short + (1046262, 2), // Grenoldt, Suit + (1048084, 15) // Genolt + ); + + private static readonly PrioritizedList FaceFemale = new + ( + //(0000000, 400), // Nothing + (1013713, 10), // Rowena, Togi + (1018496, 30), // Rowena, Poncho + (1032668, 2), // Rowena, Gown + (1042857, 10), // Rowena, Hannish + (1046255, 10), // Mowen, Miner + (1046263, 2), // Mowen, Gown + (1027544, 30), // Mowen, Bustle + (1049088, 15) // Rhodina + ); + + private bool ApplyFullCode(Actor actor, Span armor, ref CustomizeArray customize) { - if (!actor.IsCharacter || actor.AsObject->ObjectKind is not (byte)ObjectKind.Player) + if (actor.Index >= ObjectIndex.CutsceneStart) + return false; + + var id = _codes.Masked(CodeService.FullCodes) switch + { + CodeService.CodeFlag.Face when customize.Gender is Gender.Female && actor.Index != 0 => FaceFemale.GetRandom(_rng), + CodeService.CodeFlag.Face when actor.Index != 0 => FaceMale.GetRandom(_rng), + CodeService.CodeFlag.Smiles => Smile.GetRandom(_rng), + CodeService.CodeFlag.Manderville when customize.Gender is Gender.Female => MandervilleFemale.GetRandom(_rng), + CodeService.CodeFlag.Manderville => MandervilleMale.GetRandom(_rng), + _ => (NpcId)0, + }; + + if (id.Id == 0 || !_npcs.FindFirst(n => n.Id == id, out var npc)) + return false; + + customize = npc.Customize; + var idx = 0; + foreach (ref var a in armor) + a = npc.Equip[idx++]; + return true; + } + + public void ApplyFunOnLoad(Actor actor, Span armor, ref CustomizeArray customize) + { + if (!ValidFunTarget(actor)) return; - if (actor.AsCharacter->CharacterData.ModelCharaId != 0) + if (ApplyFullCode(actor, armor, ref customize)) return; - if (_codes.EnabledWorld) + // First set the race, if any. + SetRace(ref customize); + // Now apply the gender. + SetGender(ref customize); + // Randomize customizations inside the race and gender combo. + RandomizeCustomize(ref customize); + // Finally, apply forced sizes. + SetSize(actor, ref customize); + + // Apply the festival gear with priority over all gear codes. + if (IsInFestival) + { + _festivalSet!.Apply(_stains, _rng, armor); + return; + } + + if (_codes.Enabled(CodeService.CodeFlag.Crown) + && actor.OnlineStatus is OnlineStatus.Mentor or OnlineStatus.PvEMentor or OnlineStatus.PvPMentor or OnlineStatus.TradeMentor) + { + SetCrown(armor); + return; + } + + switch (_codes.Masked(CodeService.GearCodes)) + { + case CodeService.CodeFlag.Emperor: + foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex()) + SetRandomItem(slot, ref armor[idx]); + break; + case CodeService.CodeFlag.Elephants: + var stainId = ElephantStains[_rng.Next(0, ElephantStains.Length)]; + SetElephant(EquipSlot.Body, ref armor[1], stainId); + SetElephant(EquipSlot.Head, ref armor[0], stainId); + break; + case CodeService.CodeFlag.Dolphins: + SetDolphin(EquipSlot.Body, ref armor[1]); + SetDolphin(EquipSlot.Head, ref armor[0]); + break; + case CodeService.CodeFlag.World when actor.Index != 0: _worldSets.Apply(actor, _rng, armor); break; + } + + switch (_codes.Masked(CodeService.DyeCodes)) + { + case CodeService.CodeFlag.Clown: + foreach (ref var piece in armor) + SetRandomDye(ref piece); + break; + } + } + + public void ApplyFunToWeapon(Actor actor, ref CharacterWeapon weapon, EquipSlot slot) + { + if (!ValidFunTarget(actor)) + return; + + if (_codes.Enabled(CodeService.CodeFlag.World) && actor.Index != 0) _worldSets.Apply(actor, _rng, ref weapon, slot); } - public void ApplyClown(Span armors) - { - if (!_codes.EnabledClown) - return; + private static bool ValidFunTarget(Actor actor) + => actor.IsCharacter + && actor.AsObject->ObjectKind is ObjectKind.Pc + && !actor.IsTransformed + && actor.AsCharacter->ModelContainer.ModelCharaId == 0; - foreach (ref var armor in armors) - { - var stainIdx = _rng.Next(0, _stains.Length - 1); - armor.Stain = _stains[stainIdx]; - } + private static void KeepOldArmor(Actor actor, EquipSlot slot, ref CharacterArmor armor) + => armor = actor.Model.Valid ? actor.Model.GetArmor(slot) : armor; + + private void SetRandomDye(ref CharacterArmor armor) + { + var stainIdx = _rng.Next(0, _stains.Length); + armor.Stains = _stains[stainIdx]; } - public void ApplyEmperor(Span armors, EquipSlot slot = EquipSlot.Unknown) + private void SetRandomItem(EquipSlot slot, ref CharacterArmor armor) { - if (!_codes.EnabledEmperor) - return; - - void SetItem(EquipSlot slot2, ref CharacterArmor armor) - { - var list = _items.ItemService.AwaitedService[slot2.ToEquipType()]; - var rng = _rng.Next(0, list.Count - 1); - var item = list[rng]; - armor.Set = item.ModelId; - armor.Variant = item.Variant; - } - - if (armors.Length == 1) - SetItem(slot, ref armors[0]); - else - for (var i = 0u; i < armors.Length; ++i) - SetItem(i.ToEquipSlot(), ref armors[(int)i]); + var list = _items.ItemData.ByType[slot.ToEquipType()]; + var rng = _rng.Next(0, list.Count); + var item = list[rng]; + armor.Set = item.PrimaryId; + armor.Variant = item.Variant; } - public void ApplyOops(ref Customize customize) + private static ReadOnlySpan ElephantStains + => + [ + 87, 87, 87, 87, 87, // Cherry Pink + 83, 83, 83, // Colibri Pink + 80, // Iris Purple + 85, // Regal Purple + 103, // Pastel Pink + 82, 82, 82, // Lotus Pink + 7, // Rose Pink + ]; + + private static IReadOnlyList DolphinBodies + => + [ + new(6089, 1, new StainIds(4)), // Toad + new(6089, 1, new StainIds(4)), // Toad + new(6089, 1, new StainIds(4)), // Toad + new(6023, 1, new StainIds(4)), // Swine + new(6023, 1, new StainIds(4)), // Swine + new(6023, 1, new StainIds(4)), // Swine + new(6133, 1, new StainIds(4)), // Gaja + new(6182, 1, new StainIds(3)), // Imp + new(6182, 1, new StainIds(3)), // Imp + new(6182, 1, new StainIds(4)), // Imp + new(6182, 1, new StainIds(4)), // Imp + ]; + + private void SetDolphin(EquipSlot slot, ref CharacterArmor armor) { - if (_codes.EnabledOops == Race.Unknown) + armor = slot switch + { + EquipSlot.Body => DolphinBodies[_rng.Next(0, DolphinBodies.Count)], + EquipSlot.Head => new CharacterArmor(5040, 1, StainIds.None), + _ => armor, + }; + } + + private void SetElephant(EquipSlot slot, ref CharacterArmor armor, StainId stainId) + { + armor = slot switch + { + EquipSlot.Body => new CharacterArmor(6133, 1, stainId), + EquipSlot.Head => new CharacterArmor(6133, 1, stainId), + _ => armor, + }; + } + + private static void SetCrown(Span armor) + { + var clown = new CharacterArmor(6117, 1, StainIds.None); + armor[0] = clown; + armor[1] = clown; + armor[2] = clown; + armor[3] = clown; + armor[4] = clown; + } + + private void SetRace(ref CustomizeArray customize) + { + var race = _codes.GetRace(); + if (race == Race.Unknown) return; - var targetClan = (SubRace)((int)_codes.EnabledOops * 2 - (int)customize.Clan % 2); - // TODO Female Hrothgar - if (_codes.EnabledOops is Race.Hrothgar && customize.Gender is Gender.Female) - targetClan = targetClan is SubRace.Lost ? SubRace.Seawolf : SubRace.Hellsguard; + var targetClan = (SubRace)((int)race * 2 - (int)customize.Clan % 2); _customizations.ChangeClan(ref customize, targetClan); } - public void ApplyIndividual(ref Customize customize) + private void SetGender(ref CustomizeArray customize) { - if (!_codes.EnabledIndividual) - return; - - var set = _customizations.AwaitedService.GetList(customize.Clan, customize.Gender); - foreach (var index in Enum.GetValues()) - { - if (index is CustomizeIndex.Face || !set.IsAvailable(index)) - continue; - - var valueIdx = _rng.Next(0, set.Count(index) - 1); - customize[index] = set.Data(index, valueIdx).Value; - } - } - - public void Apply63(ref Customize customize) - { - if (!_codes.Enabled63 || customize.Race is Race.Hrothgar) // TODO Female Hrothgar + if (!_codes.Enabled(CodeService.CodeFlag.SixtyThree)) return; _customizations.ChangeGender(ref customize, customize.Gender is Gender.Male ? Gender.Female : Gender.Male); } - public void ApplySizing(Actor actor, ref Customize customize) + private void RandomizeCustomize(ref CustomizeArray customize) { - if (_codes.EnabledSizing == CodeService.Sizing.None) + if (!_codes.Enabled(CodeService.CodeFlag.Individual)) return; - var size = _codes.EnabledSizing switch + var set = _customizations.Manager.GetSet(customize.Clan, customize.Gender); + foreach (var index in Enum.GetValues()) { - CodeService.Sizing.Dwarf when actor.Index == 0 => 0, - CodeService.Sizing.Dwarf when actor.Index != 0 => 100, - CodeService.Sizing.Giant when actor.Index == 0 => 100, - CodeService.Sizing.Giant when actor.Index != 0 => 0, - _ => 0, + if (index is CustomizeIndex.Face || !set.IsAvailable(index)) + continue; + + var valueIdx = _rng.Next(0, set.Count(index)); + customize[index] = set.Data(index, valueIdx).Value; + } + } + + private void SetSize(Actor actor, ref CustomizeArray customize) + { + var size = _codes.Masked(CodeService.SizeCodes) switch + { + CodeService.CodeFlag.Dwarf when actor.Index == 0 => (byte)0, + CodeService.CodeFlag.Dwarf => (byte)100, + CodeService.CodeFlag.Giant when actor.Index == 0 => (byte)100, + CodeService.CodeFlag.Giant => (byte)0, + _ => byte.MaxValue, }; + if (size == byte.MaxValue) + return; if (customize.Gender is Gender.Female) customize[CustomizeIndex.BustSize] = (CustomizeValue)size; @@ -252,8 +471,7 @@ public unsafe class FunModule : IDisposable try { var tmp = _designManager.CreateTemporary(); - tmp.DesignData = _stateManager.FromActor(actor, true, true); - tmp.FixCustomizeApplication(_customizations, CustomizeFlagExtensions.AllRelevant); + tmp.SetDesignData(_customizations, _stateManager.FromActor(actor, true, true)); var data = _designConverter.ShareBase64(tmp); ImGui.SetClipboardText(data); Glamourer.Messager.NotificationMessage($"Copied current actual design of {actor.Utf8Name} to clipboard.", NotificationType.Info, diff --git a/Glamourer/State/InternalStateEditor.cs b/Glamourer/State/InternalStateEditor.cs new file mode 100644 index 0000000..69051c2 --- /dev/null +++ b/Glamourer/State/InternalStateEditor.cs @@ -0,0 +1,284 @@ +using Dalamud.Plugin.Services; +using Glamourer.Designs; +using Glamourer.Events; +using Glamourer.GameData; +using Glamourer.Interop.Material; +using Glamourer.Services; +using Penumbra.GameData.DataContainers; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.State; + +public class InternalStateEditor( + CustomizeService customizations, + HumanModelList humans, + ItemManager items, + GPoseService gPose, + ICondition condition) +{ + /// Change the model id. If the actor is changed from a human to another human, customize and equipData are unused. + /// We currently only allow changing things to humans, not humans to monsters. + public bool ChangeModelId(ActorState state, uint modelId, in CustomizeArray customize, nint equipData, StateSource source, + out uint oldModelId, uint key = 0) + { + oldModelId = state.ModelData.ModelId; + + // TODO think about this. + if (modelId != 0) + return false; + + if (!state.CanUnlock(key)) + return false; + + var oldIsHuman = state.ModelData.IsHuman; + state.ModelData.IsHuman = humans.IsHuman(modelId); + if (state.ModelData.IsHuman) + { + if (oldModelId == modelId) + return true; + + state.ModelData.ModelId = modelId; + if (oldIsHuman) + return true; + + if (!state.AllowsRedraw(condition)) + return false; + + // Fix up everything else to make sure the result is a valid human. + state.ModelData.Customize = CustomizeArray.Default; + state.ModelData.SetDefaultEquipment(items); + state.ModelData.SetHatVisible(true); + state.ModelData.SetWeaponVisible(true); + state.ModelData.SetVisor(false); + state.Sources[MetaIndex.ModelId] = source; + state.Sources[MetaIndex.HatState] = source; + state.Sources[MetaIndex.WeaponState] = source; + state.Sources[MetaIndex.VisorState] = source; + foreach (var slot in EquipSlotExtensions.FullSlots) + { + state.Sources[slot, true] = source; + state.Sources[slot, false] = source; + } + + state.Sources[CustomizeIndex.Clan] = source; + state.Sources[CustomizeIndex.Gender] = source; + var set = customizations.Manager.GetSet(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender); + foreach (var index in Enum.GetValues().Where(set.IsAvailable)) + state.Sources[index] = source; + } + else + { + if (!state.AllowsRedraw(condition)) + return false; + + state.ModelData.LoadNonHuman(modelId, customize, equipData); + state.Sources[MetaIndex.ModelId] = source; + } + + return true; + } + + /// Change a customization value. + public bool ChangeCustomize(ActorState state, CustomizeIndex idx, CustomizeValue value, StateSource source, + out CustomizeValue old, uint key = 0) + { + old = state.ModelData.Customize[idx]; + if (!state.CanUnlock(key)) + return false; + + state.ModelData.Customize[idx] = value; + state.Sources[idx] = source; + return true; + } + + /// Change an entire customization array according to functions. + public bool ChangeHumanCustomize(ActorState state, in CustomizeArray customizeInput, CustomizeFlag applyWhich, + Func source, out CustomizeArray old, out CustomizeFlag changed, uint key = 0) + { + old = state.ModelData.Customize; + changed = 0; + if (!state.CanUnlock(key)) + return false; + + (var customize, var applied, changed) = customizations.Combine(state.ModelData.Customize, customizeInput, applyWhich, true); + if (changed == 0) + return false; + + state.ModelData.Customize = customize; + applied |= changed; + foreach (var type in Enum.GetValues()) + { + if (applied.HasFlag(type.ToFlag())) + state.Sources[type] = source(type); + } + + return true; + } + + /// Change an entire customization array according to functions. + public bool ChangeHumanCustomize(ActorState state, in CustomizeArray customizeInput, Func applyWhich, + Func source, out CustomizeArray old, out CustomizeFlag changed, uint key = 0) + { + var apply = Enum.GetValues().Where(applyWhich).Aggregate((CustomizeFlag)0, (current, type) => current | type.ToFlag()); + return ChangeHumanCustomize(state, customizeInput, apply, source, out old, out changed, key); + } + + /// Change a single piece of equipment without stain. + public bool ChangeItem(ActorState state, EquipSlot slot, EquipItem item, StateSource source, out EquipItem oldItem, uint key = 0) + { + oldItem = state.ModelData.Item(slot); + if (!state.CanUnlock(key)) + return false; + + // Can not change weapon type from expected type in state. + if (slot is EquipSlot.MainHand && item.Type != state.BaseData.MainhandType + || slot is EquipSlot.OffHand && item.Type != state.BaseData.OffhandType) + { + if (!gPose.InGPose) + return false; + + var old = oldItem; + gPose.AddActionOnLeave(() => + { + if (old.Type == state.BaseData.Item(slot).Type) + ChangeItem(state, slot, old, state.Sources[slot, false], out _, key); + }); + } + + state.ModelData.SetItem(slot, item); + state.Sources[slot, false] = source; + return true; + } + + /// Change a single bonus item. + public bool ChangeBonusItem(ActorState state, BonusItemFlag slot, EquipItem item, StateSource source, out EquipItem oldItem, uint key = 0) + { + oldItem = state.ModelData.BonusItem(slot); + if (!state.CanUnlock(key)) + return false; + + state.ModelData.SetBonusItem(slot, item); + state.Sources[slot] = source; + return true; + } + + /// Change a single piece of equipment including stain. + public bool ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainIds stains, StateSource source, out EquipItem oldItem, + out StainIds oldStains, uint key = 0) + { + oldItem = state.ModelData.Item(slot); + oldStains = state.ModelData.Stain(slot); + if (!state.CanUnlock(key)) + return false; + + // Can not change weapon type from expected type in state. + if (slot is EquipSlot.MainHand && item.Type != state.BaseData.MainhandType + || slot is EquipSlot.OffHand && item.Type != state.BaseData.OffhandType) + { + if (!gPose.InGPose) + return false; + + var old = oldItem; + var oldS = oldStains; + gPose.AddActionOnLeave(() => + { + if (old.Type == state.BaseData.Item(slot).Type) + ChangeEquip(state, slot, old, oldS, state.Sources[slot, false], out _, out _, key); + }); + } + + state.ModelData.SetItem(slot, item); + state.ModelData.SetStain(slot, stains); + state.Sources[slot, false] = source; + state.Sources[slot, true] = source; + return true; + } + + /// Change only the stain of an equipment piece. + public bool ChangeStains(ActorState state, EquipSlot slot, StainIds stains, StateSource source, out StainIds oldStains, uint key = 0) + { + oldStains = state.ModelData.Stain(slot); + if (!state.CanUnlock(key)) + return false; + + state.ModelData.SetStain(slot, stains); + state.Sources[slot, true] = source; + return true; + } + + /// Change the crest of an equipment piece. + public bool ChangeCrest(ActorState state, CrestFlag slot, bool crest, StateSource source, out bool oldCrest, uint key = 0) + { + oldCrest = state.ModelData.Crest(slot); + if (!state.CanUnlock(key)) + return false; + + state.ModelData.SetCrest(slot, crest); + state.Sources[slot] = source; + return true; + } + + /// Change the customize flags of a character. + public bool ChangeParameter(ActorState state, CustomizeParameterFlag flag, CustomizeParameterValue value, StateSource source, + out CustomizeParameterValue oldValue, uint key = 0) + { + oldValue = state.ModelData.Parameters[flag]; + if (!state.CanUnlock(key)) + return false; + + state.ModelData.Parameters.Set(flag, value); + state.Sources[flag] = source; + + return true; + } + + /// Change the value of a single material color table entry. + public bool ChangeMaterialValue(ActorState state, MaterialValueIndex index, in MaterialValueState newValue, StateSource source, + out ColorRow? oldValue, uint key = 0) + { + // We already have an existing value. + if (state.Materials.TryGetValue(index, out var old)) + { + oldValue = old.Model; + if (!state.CanUnlock(key)) + return false; + + // Remove if overwritten by a game value. + if (source is StateSource.Game) + { + state.Materials.RemoveValue(index); + return true; + } + + // Update if edited. + state.Materials.UpdateValue(index, newValue, out _); + return true; + } + + // We do not have an existing value. + oldValue = null; + // Do not do anything if locked or if the game value updates, because then we do not need to add an entry. + if (!state.CanUnlock(key) || source is StateSource.Game) + return false; + + // Only add an entry if it is different from the game value. + return state.Materials.TryAddValue(index, newValue); + } + + /// Reset the value of a single material color table entry. + public bool ResetMaterialValue(ActorState state, MaterialValueIndex index, uint key = 0) + => state.CanUnlock(key) && state.Materials.RemoveValue(index); + + public bool ChangeMetaState(ActorState state, MetaIndex index, bool value, StateSource source, out bool oldValue, + uint key = 0) + { + oldValue = state.ModelData.GetMeta(index); + if (!state.CanUnlock(key)) + return false; + + state.ModelData.SetMeta(index, value); + state.Sources[index] = source; + return true; + } +} diff --git a/Glamourer/State/JobChangeState.cs b/Glamourer/State/JobChangeState.cs new file mode 100644 index 0000000..d568375 --- /dev/null +++ b/Glamourer/State/JobChangeState.cs @@ -0,0 +1,37 @@ +using Glamourer.Designs.Links; +using OtterGui.Services; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.State; + +public sealed class JobChangeState : IService +{ + private readonly WeaponList _weaponList = new(); + + public ActorState? State { get; private set; } + + public void Reset() + { + State = null; + _weaponList.Clear(); + } + + public bool HasState + => State != null; + + public ActorIdentifier Identifier + => State?.Identifier ?? ActorIdentifier.Invalid; + + public bool TryGetValue(FullEquipType slot, JobId jobId, bool gameStateAllowed, out (EquipItem, StateSource) data) + => _weaponList.TryGet(slot, jobId, gameStateAllowed, out data); + + public void Set(ActorState state, IEnumerable<(EquipItem, StateSource, JobFlag)> items) + { + Reset(); + foreach (var (item, source, flags) in items.Where(p => p.Item1.Valid)) + _weaponList.TryAdd(item.Type, item, source, flags); + State = state; + } +} diff --git a/Glamourer/State/StateApplier.cs b/Glamourer/State/StateApplier.cs index 88a97e3..9800445 100644 --- a/Glamourer/State/StateApplier.cs +++ b/Glamourer/State/StateApplier.cs @@ -1,14 +1,13 @@ -using System.Linq; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Glamourer.Customization; -using Glamourer.Events; +using Glamourer.Designs; +using Glamourer.GameData; using Glamourer.Interop; +using Glamourer.Interop.Material; using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; using Penumbra.Api.Enums; -using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.State; @@ -17,30 +16,18 @@ namespace Glamourer.State; /// This class applies changes made to state to actual objects in the game. /// It handles applying those changes as well as redrawing the actor if necessary. /// -public class StateApplier +public class StateApplier( + UpdateSlotService _updateSlot, + VisorService _visor, + WeaponService _weapon, + ChangeCustomizeService _changeCustomize, + ItemManager _items, + PenumbraService _penumbra, + MetaService _metaService, + ActorObjectManager _objects, + CrestService _crests, + DirectXService _directX) { - private readonly PenumbraService _penumbra; - private readonly UpdateSlotService _updateSlot; - private readonly VisorService _visor; - private readonly WeaponService _weapon; - private readonly MetaService _metaService; - private readonly ChangeCustomizeService _changeCustomize; - private readonly ItemManager _items; - private readonly ObjectManager _objects; - - public StateApplier(UpdateSlotService updateSlot, VisorService visor, WeaponService weapon, ChangeCustomizeService changeCustomize, - ItemManager items, PenumbraService penumbra, MetaService metaService, ObjectManager objects) - { - _updateSlot = updateSlot; - _visor = visor; - _weapon = weapon; - _changeCustomize = changeCustomize; - _items = items; - _penumbra = penumbra; - _metaService = metaService; - _objects = objects; - } - /// Simply force a redraw regardless of conditions. public void ForceRedraw(ActorData data) { @@ -62,7 +49,7 @@ public class StateApplier /// Change the customization values of actors either by applying them via update or redrawing, /// this depends on whether the changes include changes to Race, Gender, Body Type or Face. /// - public unsafe void ChangeCustomize(ActorData data, in Customize customize, ActorState? state = null) + public unsafe void ChangeCustomize(ActorData data, in CustomizeArray customize, ActorState? _ = null) { foreach (var actor in data.Objects) { @@ -70,15 +57,16 @@ public class StateApplier if (!mdl.IsCharacterBase) continue; - var flags = Customize.Compare(mdl.GetCustomize(), customize); + var flags = CustomizeArray.Compare(mdl.GetCustomize(), customize); if (!flags.RequiresRedraw() || !mdl.IsHuman) { - _changeCustomize.UpdateCustomize(mdl, customize.Data); + _changeCustomize.UpdateCustomize(mdl, customize); } else if (data.Objects.Count > 1 && _objects.IsInGPose && !actor.IsGPoseOrCutscene) { - var mdlCustomize = (Customize*)&mdl.AsHuman->Customize; - mdlCustomize->Load(customize); + var mdlCustomize = (CustomizeArray*)&mdl.AsHuman->Customize; + *mdlCustomize = customize; + _penumbra.RedrawObject(actor, RedrawType.AfterGPose); } else { @@ -87,7 +75,7 @@ public class StateApplier } } - /// + /// public ActorData ChangeCustomize(ActorState state, bool apply) { var data = GetData(state); @@ -117,11 +105,11 @@ public class StateApplier { var customize = mdl.GetCustomize(); var (_, resolvedItem) = _items.ResolveRestrictedGear(armor, slot, customize.Race, customize.Gender); - _updateSlot.UpdateSlot(actor.Model, slot, resolvedItem); + _updateSlot.UpdateEquipSlot(actor.Model, slot, resolvedItem); } else { - _updateSlot.UpdateSlot(actor.Model, slot, armor); + _updateSlot.UpdateEquipSlot(actor.Model, slot, armor); } } } @@ -132,8 +120,34 @@ public class StateApplier // If the source is not IPC we do not want to apply restrictions. var data = GetData(state); if (apply) - ChangeArmor(data, slot, state.ModelData.Armor(slot), state[slot, false] is not StateChanged.Source.Ipc, - state.ModelData.IsHatVisible()); + ChangeArmor(data, slot, state.ModelData.Armor(slot), !state.Sources[slot, false].IsIpc(), state.ModelData.IsHatVisible()); + + return data; + } + + public void ChangeBonusItem(ActorData data, BonusItemFlag slot, PrimaryId id, Variant variant) + { + var item = new CharacterArmor(id, variant, StainIds.None); + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + { + var mdl = actor.Model; + if (!mdl.IsHuman) + continue; + + _updateSlot.UpdateBonusSlot(actor.Model, slot, item); + } + } + + /// + public ActorData ChangeBonusItem(ActorState state, BonusItemFlag slot, bool apply) + { + // If the source is not IPC we do not want to apply restrictions. + var data = GetData(state); + if (apply) + { + var item = state.ModelData.BonusItem(slot); + ChangeBonusItem(data, slot, item.PrimaryId, item.Variant); + } return data; } @@ -143,27 +157,27 @@ public class StateApplier /// Change the stain of a single piece of armor or weapon. /// If the offhand is empty, the stain will be fixed to 0 to prevent crashes. /// - public void ChangeStain(ActorData data, EquipSlot slot, StainId stain) + public void ChangeStain(ActorData data, EquipSlot slot, StainIds stains) { var idx = slot.ToIndex(); switch (idx) { case < 10: foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) - _updateSlot.UpdateStain(actor.Model, slot, stain); + _updateSlot.UpdateStain(actor.Model, slot, stains); break; case 10: foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) - _weapon.LoadStain(actor, EquipSlot.MainHand, stain); + _weapon.LoadStain(actor, EquipSlot.MainHand, stains); break; case 11: foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) - _weapon.LoadStain(actor, EquipSlot.OffHand, stain); + _weapon.LoadStain(actor, EquipSlot.OffHand, stains); break; } } - /// + /// public ActorData ChangeStain(ActorState state, EquipSlot slot, bool apply) { var data = GetData(state); @@ -175,15 +189,15 @@ public class StateApplier /// Apply a weapon to the appropriate slot. - public void ChangeWeapon(ActorData data, EquipSlot slot, EquipItem item, StainId stain) + public void ChangeWeapon(ActorData data, EquipSlot slot, EquipItem item, StainIds stains) { if (slot is EquipSlot.MainHand) - ChangeMainhand(data, item, stain); + ChangeMainhand(data, item, stains); else - ChangeOffhand(data, item, stain); + ChangeOffhand(data, item, stains); } - /// + /// public ActorData ChangeWeapon(ActorState state, EquipSlot slot, bool apply, bool onlyGPose) { var data = GetData(state); @@ -199,88 +213,213 @@ public class StateApplier /// /// Apply a weapon to the mainhand. If the weapon type has no associated offhand type, apply both. /// - public void ChangeMainhand(ActorData data, EquipItem weapon, StainId stain) + public void ChangeMainhand(ActorData data, EquipItem weapon, StainIds stains) { var slot = weapon.Type.ValidOffhand() == FullEquipType.Unknown ? EquipSlot.BothHand : EquipSlot.MainHand; foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) - _weapon.LoadWeapon(actor, slot, weapon.Weapon().With(stain)); + _weapon.LoadWeapon(actor, slot, weapon.Weapon().With(stains)); } /// Apply a weapon to the offhand. - public void ChangeOffhand(ActorData data, EquipItem weapon, StainId stain) + public void ChangeOffhand(ActorData data, EquipItem weapon, StainIds stains) { - stain = weapon.ModelId.Id == 0 ? 0 : stain; + stains = weapon.PrimaryId.Id == 0 ? StainIds.None : stains; foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) - _weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon().With(stain)); + _weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon().With(stains)); } - /// Change the visor state of actors only on the draw object. - public void ChangeVisor(ActorData data, bool value) + /// Change a meta state. + public void ChangeMetaState(ActorData data, MetaIndex index, bool value) { - foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) - _visor.SetVisorState(actor.Model, value); + switch (index) + { + case MetaIndex.Wetness: + { + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + actor.IsGPoseWet = value; + return; + } + case MetaIndex.HatState: + { + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + _metaService.SetHatState(actor, value); + return; + } + case MetaIndex.WeaponState: + { + // Only apply to the GPose character because otherwise we get some weird incompatibility when leaving GPose. + if (_objects.IsInGPose) + foreach (var actor in data.Objects.Where(a => a.IsGPoseOrCutscene)) + _metaService.SetWeaponState(actor, value); + else + foreach (var actor in data.Objects.Where(a => a.IsCharacter)) + _metaService.SetWeaponState(actor, value); + return; + } + case MetaIndex.VisorState: + { + foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) + _visor.SetVisorState(actor.Model, value); + return; + } + case MetaIndex.EarState: + foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) + { + var model = actor.Model; + model.VieraEarsVisible = value; + } + + return; + } } - /// - public ActorData ChangeVisor(ActorState state, bool apply) + /// + public ActorData ChangeMetaState(ActorState state, MetaIndex index, bool apply) { var data = GetData(state); if (apply) - ChangeVisor(data, state.ModelData.IsVisorToggled()); + ChangeMetaState(data, index, state.ModelData.GetMeta(index)); return data; } - /// Change the forced wetness state on actors. - public unsafe void ChangeWetness(ActorData data, bool value) + /// Change the crest state on actors. + public void ChangeCrests(ActorData data, CrestFlag flags) { foreach (var actor in data.Objects.Where(a => a.IsCharacter)) - actor.AsCharacter->IsGPoseWet = value; + _crests.UpdateCrests(actor, flags); } - /// - public ActorData ChangeWetness(ActorState state, bool apply) + /// + public ActorData ChangeCrests(ActorState state, bool apply) { var data = GetData(state); if (apply) - ChangeWetness(data, state.ModelData.IsWet()); + ChangeCrests(data, state.ModelData.CrestVisibility); return data; } - /// Change the hat-visibility state on actors. - public void ChangeHatState(ActorData data, bool value) + /// Change the customize parameters on models. Can change multiple at once. + public void ChangeParameters(ActorData data, CustomizeParameterFlag flags, in CustomizeParameterData values) { - foreach (var actor in data.Objects.Where(a => a.IsCharacter)) - _metaService.SetHatState(actor, value); + if (flags == 0) + return; + + foreach (var actor in data.Objects.Where(a => a is { IsCharacter: true, Model.IsHuman: true })) + actor.Model.ApplyParameterData(flags, values); } - /// - public ActorData ChangeHatState(ActorState state, bool apply) + /// + public ActorData ChangeParameters(ActorState state, CustomizeParameterFlag flags, bool apply) { var data = GetData(state); if (apply) - ChangeHatState(data, state.ModelData.IsHatVisible()); + ChangeParameters(data, flags, state.ModelData.Parameters); return data; } - /// Change the weapon-visibility state on actors. - public void ChangeWeaponState(ActorData data, bool value) + public unsafe void ChangeMaterialValue(ActorState state, ActorData data, MaterialValueIndex changedIndex, ColorRow? changedValue) { - foreach (var actor in data.Objects.Where(a => a.IsCharacter)) - _metaService.SetWeaponState(actor, value); + foreach (var actor in data.Objects.Where(a => a is { IsCharacter: true, Model.IsHuman: true })) + { + if (!changedIndex.TryGetTexture(actor, out var texture)) + continue; + + if (!PrepareColorSet.TryGetColorTable(actor, changedIndex, out var baseTable, out var mode)) + continue; + + foreach (var (index, value) in state.Materials.GetValues( + MaterialValueIndex.Min(changedIndex.DrawObject, changedIndex.SlotIndex, changedIndex.MaterialIndex), + MaterialValueIndex.Max(changedIndex.DrawObject, changedIndex.SlotIndex, changedIndex.MaterialIndex))) + { + if (index == changedIndex.Key) + changedValue?.Apply(ref baseTable[changedIndex.RowIndex], mode); + else + value.Model.Apply(ref baseTable[MaterialValueIndex.FromKey(index).RowIndex], mode); + } + + _directX.ReplaceColorTable(texture, baseTable); + } } - /// - public ActorData ChangeWeaponState(ActorState state, bool apply) + public ActorData ChangeMaterialValue(ActorState state, MaterialValueIndex index, bool apply) { var data = GetData(state); if (apply) - ChangeWeaponState(data, state.ModelData.IsWeaponVisible()); + ChangeMaterialValue(state, data, index, state.Materials.TryGetValue(index, out var v) ? v.Model : null); + return data; } - private ActorData GetData(ActorState state) + public unsafe void ChangeMaterialValues(ActorData data, in StateMaterialManager materials) { - _objects.Update(); - return _objects.TryGetValue(state.Identifier, out var data) ? data : ActorData.Invalid; + var groupedMaterialValues = materials.Values.Select(p => (MaterialValueIndex.FromKey(p.Key), p.Value)) + .GroupBy(p => (p.Item1.DrawObject, p.Item1.SlotIndex, p.Item1.MaterialIndex)); + + foreach (var group in groupedMaterialValues) + { + var values = group.ToList(); + var mainKey = values[0].Item1; + foreach (var actor in data.Objects.Where(a => a is { IsCharacter: true, Model.IsHuman: true })) + { + if (!mainKey.TryGetTexture(actor, out var texture)) + continue; + + if (!PrepareColorSet.TryGetColorTable(actor, mainKey, out var table, out var mode)) + continue; + + foreach (var (key, value) in values) + value.Model.Apply(ref table[key.RowIndex], mode); + + _directX.ReplaceColorTable(texture, table); + } + } } + + /// Apply the entire state of an actor to all relevant actors, either via immediate redraw or piecewise. + /// The state to apply. + /// Whether a redraw should be forced. + /// Whether a temporary lock should be applied for the redraw. + /// The actor data for the actors who got changed. + public ActorData ApplyAll(ActorState state, bool redraw, bool withLock) + { + var actors = ChangeMetaState(state, MetaIndex.Wetness, true); + if (redraw) + { + if (withLock && actors.Valid) + state.TempLock(); + ForceRedraw(actors); + } + else + { + ChangeCustomize(actors, state.ModelData.Customize); + foreach (var slot in EquipSlotExtensions.EqdpSlots) + ChangeArmor(actors, slot, state.ModelData.Armor(slot), !state.Sources[slot, false].IsIpc(), state.ModelData.IsHatVisible()); + foreach (var slot in BonusExtensions.AllFlags) + { + var item = state.ModelData.BonusItem(slot); + ChangeBonusItem(actors, slot, item.PrimaryId, item.Variant); + } + + var mainhandActors = state.ModelData.MainhandType != state.BaseData.MainhandType ? actors.OnlyGPose() : actors; + ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand)); + var offhandActors = state.ModelData.OffhandType != state.BaseData.OffhandType ? actors.OnlyGPose() : actors; + ChangeOffhand(offhandActors, state.ModelData.Item(EquipSlot.OffHand), state.ModelData.Stain(EquipSlot.OffHand)); + + if (state.ModelData.IsHuman) + { + ChangeMetaState(actors, MetaIndex.HatState, state.ModelData.IsHatVisible()); + ChangeMetaState(actors, MetaIndex.WeaponState, state.ModelData.IsWeaponVisible()); + ChangeMetaState(actors, MetaIndex.VisorState, state.ModelData.IsVisorToggled()); + ChangeMetaState(actors, MetaIndex.EarState, state.ModelData.AreEarsVisible()); + ChangeCrests(actors, state.ModelData.CrestVisibility); + ChangeParameters(actors, state.OnlyChangedParameters(), state.ModelData.Parameters); + ChangeMaterialValues(actors, state.Materials); + } + } + + return actors; + } + + public ActorData GetData(ActorState state) + => _objects.TryGetValue(state.Identifier, out var data) ? data : ActorData.Invalid; } diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index 4bd39b4..986bdc2 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -1,220 +1,480 @@ -using System; -using System.Linq; -using Dalamud.Plugin.Services; -using Glamourer.Customization; +using Glamourer.Api.Enums; +using Glamourer.Designs; +using Glamourer.Designs.History; +using Glamourer.Designs.Links; using Glamourer.Events; +using Glamourer.GameData; +using Glamourer.Interop.Material; +using Glamourer.Interop.Penumbra; +using Glamourer.Interop.Structs; using Glamourer.Services; -using Penumbra.GameData.Data; using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.State; -public class StateEditor +public class StateEditor( + InternalStateEditor editor, + StateApplier applier, + StateChanged stateChanged, + StateFinalized stateFinalized, + JobChangeState jobChange, + Configuration config, + ItemManager items, + DesignMerger merger, + ModSettingApplier modApplier, + GPoseService gPose) : IDesignEditor { - private readonly ItemManager _items; - private readonly CustomizationService _customizations; - private readonly HumanModelList _humans; - private readonly GPoseService _gPose; - private readonly ICondition _condition; + protected readonly InternalStateEditor Editor = editor; + protected readonly StateApplier Applier = applier; + protected readonly StateChanged StateChanged = stateChanged; + protected readonly StateFinalized StateFinalized = stateFinalized; + protected readonly Configuration Config = config; + protected readonly ItemManager Items = items; - public StateEditor(CustomizationService customizations, HumanModelList humans, ItemManager items, GPoseService gPose, ICondition condition) + /// Turn an actor to. + public void ChangeModelId(ActorState state, uint modelId, CustomizeArray customize, nint equipData, StateSource source, + uint key = 0) { - _customizations = customizations; - _humans = humans; - _items = items; - _gPose = gPose; - _condition = condition; + if (!Editor.ChangeModelId(state, modelId, customize, equipData, source, out var old, key)) + return; + + var actors = Applier.ForceRedraw(state, source.RequiresChange()); + Glamourer.Log.Verbose( + $"Set model id in state {state.Identifier.Incognito(null)} from {old} to {modelId}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Model, source, state, actors, null); + StateFinalized.Invoke(StateFinalizationType.ModelChange, actors); } - /// Change the model id. If the actor is changed from a human to another human, customize and equipData are unused. - /// We currently only allow changing things to humans, not humans to monsters. - public bool ChangeModelId(ActorState state, uint modelId, in Customize customize, nint equipData, StateChanged.Source source, - out uint oldModelId, uint key = 0) + /// + public void ChangeCustomize(object data, CustomizeIndex idx, CustomizeValue value, ApplySettings settings) { - oldModelId = state.ModelData.ModelId; + var state = (ActorState)data; + if (!Editor.ChangeCustomize(state, idx, value, settings.Source, out var old, settings.Key)) + return; - // TODO think about this. - if (modelId != 0) - return false; + var actors = Applier.ChangeCustomize(state, settings.Source.RequiresChange()); + Glamourer.Log.Verbose( + $"Set {idx.ToDefaultName()} customizations in state {state.Identifier.Incognito(null)} from {old.Value} to {value.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Customize, settings.Source, state, actors, new CustomizeTransaction(idx, old, value)); + } - if (!state.CanUnlock(key)) - return false; + /// + public void ChangeEntireCustomize(object data, in CustomizeArray customizeInput, CustomizeFlag apply, ApplySettings settings) + { + var state = (ActorState)data; + if (!Editor.ChangeHumanCustomize(state, customizeInput, apply, _ => settings.Source, out var old, out var applied, settings.Key)) + return; - var oldIsHuman = state.ModelData.IsHuman; - state.ModelData.IsHuman = _humans.IsHuman(modelId); - if (state.ModelData.IsHuman) + var actors = Applier.ChangeCustomize(state, settings.Source.RequiresChange()); + Glamourer.Log.Verbose( + $"Set {applied} customizations in state {state.Identifier.Incognito(null)} from {old} to {customizeInput}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.EntireCustomize, settings.Source, state, actors, + new EntireCustomizeTransaction(applied, old, customizeInput)); + } + + /// + public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings settings = default) + { + var state = (ActorState)data; + if (!Editor.ChangeItem(state, slot, item, settings.Source, out var old, settings.Key)) + return; + + var type = slot.ToIndex() < 10 ? StateChangeType.Equip : StateChangeType.Weapon; + var actors = type is StateChangeType.Equip + ? Applier.ChangeArmor(state, slot, settings.Source.RequiresChange()) + : Applier.ChangeWeapon(state, slot, settings.Source.RequiresChange(), + item.Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType)); + + if (slot is EquipSlot.MainHand) + ApplyMainhandPeriphery(state, item, null, settings); + + Glamourer.Log.Verbose( + $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}). [Affecting {actors.ToLazyString("nothing")}.]"); + + if (type is StateChangeType.Equip) { - if (oldModelId == modelId) - return true; - - state.ModelData.ModelId = modelId; - if (oldIsHuman) - return true; - - if (!state.AllowsRedraw(_condition)) - return false; - - // Fix up everything else to make sure the result is a valid human. - state.ModelData.Customize = Customize.Default; - state.ModelData.SetDefaultEquipment(_items); - state.ModelData.SetHatVisible(true); - state.ModelData.SetWeaponVisible(true); - state.ModelData.SetVisor(false); - state[ActorState.MetaIndex.ModelId] = source; - state[ActorState.MetaIndex.HatState] = source; - state[ActorState.MetaIndex.WeaponState] = source; - state[ActorState.MetaIndex.VisorState] = source; - foreach (var slot in EquipSlotExtensions.FullSlots) - { - state[slot, true] = source; - state[slot, false] = source; - } - - state[CustomizeIndex.Clan] = source; - state[CustomizeIndex.Gender] = source; - var set = _customizations.AwaitedService.GetList(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender); - foreach (var index in Enum.GetValues().Where(set.IsAvailable)) - state[index] = source; + StateChanged.Invoke(type, settings.Source, state, actors, new EquipTransaction(slot, old, item)); + } + else if (slot is EquipSlot.MainHand) + { + var oldOff = state.ModelData.Item(EquipSlot.OffHand); + var oldGauntlets = state.ModelData.Item(EquipSlot.Hands); + StateChanged.Invoke(type, settings.Source, state, actors, + new WeaponTransaction(old, oldOff, oldGauntlets, item, oldOff, oldGauntlets)); } else { - if (!state.AllowsRedraw(_condition)) - return false; + var oldMain = state.ModelData.Item(EquipSlot.MainHand); + var oldGauntlets = state.ModelData.Item(EquipSlot.Hands); + StateChanged.Invoke(type, settings.Source, state, actors, + new WeaponTransaction(oldMain, old, oldGauntlets, oldMain, item, oldGauntlets)); + } + } - state.ModelData.LoadNonHuman(modelId, customize, equipData); - state[ActorState.MetaIndex.ModelId] = source; + public void ChangeBonusItem(object data, BonusItemFlag slot, EquipItem item, ApplySettings settings = default) + { + var state = (ActorState)data; + if (!Editor.ChangeBonusItem(state, slot, item, settings.Source, out var old, settings.Key)) + return; + + var actors = Applier.ChangeBonusItem(state, slot, settings.Source.RequiresChange()); + Glamourer.Log.Verbose( + $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.BonusItem, settings.Source, state, actors, new BonusItemTransaction(slot, old, item)); + } + + /// + public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainIds? stains, ApplySettings settings) + { + switch (item.HasValue, stains.HasValue) + { + case (false, false): return; + case (true, false): + ChangeItem(data, slot, item!.Value, settings); + return; + case (false, true): + ChangeStains(data, slot, stains!.Value, settings); + return; } - return true; - } + var state = (ActorState)data; + if (!Editor.ChangeEquip(state, slot, item ?? state.ModelData.Item(slot), stains ?? state.ModelData.Stain(slot), settings.Source, + out var old, out var oldStains, settings.Key)) + return; - /// Change a customization value. - public bool ChangeCustomize(ActorState state, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source, - out CustomizeValue old, uint key = 0) - { - old = state.ModelData.Customize[idx]; - if (!state.CanUnlock(key)) - return false; + var type = slot.ToIndex() < 10 ? StateChangeType.Equip : StateChangeType.Weapon; + var actors = type is StateChangeType.Equip + ? Applier.ChangeArmor(state, slot, settings.Source.RequiresChange()) + : Applier.ChangeWeapon(state, slot, settings.Source.RequiresChange(), + item!.Value.Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType)); - state.ModelData.Customize[idx] = value; - state[idx] = source; - return true; - } + if (slot is EquipSlot.MainHand) + ApplyMainhandPeriphery(state, item, stains, settings); - /// Change an entire customization array according to flags. - public bool ChangeHumanCustomize(ActorState state, in Customize customizeInput, CustomizeFlag applyWhich, StateChanged.Source source, - out Customize old, out CustomizeFlag changed, uint key = 0) - { - old = state.ModelData.Customize; - changed = 0; - if (!state.CanUnlock(key)) - return false; - - (var customize, var applied, changed) = _customizations.Combine(state.ModelData.Customize, customizeInput, applyWhich, true); - if (changed == 0) - return false; - - state.ModelData.Customize = customize; - applied |= changed; - foreach (var type in Enum.GetValues()) + Glamourer.Log.Verbose( + $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item!.Value.Name} ({item.Value.ItemId}) and its stain from {oldStains} to {stains!.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); + if (type is StateChangeType.Equip) { - if (applied.HasFlag(type.ToFlag())) - state[type] = source; + StateChanged.Invoke(type, settings.Source, state, actors, new EquipTransaction(slot, old, item!.Value)); + } + else if (slot is EquipSlot.MainHand) + { + var oldOff = state.ModelData.Item(EquipSlot.OffHand); + var oldGauntlets = state.ModelData.Item(EquipSlot.Hands); + StateChanged.Invoke(type, settings.Source, state, actors, + new WeaponTransaction(old, oldOff, oldGauntlets, item!.Value, oldOff, oldGauntlets)); + } + else + { + var oldMain = state.ModelData.Item(EquipSlot.MainHand); + var oldGauntlets = state.ModelData.Item(EquipSlot.Hands); + StateChanged.Invoke(type, settings.Source, state, actors, + new WeaponTransaction(oldMain, old, oldGauntlets, oldMain, item!.Value, oldGauntlets)); } - return true; + StateChanged.Invoke(StateChangeType.Stains, settings.Source, state, actors, new StainTransaction(slot, oldStains, stains!.Value)); } - /// Change a single piece of equipment without stain. - public bool ChangeItem(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source, out EquipItem oldItem, uint key = 0) + /// + public void ChangeStains(object data, EquipSlot slot, StainIds stains, ApplySettings settings) { - oldItem = state.ModelData.Item(slot); - if (!state.CanUnlock(key)) - return false; + var state = (ActorState)data; + if (!Editor.ChangeStains(state, slot, stains, settings.Source, out var old, settings.Key)) + return; - // Can not change weapon type from expected type in state. - if (slot is EquipSlot.MainHand && item.Type != state.BaseData.MainhandType - || slot is EquipSlot.OffHand && item.Type != state.BaseData.OffhandType) + var actors = Applier.ChangeStain(state, slot, settings.Source.RequiresChange()); + Glamourer.Log.Verbose( + $"Set {slot.ToName()} stain in state {state.Identifier.Incognito(null)} from {old} to {stains}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Stains, settings.Source, state, actors, new StainTransaction(slot, old, stains)); + } + + /// + public void ChangeCrest(object data, CrestFlag slot, bool crest, ApplySettings settings) + { + var state = (ActorState)data; + if (!Editor.ChangeCrest(state, slot, crest, settings.Source, out var old, settings.Key)) + return; + + var actors = Applier.ChangeCrests(state, settings.Source.RequiresChange()); + Glamourer.Log.Verbose( + $"Set {slot.ToLabel()} crest in state {state.Identifier.Incognito(null)} from {old} to {crest}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Crest, settings.Source, state, actors, new CrestTransaction(slot, old, crest)); + } + + /// + public void ChangeCustomizeParameter(object data, CustomizeParameterFlag flag, CustomizeParameterValue value, ApplySettings settings) + { + var state = (ActorState)data; + // Also apply main color to highlights when highlights is off. + if (!state.ModelData.Customize.Highlights && flag is CustomizeParameterFlag.HairDiffuse) + ChangeCustomizeParameter(state, CustomizeParameterFlag.HairHighlight, value, settings); + + if (!Editor.ChangeParameter(state, flag, value, settings.Source, out var old, settings.Key)) + return; + + var @new = state.ModelData.Parameters[flag]; + var actors = Applier.ChangeParameters(state, flag, settings.Source.RequiresChange()); + Glamourer.Log.Verbose( + $"Set {flag} in state {state.Identifier.Incognito(null)} from {old} to {@new}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Parameter, settings.Source, state, actors, new ParameterTransaction(flag, old, @new)); + } + + public void ChangeMaterialValue(object data, MaterialValueIndex index, in MaterialValueState newValue, ApplySettings settings) + { + var state = (ActorState)data; + if (!Editor.ChangeMaterialValue(state, index, newValue, settings.Source, out var oldValue, settings.Key)) + return; + + var actors = Applier.ChangeMaterialValue(state, index, settings.Source.RequiresChange()); + Glamourer.Log.Verbose( + $"Set material value in state {state.Identifier.Incognito(null)} from {oldValue} to {newValue.Game}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.MaterialValue, settings.Source, state, actors, + new MaterialTransaction(index, oldValue, newValue.Game)); + } + + public void ResetMaterialValue(object data, MaterialValueIndex index, ApplySettings settings) + { + var state = (ActorState)data; + if (!Editor.ResetMaterialValue(state, index, settings.Key)) + return; + + var actors = Applier.ChangeMaterialValue(state, index, true); + Glamourer.Log.Verbose( + $"Reset material value in state {state.Identifier.Incognito(null)} to game value. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.MaterialValue, settings.Source, state, actors, new MaterialTransaction(index, null, null)); + } + + /// + public void ChangeMetaState(object data, MetaIndex index, bool value, ApplySettings settings) + { + var state = (ActorState)data; + if (!Editor.ChangeMetaState(state, index, value, settings.Source, out var old, settings.Key)) + return; + + var actors = Applier.ChangeMetaState(state, index, settings.Source.RequiresChange()); + Glamourer.Log.Verbose( + $"Set {index.ToName()} in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Other, settings.Source, state, actors, new MetaTransaction(index, old, value)); + } + + /// + public void ApplyDesign(object data, MergedDesign mergedDesign, ApplySettings settings) + { + var state = (ActorState)data; + modApplier.HandleStateApplication(state, mergedDesign, settings.Source, true, settings.RespectManual); + if (!Editor.ChangeModelId(state, mergedDesign.Design.DesignData.ModelId, mergedDesign.Design.DesignData.Customize, + mergedDesign.Design.GetDesignDataRef().GetEquipmentPtr(), settings.Source, out var oldModelId, settings.Key)) + return; + + var requiresRedraw = mergedDesign.ForcedRedraw + || oldModelId != mergedDesign.Design.DesignData.ModelId + || !mergedDesign.Design.DesignData.IsHuman; + + if (state.ModelData.IsHuman) { - if (!_gPose.InGPose) - return false; - - var old = oldItem; - _gPose.AddActionOnLeave(() => + foreach (var slot in CrestExtensions.AllRelevantSet.Where(mergedDesign.Design.DoApplyCrest)) { - if (old.Type == state.BaseData.Item(slot).Type) - ChangeItem(state, slot, old, state[slot, false], out _, key); - }); - } + if (!settings.RespectManual || !state.Sources[slot].IsManual()) + Editor.ChangeCrest(state, slot, mergedDesign.Design.DesignData.Crest(slot), Source(slot), + out _, settings.Key); + } - state.ModelData.SetItem(slot, item); - state[slot, false] = source; - return true; - } + var customizeFlags = mergedDesign.Design.Application.Customize; + if (mergedDesign.Design.DoApplyCustomize(CustomizeIndex.Clan)) + customizeFlags |= CustomizeFlag.Race; - /// Change a single piece of equipment including stain. - public bool ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateChanged.Source source, out EquipItem oldItem, - out StainId oldStain, uint key = 0) - { - oldItem = state.ModelData.Item(slot); - oldStain = state.ModelData.Stain(slot); - if (!state.CanUnlock(key)) - return false; + Func applyWhich = settings.RespectManual + ? i => customizeFlags.HasFlag(i.ToFlag()) && !state.Sources[i].IsManual() + : i => customizeFlags.HasFlag(i.ToFlag()); - // Can not change weapon type from expected type in state. - if (slot is EquipSlot.MainHand && item.Type != state.BaseData.MainhandType - || slot is EquipSlot.OffHand && item.Type != state.BaseData.OffhandType) - { - if (!_gPose.InGPose) - return false; + if (Editor.ChangeHumanCustomize(state, mergedDesign.Design.DesignData.Customize, applyWhich, i => Source(i), out _, out var changed, + settings.Key)) + requiresRedraw |= changed.RequiresRedraw(); - var old = oldItem; - var oldS = oldStain; - _gPose.AddActionOnLeave(() => + if (settings.ResetMaterials) { - if (old.Type == state.BaseData.Item(slot).Type) - ChangeEquip(state, slot, old, oldS, state[slot, false], out _, out _, key); - }); + state.ModelData.Parameters = state.BaseData.Parameters; + foreach (var parameter in CustomizeParameterExtensions.AllFlags) + state.Sources[parameter] = StateSource.Game; + } + + foreach (var parameter in mergedDesign.Design.Application.Parameters.Iterate()) + { + if (settings.RespectManual && state.Sources[parameter].IsManual()) + continue; + + var source = Source(parameter).SetPending(); + Editor.ChangeParameter(state, parameter, mergedDesign.Design.DesignData.Parameters[parameter], source, out _, settings.Key); + } + + // Do not apply highlights from a design if highlights is unchecked. + if (!state.ModelData.Customize.Highlights) + Editor.ChangeParameter(state, CustomizeParameterFlag.HairHighlight, + state.ModelData.Parameters[CustomizeParameterFlag.HairDiffuse], + state.Sources[CustomizeParameterFlag.HairDiffuse], out _, settings.Key); + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + if (mergedDesign.Design.DoApplyEquip(slot)) + if (!settings.RespectManual || !state.Sources[slot, false].IsManual()) + Editor.ChangeItem(state, slot, mergedDesign.Design.DesignData.Item(slot), + Source(slot.ToState()), out _, settings.Key); + + if (mergedDesign.Design.DoApplyStain(slot)) + if (!settings.RespectManual || !state.Sources[slot, true].IsManual()) + Editor.ChangeStains(state, slot, mergedDesign.Design.DesignData.Stain(slot), + Source(slot.ToState(true)), out _, settings.Key); + } + + foreach (var slot in BonusExtensions.AllFlags) + { + if (mergedDesign.Design.DoApplyBonusItem(slot)) + if (!settings.RespectManual || !state.Sources[slot].IsManual()) + Editor.ChangeBonusItem(state, slot, mergedDesign.Design.DesignData.BonusItem(slot), Source(slot), out _, settings.Key); + } + + foreach (var weaponSlot in EquipSlotExtensions.WeaponSlots) + { + if (mergedDesign.Design.DoApplyStain(weaponSlot)) + if (!settings.RespectManual || !state.Sources[weaponSlot, true].IsManual()) + Editor.ChangeStains(state, weaponSlot, mergedDesign.Design.DesignData.Stain(weaponSlot), + Source(weaponSlot.ToState(true)), out _, settings.Key); + + if (!mergedDesign.Design.DoApplyEquip(weaponSlot)) + continue; + + if (settings.RespectManual && state.Sources[weaponSlot, false].IsManual()) + continue; + + if (!settings.FromJobChange) + { + if (gPose.InGPose) + { + Editor.ChangeItem(state, weaponSlot, mergedDesign.Design.DesignData.Item(weaponSlot), + settings.UseSingleSource ? settings.Source : mergedDesign.Sources[weaponSlot, false], out var old, settings.Key); + var oldSource = state.Sources[weaponSlot, false]; + gPose.AddActionOnLeave(() => + { + if (old.Type == state.BaseData.Item(weaponSlot).Type) + Editor.ChangeItem(state, weaponSlot, old, oldSource, out _, settings.Key); + }); + } + + var currentType = state.BaseData.Item(weaponSlot).Type; + if (mergedDesign.Weapons.TryGet(currentType, state.LastJob, true, out var weapon)) + { + var source = settings.UseSingleSource ? settings.Source : + weapon.Item2 is StateSource.Game ? StateSource.Game : settings.Source; + Editor.ChangeItem(state, weaponSlot, weapon.Item1, source, out _, + settings.Key); + } + } + } + + if (settings.FromJobChange) + jobChange.Set(state, mergedDesign.Weapons.Values.Select(m => + (m.Item1, settings.UseSingleSource ? settings.Source : + m.Item2 is StateSource.Game ? StateSource.Game : settings.Source, m.Item3))); + + foreach (var meta in MetaExtensions.AllRelevant.Where(mergedDesign.Design.DoApplyMeta)) + { + if (!settings.RespectManual || !state.Sources[meta].IsManual()) + Editor.ChangeMetaState(state, meta, mergedDesign.Design.DesignData.GetMeta(meta), Source(meta), out _, settings.Key); + } + + if (settings.ResetMaterials || !settings.RespectManual && mergedDesign.ResetAdvancedDyes) + state.Materials.Clear(); + + foreach (var (key, value) in mergedDesign.Design.Materials) + { + if (!value.Enabled) + continue; + + var idx = MaterialValueIndex.FromKey(key); + var source = settings.Source.SetPending(); + if (state.Materials.TryGetValue(idx, out var materialState)) + { + if (settings.RespectManual && materialState.Source.IsManual()) + continue; + + if (value.Revert) + Editor.ChangeMaterialValue(state, idx, default, StateSource.Game, out _, settings.Key); + else + Editor.ChangeMaterialValue(state, idx, + new MaterialValueState(materialState.Game, value.Value, materialState.DrawData, source), settings.Source, out _, + settings.Key); + } + else if (!value.Revert) + { + Editor.ChangeMaterialValue(state, idx, + new MaterialValueState(ColorRow.Empty, value.Value, CharacterWeapon.Empty, source), + settings.Source, out _, settings.Key); + } + } } - state.ModelData.SetItem(slot, item); - state.ModelData.SetStain(slot, stain); - state[slot, false] = source; - state[slot, true] = source; - return true; - } + var actors = settings.Source.RequiresChange() + ? Applier.ApplyAll(state, requiresRedraw, false) + : ActorData.Invalid; - /// Change only the stain of an equipment piece. - public bool ChangeStain(ActorState state, EquipSlot slot, StainId stain, StateChanged.Source source, out StainId oldStain, uint key = 0) - { - oldStain = state.ModelData.Stain(slot); - if (!state.CanUnlock(key)) - return false; + Glamourer.Log.Verbose( + $"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Design, state.Sources[MetaIndex.Wetness], state, actors, null); // FIXME: maybe later + if (settings.IsFinal) + StateFinalized.Invoke(StateFinalizationType.DesignApplied, actors); - state.ModelData.SetStain(slot, stain); - state[slot, true] = source; - return true; - } + return; - public bool ChangeMetaState(ActorState state, ActorState.MetaIndex index, bool value, StateChanged.Source source, out bool oldValue, - uint key = 0) - { - (var setter, oldValue) = index switch + StateSource Source(StateIndex index) { - ActorState.MetaIndex.Wetness => ((Func)(v => state.ModelData.SetIsWet(v)), state.ModelData.IsWet()), - ActorState.MetaIndex.HatState => ((Func)(v => state.ModelData.SetHatVisible(v)), state.ModelData.IsHatVisible()), - ActorState.MetaIndex.VisorState => ((Func)(v => state.ModelData.SetVisor(v)), state.ModelData.IsVisorToggled()), - ActorState.MetaIndex.WeaponState => ((Func)(v => state.ModelData.SetWeaponVisible(v)), - state.ModelData.IsWeaponVisible()), - _ => throw new Exception("Invalid MetaIndex."), - }; + if (settings.UseSingleSource) + return settings.Source; - if (!state.CanUnlock(key)) - return false; + var source = mergedDesign.Sources[index]; + return source is StateSource.Game ? StateSource.Game : settings.Source; + } + } - setter(value); - state[index] = source; - return true; + public void ApplyDesign(object data, DesignBase design, ApplySettings settings) + { + var state = (ActorState)data; + MergedDesign merged; + if (!settings.MergeLinks || design is not Design d) + merged = new MergedDesign(design); + else + merged = merger.Merge(d.AllLinks(true), state.ModelData.IsHuman ? state.ModelData.Customize : CustomizeArray.Default, + state.BaseData, + false, Config.AlwaysApplyAssociatedMods); + + ApplyDesign(data, merged, settings with + { + FromJobChange = false, + RespectManual = false, + UseSingleSource = true, + }); + } + + + /// Apply offhand item and potentially gauntlets if configured. + private void ApplyMainhandPeriphery(ActorState state, EquipItem? newMainhand, StainIds? newStains, ApplySettings settings) + { + if (!Config.ChangeEntireItem || !settings.Source.IsManual()) + return; + + var mh = newMainhand ?? state.ModelData.Item(EquipSlot.MainHand); + // Do not change Shields to nothing. + if (mh.Type is FullEquipType.Sword) + return; + + var offhand = newMainhand != null ? Items.GetDefaultOffhand(mh) : state.ModelData.Item(EquipSlot.OffHand); + var stains = newStains ?? state.ModelData.Stain(EquipSlot.MainHand); + if (offhand.Valid) + ChangeEquip(state, EquipSlot.OffHand, offhand, stains, settings); + + if (mh is { Type: FullEquipType.Fists } && Items.ItemData.Tertiary.TryGetValue(mh.ItemId, out var gauntlets)) + ChangeEquip(state, EquipSlot.Hands, newMainhand != null ? gauntlets : state.ModelData.Item(EquipSlot.Hands), + stains, settings); } } diff --git a/Glamourer/State/StateIndex.cs b/Glamourer/State/StateIndex.cs new file mode 100644 index 0000000..dff05a3 --- /dev/null +++ b/Glamourer/State/StateIndex.cs @@ -0,0 +1,370 @@ +using Glamourer.Api.Enums; +using Glamourer.Designs; +using Glamourer.GameData; +using Penumbra.GameData.Enums; + +namespace Glamourer.State; + +public readonly record struct StateIndex(int Value) : IEqualityOperators +{ + public static readonly StateIndex Invalid = new(-1); + + public static implicit operator StateIndex(EquipFlag flag) + => flag switch + { + EquipFlag.Head => new StateIndex(EquipHead), + EquipFlag.Body => new StateIndex(EquipBody), + EquipFlag.Hands => new StateIndex(EquipHands), + EquipFlag.Legs => new StateIndex(EquipLegs), + EquipFlag.Feet => new StateIndex(EquipFeet), + EquipFlag.Ears => new StateIndex(EquipEars), + EquipFlag.Neck => new StateIndex(EquipNeck), + EquipFlag.Wrist => new StateIndex(EquipWrist), + EquipFlag.RFinger => new StateIndex(EquipRFinger), + EquipFlag.LFinger => new StateIndex(EquipLFinger), + EquipFlag.Mainhand => new StateIndex(EquipMainhand), + EquipFlag.Offhand => new StateIndex(EquipOffhand), + EquipFlag.HeadStain => new StateIndex(StainHead), + EquipFlag.BodyStain => new StateIndex(StainBody), + EquipFlag.HandsStain => new StateIndex(StainHands), + EquipFlag.LegsStain => new StateIndex(StainLegs), + EquipFlag.FeetStain => new StateIndex(StainFeet), + EquipFlag.EarsStain => new StateIndex(StainEars), + EquipFlag.NeckStain => new StateIndex(StainNeck), + EquipFlag.WristStain => new StateIndex(StainWrist), + EquipFlag.RFingerStain => new StateIndex(StainRFinger), + EquipFlag.LFingerStain => new StateIndex(StainLFinger), + EquipFlag.MainhandStain => new StateIndex(StainMainhand), + EquipFlag.OffhandStain => new StateIndex(StainOffhand), + _ => Invalid, + }; + + public static implicit operator StateIndex(BonusItemFlag flag) + => flag switch + { + BonusItemFlag.Glasses => new StateIndex(BonusItemGlasses), + _ => Invalid, + }; + + public static implicit operator StateIndex(CustomizeIndex index) + => index switch + { + CustomizeIndex.Race => new StateIndex(CustomizeRace), + CustomizeIndex.Gender => new StateIndex(CustomizeGender), + CustomizeIndex.BodyType => new StateIndex(CustomizeBodyType), + CustomizeIndex.Height => new StateIndex(CustomizeHeight), + CustomizeIndex.Clan => new StateIndex(CustomizeClan), + CustomizeIndex.Face => new StateIndex(CustomizeFace), + CustomizeIndex.Hairstyle => new StateIndex(CustomizeHairstyle), + CustomizeIndex.Highlights => new StateIndex(CustomizeHighlights), + CustomizeIndex.SkinColor => new StateIndex(CustomizeSkinColor), + CustomizeIndex.EyeColorRight => new StateIndex(CustomizeEyeColorRight), + CustomizeIndex.HairColor => new StateIndex(CustomizeHairColor), + CustomizeIndex.HighlightsColor => new StateIndex(CustomizeHighlightsColor), + CustomizeIndex.FacialFeature1 => new StateIndex(CustomizeFacialFeature1), + CustomizeIndex.FacialFeature2 => new StateIndex(CustomizeFacialFeature2), + CustomizeIndex.FacialFeature3 => new StateIndex(CustomizeFacialFeature3), + CustomizeIndex.FacialFeature4 => new StateIndex(CustomizeFacialFeature4), + CustomizeIndex.FacialFeature5 => new StateIndex(CustomizeFacialFeature5), + CustomizeIndex.FacialFeature6 => new StateIndex(CustomizeFacialFeature6), + CustomizeIndex.FacialFeature7 => new StateIndex(CustomizeFacialFeature7), + CustomizeIndex.LegacyTattoo => new StateIndex(CustomizeLegacyTattoo), + CustomizeIndex.TattooColor => new StateIndex(CustomizeTattooColor), + CustomizeIndex.Eyebrows => new StateIndex(CustomizeEyebrows), + CustomizeIndex.EyeColorLeft => new StateIndex(CustomizeEyeColorLeft), + CustomizeIndex.EyeShape => new StateIndex(CustomizeEyeShape), + CustomizeIndex.SmallIris => new StateIndex(CustomizeSmallIris), + CustomizeIndex.Nose => new StateIndex(CustomizeNose), + CustomizeIndex.Jaw => new StateIndex(CustomizeJaw), + CustomizeIndex.Mouth => new StateIndex(CustomizeMouth), + CustomizeIndex.Lipstick => new StateIndex(CustomizeLipstick), + CustomizeIndex.LipColor => new StateIndex(CustomizeLipColor), + CustomizeIndex.MuscleMass => new StateIndex(CustomizeMuscleMass), + CustomizeIndex.TailShape => new StateIndex(CustomizeTailShape), + CustomizeIndex.BustSize => new StateIndex(CustomizeBustSize), + CustomizeIndex.FacePaint => new StateIndex(CustomizeFacePaint), + CustomizeIndex.FacePaintReversed => new StateIndex(CustomizeFacePaintReversed), + CustomizeIndex.FacePaintColor => new StateIndex(CustomizeFacePaintColor), + _ => Invalid, + }; + + public static implicit operator StateIndex(MetaIndex meta) + => new((int)meta); + + public static implicit operator StateIndex(CrestFlag crest) + => crest switch + { + CrestFlag.OffHand => new StateIndex(CrestOffhand), + CrestFlag.Head => new StateIndex(CrestHead), + CrestFlag.Body => new StateIndex(CrestBody), + _ => Invalid, + }; + + public static implicit operator StateIndex(CustomizeParameterFlag param) + => param switch + { + CustomizeParameterFlag.SkinDiffuse => new StateIndex(ParamSkinDiffuse), + CustomizeParameterFlag.MuscleTone => new StateIndex(ParamMuscleTone), + CustomizeParameterFlag.SkinSpecular => new StateIndex(ParamSkinSpecular), + CustomizeParameterFlag.LipDiffuse => new StateIndex(ParamLipDiffuse), + CustomizeParameterFlag.HairDiffuse => new StateIndex(ParamHairDiffuse), + CustomizeParameterFlag.HairSpecular => new StateIndex(ParamHairSpecular), + CustomizeParameterFlag.HairHighlight => new StateIndex(ParamHairHighlight), + CustomizeParameterFlag.LeftEye => new StateIndex(ParamLeftEye), + CustomizeParameterFlag.LeftLimbalIntensity => new StateIndex(ParamLeftLimbalIntensity), + CustomizeParameterFlag.RightEye => new StateIndex(ParamRightEye), + CustomizeParameterFlag.RightLimbalIntensity => new StateIndex(ParamRightLimbalIntensity), + CustomizeParameterFlag.FeatureColor => new StateIndex(ParamFeatureColor), + CustomizeParameterFlag.FacePaintUvMultiplier => new StateIndex(ParamFacePaintUvMultiplier), + CustomizeParameterFlag.FacePaintUvOffset => new StateIndex(ParamFacePaintUvOffset), + CustomizeParameterFlag.DecalColor => new StateIndex(ParamDecalColor), + _ => Invalid, + }; + + public const int EquipHead = 0; + public const int EquipBody = EquipHead + 1; + public const int EquipHands = EquipBody + 1; + public const int EquipLegs = EquipHands + 1; + public const int EquipFeet = EquipLegs + 1; + public const int EquipEars = EquipFeet + 1; + public const int EquipNeck = EquipEars + 1; + public const int EquipWrist = EquipNeck + 1; + public const int EquipRFinger = EquipWrist + 1; + public const int EquipLFinger = EquipRFinger + 1; + public const int EquipMainhand = EquipLFinger + 1; + public const int EquipOffhand = EquipMainhand + 1; + + public const int StainHead = EquipOffhand + 1; + public const int StainBody = StainHead + 1; + public const int StainHands = StainBody + 1; + public const int StainLegs = StainHands + 1; + public const int StainFeet = StainLegs + 1; + public const int StainEars = StainFeet + 1; + public const int StainNeck = StainEars + 1; + public const int StainWrist = StainNeck + 1; + public const int StainRFinger = StainWrist + 1; + public const int StainLFinger = StainRFinger + 1; + public const int StainMainhand = StainLFinger + 1; + public const int StainOffhand = StainMainhand + 1; + + public const int CustomizeRace = StainOffhand + 1; + public const int CustomizeGender = CustomizeRace + 1; + public const int CustomizeBodyType = CustomizeGender + 1; + public const int CustomizeHeight = CustomizeBodyType + 1; + public const int CustomizeClan = CustomizeHeight + 1; + public const int CustomizeFace = CustomizeClan + 1; + public const int CustomizeHairstyle = CustomizeFace + 1; + public const int CustomizeHighlights = CustomizeHairstyle + 1; + public const int CustomizeSkinColor = CustomizeHighlights + 1; + public const int CustomizeEyeColorRight = CustomizeSkinColor + 1; + public const int CustomizeHairColor = CustomizeEyeColorRight + 1; + public const int CustomizeHighlightsColor = CustomizeHairColor + 1; + public const int CustomizeFacialFeature1 = CustomizeHighlightsColor + 1; + public const int CustomizeFacialFeature2 = CustomizeFacialFeature1 + 1; + public const int CustomizeFacialFeature3 = CustomizeFacialFeature2 + 1; + public const int CustomizeFacialFeature4 = CustomizeFacialFeature3 + 1; + public const int CustomizeFacialFeature5 = CustomizeFacialFeature4 + 1; + public const int CustomizeFacialFeature6 = CustomizeFacialFeature5 + 1; + public const int CustomizeFacialFeature7 = CustomizeFacialFeature6 + 1; + public const int CustomizeLegacyTattoo = CustomizeFacialFeature7 + 1; + public const int CustomizeTattooColor = CustomizeLegacyTattoo + 1; + public const int CustomizeEyebrows = CustomizeTattooColor + 1; + public const int CustomizeEyeColorLeft = CustomizeEyebrows + 1; + public const int CustomizeEyeShape = CustomizeEyeColorLeft + 1; + public const int CustomizeSmallIris = CustomizeEyeShape + 1; + public const int CustomizeNose = CustomizeSmallIris + 1; + public const int CustomizeJaw = CustomizeNose + 1; + public const int CustomizeMouth = CustomizeJaw + 1; + public const int CustomizeLipstick = CustomizeMouth + 1; + public const int CustomizeLipColor = CustomizeLipstick + 1; + public const int CustomizeMuscleMass = CustomizeLipColor + 1; + public const int CustomizeTailShape = CustomizeMuscleMass + 1; + public const int CustomizeBustSize = CustomizeTailShape + 1; + public const int CustomizeFacePaint = CustomizeBustSize + 1; + public const int CustomizeFacePaintReversed = CustomizeFacePaint + 1; + public const int CustomizeFacePaintColor = CustomizeFacePaintReversed + 1; + + public const int MetaWetness = CustomizeFacePaintColor + 1; + public const int MetaHatState = MetaWetness + 1; + public const int MetaVisorState = MetaHatState + 1; + public const int MetaWeaponState = MetaVisorState + 1; + public const int MetaModelId = MetaWeaponState + 1; + public const int MetaEarState = MetaModelId + 1; + + public const int CrestHead = MetaEarState + 1; + public const int CrestBody = CrestHead + 1; + public const int CrestOffhand = CrestBody + 1; + + public const int ParamSkinDiffuse = CrestOffhand + 1; + public const int ParamMuscleTone = ParamSkinDiffuse + 1; + public const int ParamSkinSpecular = ParamMuscleTone + 1; + public const int ParamLipDiffuse = ParamSkinSpecular + 1; + public const int ParamHairDiffuse = ParamLipDiffuse + 1; + public const int ParamHairSpecular = ParamHairDiffuse + 1; + public const int ParamHairHighlight = ParamHairSpecular + 1; + public const int ParamLeftEye = ParamHairHighlight + 1; + public const int ParamLeftLimbalIntensity = ParamLeftEye + 1; + public const int ParamRightEye = ParamLeftLimbalIntensity + 1; + public const int ParamRightLimbalIntensity = ParamRightEye + 1; + public const int ParamFeatureColor = ParamRightLimbalIntensity + 1; + public const int ParamFacePaintUvMultiplier = ParamFeatureColor + 1; + public const int ParamFacePaintUvOffset = ParamFacePaintUvMultiplier + 1; + public const int ParamDecalColor = ParamFacePaintUvOffset + 1; + + public const int BonusItemGlasses = ParamDecalColor + 1; + + public const int Size = BonusItemGlasses + 1; + + public static IEnumerable All + => Enumerable.Range(0, Size - 1).Select(i => new StateIndex(i)); + + public string ToName() + => GetFlag() switch + { + EquipFlag e => GetName(e), + CustomizeFlag c => c.ToIndex().ToDefaultName(), + MetaFlag m => m.ToIndex().ToName(), + CrestFlag c => c.ToLabel(), + CustomizeParameterFlag c => c.ToName(), + BonusItemFlag b => b.ToName(), + bool v => "Model ID", + _ => "Unknown", + }; + + public object GetFlag() + => Value switch + { + EquipHead => EquipFlag.Head, + EquipBody => EquipFlag.Body, + EquipHands => EquipFlag.Hands, + EquipLegs => EquipFlag.Legs, + EquipFeet => EquipFlag.Feet, + EquipEars => EquipFlag.Ears, + EquipNeck => EquipFlag.Neck, + EquipWrist => EquipFlag.Wrist, + EquipRFinger => EquipFlag.RFinger, + EquipLFinger => EquipFlag.LFinger, + EquipMainhand => EquipFlag.Mainhand, + EquipOffhand => EquipFlag.Offhand, + + StainHead => EquipFlag.HeadStain, + StainBody => EquipFlag.BodyStain, + StainHands => EquipFlag.HandsStain, + StainLegs => EquipFlag.LegsStain, + StainFeet => EquipFlag.FeetStain, + StainEars => EquipFlag.EarsStain, + StainNeck => EquipFlag.NeckStain, + StainWrist => EquipFlag.WristStain, + StainRFinger => EquipFlag.RFingerStain, + StainLFinger => EquipFlag.LFingerStain, + StainMainhand => EquipFlag.MainhandStain, + StainOffhand => EquipFlag.OffhandStain, + + CustomizeRace => CustomizeFlag.Race, + CustomizeGender => CustomizeFlag.Gender, + CustomizeBodyType => CustomizeFlag.BodyType, + CustomizeHeight => CustomizeFlag.Height, + CustomizeClan => CustomizeFlag.Clan, + CustomizeFace => CustomizeFlag.Face, + CustomizeHairstyle => CustomizeFlag.Hairstyle, + CustomizeHighlights => CustomizeFlag.Highlights, + CustomizeSkinColor => CustomizeFlag.SkinColor, + CustomizeEyeColorRight => CustomizeFlag.EyeColorRight, + CustomizeHairColor => CustomizeFlag.HairColor, + CustomizeHighlightsColor => CustomizeFlag.HighlightsColor, + CustomizeFacialFeature1 => CustomizeFlag.FacialFeature1, + CustomizeFacialFeature2 => CustomizeFlag.FacialFeature2, + CustomizeFacialFeature3 => CustomizeFlag.FacialFeature3, + CustomizeFacialFeature4 => CustomizeFlag.FacialFeature4, + CustomizeFacialFeature5 => CustomizeFlag.FacialFeature5, + CustomizeFacialFeature6 => CustomizeFlag.FacialFeature6, + CustomizeFacialFeature7 => CustomizeFlag.FacialFeature7, + CustomizeLegacyTattoo => CustomizeFlag.LegacyTattoo, + CustomizeTattooColor => CustomizeFlag.TattooColor, + CustomizeEyebrows => CustomizeFlag.Eyebrows, + CustomizeEyeColorLeft => CustomizeFlag.EyeColorLeft, + CustomizeEyeShape => CustomizeFlag.EyeShape, + CustomizeSmallIris => CustomizeFlag.SmallIris, + CustomizeNose => CustomizeFlag.Nose, + CustomizeJaw => CustomizeFlag.Jaw, + CustomizeMouth => CustomizeFlag.Mouth, + CustomizeLipstick => CustomizeFlag.Lipstick, + CustomizeLipColor => CustomizeFlag.LipColor, + CustomizeMuscleMass => CustomizeFlag.MuscleMass, + CustomizeTailShape => CustomizeFlag.TailShape, + CustomizeBustSize => CustomizeFlag.BustSize, + CustomizeFacePaint => CustomizeFlag.FacePaint, + CustomizeFacePaintReversed => CustomizeFlag.FacePaintReversed, + CustomizeFacePaintColor => CustomizeFlag.FacePaintColor, + + MetaWetness => MetaFlag.Wetness, + MetaHatState => MetaFlag.HatState, + MetaVisorState => MetaFlag.VisorState, + MetaWeaponState => MetaFlag.WeaponState, + MetaEarState => MetaFlag.EarState, + MetaModelId => true, + + CrestHead => CrestFlag.Head, + CrestBody => CrestFlag.Body, + CrestOffhand => CrestFlag.OffHand, + + ParamSkinDiffuse => CustomizeParameterFlag.SkinDiffuse, + ParamMuscleTone => CustomizeParameterFlag.MuscleTone, + ParamSkinSpecular => CustomizeParameterFlag.SkinSpecular, + ParamLipDiffuse => CustomizeParameterFlag.LipDiffuse, + ParamHairDiffuse => CustomizeParameterFlag.HairDiffuse, + ParamHairSpecular => CustomizeParameterFlag.HairSpecular, + ParamHairHighlight => CustomizeParameterFlag.HairHighlight, + ParamLeftEye => CustomizeParameterFlag.LeftEye, + ParamLeftLimbalIntensity => CustomizeParameterFlag.LeftLimbalIntensity, + ParamRightEye => CustomizeParameterFlag.RightEye, + ParamRightLimbalIntensity => CustomizeParameterFlag.RightLimbalIntensity, + ParamFeatureColor => CustomizeParameterFlag.FeatureColor, + ParamFacePaintUvMultiplier => CustomizeParameterFlag.FacePaintUvMultiplier, + ParamFacePaintUvOffset => CustomizeParameterFlag.FacePaintUvOffset, + ParamDecalColor => CustomizeParameterFlag.DecalColor, + + BonusItemGlasses => BonusItemFlag.Glasses, + + _ => -1, + }; + + private static string GetName(EquipFlag flag) + { + var slot = flag.ToSlot(out var stain); + return stain ? $"{slot.ToName()} Stain" : slot.ToName(); + } +} + +public static class StateExtensions +{ + public static StateIndex ToState(this EquipSlot slot, bool stain = false) + => (slot, stain) switch + { + (EquipSlot.Head, true) => new StateIndex(StateIndex.EquipHead), + (EquipSlot.Body, true) => new StateIndex(StateIndex.EquipBody), + (EquipSlot.Hands, true) => new StateIndex(StateIndex.EquipHands), + (EquipSlot.Legs, true) => new StateIndex(StateIndex.EquipLegs), + (EquipSlot.Feet, true) => new StateIndex(StateIndex.EquipFeet), + (EquipSlot.Ears, true) => new StateIndex(StateIndex.EquipEars), + (EquipSlot.Neck, true) => new StateIndex(StateIndex.EquipNeck), + (EquipSlot.Wrists, true) => new StateIndex(StateIndex.EquipWrist), + (EquipSlot.RFinger, true) => new StateIndex(StateIndex.EquipRFinger), + (EquipSlot.LFinger, true) => new StateIndex(StateIndex.EquipLFinger), + (EquipSlot.MainHand, true) => new StateIndex(StateIndex.EquipMainhand), + (EquipSlot.OffHand, true) => new StateIndex(StateIndex.EquipOffhand), + (EquipSlot.Head, false) => new StateIndex(StateIndex.StainHead), + (EquipSlot.Body, false) => new StateIndex(StateIndex.StainBody), + (EquipSlot.Hands, false) => new StateIndex(StateIndex.StainHands), + (EquipSlot.Legs, false) => new StateIndex(StateIndex.StainLegs), + (EquipSlot.Feet, false) => new StateIndex(StateIndex.StainFeet), + (EquipSlot.Ears, false) => new StateIndex(StateIndex.StainEars), + (EquipSlot.Neck, false) => new StateIndex(StateIndex.StainNeck), + (EquipSlot.Wrists, false) => new StateIndex(StateIndex.StainWrist), + (EquipSlot.RFinger, false) => new StateIndex(StateIndex.StainRFinger), + (EquipSlot.LFinger, false) => new StateIndex(StateIndex.StainLFinger), + (EquipSlot.MainHand, false) => new StateIndex(StateIndex.StainMainhand), + (EquipSlot.OffHand, false) => new StateIndex(StateIndex.StainOffhand), + _ => StateIndex.Invalid, + }; +} diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index 6e1d060..4b70718 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -1,18 +1,21 @@ using Glamourer.Automation; -using Glamourer.Customization; using Glamourer.Events; using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; -using OtterGui.Classes; using Penumbra.GameData.Actors; -using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using System; using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Glamourer.GameData; +using Penumbra.GameData.DataContainers; +using Glamourer.Designs; +using Penumbra.GameData.Interop; +using Glamourer.Api.Enums; namespace Glamourer.State; @@ -24,48 +27,52 @@ namespace Glamourer.State; public class StateListener : IDisposable { private readonly Configuration _config; - private readonly ActorService _actors; - private readonly ObjectManager _objects; + private readonly ActorManager _actors; + private readonly ActorObjectManager _objects; private readonly StateManager _manager; private readonly StateApplier _applier; private readonly ItemManager _items; - private readonly CustomizationService _customizations; + private readonly CustomizeService _customizations; private readonly PenumbraService _penumbra; - private readonly SlotUpdating _slotUpdating; + private readonly EquipSlotUpdating _equipSlotUpdating; + private readonly BonusSlotUpdating _bonusSlotUpdating; + private readonly GearsetDataLoaded _gearsetDataLoaded; private readonly WeaponLoading _weaponLoading; private readonly HeadGearVisibilityChanged _headGearVisibility; private readonly VisorStateChanged _visorState; + private readonly VieraEarStateChanged _vieraEarState; private readonly WeaponVisibilityChanged _weaponVisibility; + private readonly StateFinalized _stateFinalized; private readonly AutoDesignApplier _autoDesignApplier; private readonly FunModule _funModule; private readonly HumanModelList _humans; private readonly MovedEquipment _movedEquipment; private readonly GPoseService _gPose; private readonly ChangeCustomizeService _changeCustomizeService; + private readonly CrestService _crestService; private readonly ICondition _condition; + private readonly Dictionary _fistOffhands = []; + private ActorIdentifier _creatingIdentifier = ActorIdentifier.Invalid; + private bool _isPlayerNpc; private ActorState? _creatingState; - private CharacterWeapon _lastFistOffhand = CharacterWeapon.Empty; + private ActorState? _customizeState; - public bool Enabled - { - get => _config.Enabled; - set => Enable(value); - } - - public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorService actors, Configuration config, - SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility, - HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans, - StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gPose, - ChangeCustomizeService changeCustomizeService, CustomizationService customizations, ICondition condition) + public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorManager actors, Configuration config, + EquipSlotUpdating equipSlotUpdating, GearsetDataLoaded gearsetDataLoaded, WeaponLoading weaponLoading, VisorStateChanged visorState, + WeaponVisibilityChanged weaponVisibility, HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, + FunModule funModule, HumanModelList humans, StateApplier applier, MovedEquipment movedEquipment, ActorObjectManager objects, + GPoseService gPose, ChangeCustomizeService changeCustomizeService, CustomizeService customizations, ICondition condition, + CrestService crestService, BonusSlotUpdating bonusSlotUpdating, StateFinalized stateFinalized, VieraEarStateChanged vieraEarState) { _manager = manager; _items = items; _penumbra = penumbra; _actors = actors; _config = config; - _slotUpdating = slotUpdating; + _equipSlotUpdating = equipSlotUpdating; + _gearsetDataLoaded = gearsetDataLoaded; _weaponLoading = weaponLoading; _visorState = visorState; _weaponVisibility = weaponVisibility; @@ -80,30 +87,15 @@ public class StateListener : IDisposable _changeCustomizeService = changeCustomizeService; _customizations = customizations; _condition = condition; - - if (Enabled) - Subscribe(); - } - - public void Enable(bool value) - { - if (value == Enabled) - return; - - _config.Enabled = value; - _config.Save(); - - if (value) - Subscribe(); - else - Unsubscribe(); + _crestService = crestService; + _bonusSlotUpdating = bonusSlotUpdating; + _stateFinalized = stateFinalized; + _vieraEarState = vieraEarState; + Subscribe(); } void IDisposable.Dispose() - { - if (Enabled) - Unsubscribe(); - } + => Unsubscribe(); /// The result of updating the base state of an ActorState. private enum UpdateState @@ -116,6 +108,9 @@ public class StateListener : IDisposable /// The base state changed compared to prior state. Change, + + /// Special case for hat stuff. + HatHack, } /// @@ -124,18 +119,20 @@ public class StateListener : IDisposable /// Weapons and meta flags are updated independently. /// We also need to apply fixed designs here. /// - private unsafe void OnCreatingCharacterBase(nint actorPtr, string _, nint modelPtr, nint customizePtr, nint equipDataPtr) + private unsafe void OnCreatingCharacterBase(nint actorPtr, Guid _, nint modelPtr, nint customizePtr, nint equipDataPtr) { var actor = (Actor)actorPtr; if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) return; - _creatingIdentifier = actor.GetIdentifier(_actors.AwaitedService); - + _creatingIdentifier = actor.GetIdentifier(_actors); ref var modelId = ref *(uint*)modelPtr; - ref var customize = ref *(Customize*)customizePtr; + ref var customize = ref *(CustomizeArray*)customizePtr; if (_autoDesignApplier.Reduce(actor, _creatingIdentifier, out _creatingState)) { + _isPlayerNpc = _creatingIdentifier.Type is IdentifierType.Player + && actor.IsCharacter + && actor.AsCharacter->GetObjectKind() is ObjectKind.EventNpc; switch (UpdateBaseData(actor, _creatingState, modelId, customizePtr, equipDataPtr)) { // TODO handle right @@ -154,12 +151,12 @@ public class StateListener : IDisposable _creatingState.TempUnlock(); } - _funModule.ApplyFun(actor, new Span((void*)equipDataPtr, 10), ref customize); - if (modelId == 0) + _funModule.ApplyFunOnLoad(actor, new Span((void*)equipDataPtr, 10), ref customize); + if (modelId == 0 && _creatingState is not { IsLocked: true }) ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender); } - private unsafe void OnCustomizeChange(Model model, Ref customize) + private void OnCustomizeChange(Model model, ref CustomizeArray customize) { if (!model.IsHuman) return; @@ -168,14 +165,14 @@ public class StateListener : IDisposable if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) return; - if (!actor.Identifier(_actors.AwaitedService, out var identifier) - || !_manager.TryGetValue(identifier, out var state)) + if (!actor.Identifier(_actors, out var identifier) + || !_manager.TryGetValue(identifier, out _customizeState)) return; - UpdateCustomize(actor, state, ref customize.Value, false); + UpdateCustomize(actor, _customizeState, ref customize, false); } - private void UpdateCustomize(Actor actor, ActorState state, ref Customize customize, bool checkTransform) + private void UpdateCustomize(Actor actor, ActorState state, ref CustomizeArray customize, bool checkTransform) { switch (UpdateBaseData(actor, state, customize, checkTransform)) { @@ -184,21 +181,21 @@ public class StateListener : IDisposable var model = state.ModelData.Customize; if (customize.Gender != model.Gender || customize.Clan != model.Clan) { - _manager.ChangeCustomize(state, in customize, CustomizeFlagExtensions.AllRelevant, StateChanged.Source.Game); + _manager.ChangeEntireCustomize(state, in customize, CustomizeFlagExtensions.All, ApplySettings.Game); return; } - var set = _customizations.AwaitedService.GetList(model.Clan, model.Gender); + var set = _customizations.Manager.GetSet(model.Clan, model.Gender); foreach (var index in CustomizationExtensions.AllBasic) { - if (state[index] is not StateChanged.Source.Fixed) + if (!state.Sources[index].IsFixed()) { var newValue = customize[index]; var oldValue = model[index]; if (newValue != oldValue) { if (set.Validate(index, newValue, out _, model.Face)) - _manager.ChangeCustomize(state, index, newValue, StateChanged.Source.Game); + _manager.ChangeCustomize(state, index, newValue, ApplySettings.Game); else customize[index] = oldValue; } @@ -210,9 +207,7 @@ public class StateListener : IDisposable } break; - case UpdateState.NoChange: - customize = state.ModelData.Customize; - break; + case UpdateState.NoChange: customize = state.ModelData.Customize; break; } } @@ -220,7 +215,7 @@ public class StateListener : IDisposable /// A draw model loads a new equipment piece. /// Update base data, apply or update model data, and protect against restricted gear. /// - private void OnSlotUpdating(Model model, EquipSlot slot, Ref armor, Ref returnValue) + private void OnEquipSlotUpdating(Model model, EquipSlot slot, ref CharacterArmor armor, ref ulong returnValue) { var actor = _penumbra.GameObjectFromDrawObject(model); if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) @@ -230,24 +225,64 @@ public class StateListener : IDisposable // then we do not want to use our restricted gear protection // since we assume the player has that gear modded to availability. var locked = false; - if (actor.Identifier(_actors.AwaitedService, out var identifier) + if (actor.Identifier(_actors, out var identifier) && _manager.TryGetValue(identifier, out var state)) { - HandleEquipSlot(actor, state, slot, ref armor.Value); - locked = state[slot, false] is StateChanged.Source.Ipc; + HandleEquipSlot(actor, state, slot, ref armor); + locked = state.Sources[slot, false] is StateSource.IpcFixed; } - _funModule.ApplyFun(actor, ref armor.Value, slot); + _funModule.ApplyFunToSlot(actor, ref armor, slot); if (!_config.UseRestrictedGearProtection || locked) return; var customize = model.GetCustomize(); - (_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); + (_, armor) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender); } - private void OnMovedEquipment((EquipSlot, uint, StainId)[] items) + private void OnBonusSlotUpdating(Model model, BonusItemFlag slot, ref CharacterArmor item, ref ulong returnValue) + { + var actor = _penumbra.GameObjectFromDrawObject(model); + if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) + return; + + if (actor.Identifier(_actors, out var identifier) + && _manager.TryGetValue(identifier, out var state)) + switch (UpdateBaseData(actor, state, slot, item)) + { + // Base data changed equipment while actors were not there. + // Update model state if not on fixed design. + case UpdateState.Change: + var apply = false; + if (!state.Sources[slot].IsFixed()) + _manager.ChangeBonusItem(state, slot, state.BaseData.BonusItem(slot), ApplySettings.Game); + else + apply = true; + if (apply) + item = state.ModelData.BonusItem(slot).Armor(); + break; + // Use current model data. + case UpdateState.NoChange: item = state.ModelData.BonusItem(slot).Armor(); break; + case UpdateState.Transformed: break; + } + } + + private void OnGearsetDataLoaded(Actor actor, Model model) + { + if (!actor.Valid || _condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) + return; + + // ensure actor and state are valid. + if (!actor.Identifier(_actors, out var identifier)) + return; + + if (_objects.TryGetValue(identifier, out var actors) && actors.Valid) + _stateFinalized.Invoke(StateFinalizationType.Gearset, actors); + } + + + private void OnMovedEquipment((EquipSlot, uint, StainIds)[] items) { - _objects.Update(); var (identifier, objects) = _objects.PlayerData; if (!identifier.IsValid || !_manager.TryGetValue(identifier, out var state)) return; @@ -255,27 +290,43 @@ public class StateListener : IDisposable foreach (var (slot, item, stain) in items) { var currentItem = state.BaseData.Item(slot); - var model = state.ModelData.Weapon(slot); - var current = currentItem.Weapon(state.BaseData.Stain(slot)); - if (model.Value == current.Value || !_items.ItemService.AwaitedService.TryGetValue(item, EquipSlot.MainHand, out var changedItem)) + var model = slot is EquipSlot.MainHand or EquipSlot.OffHand + ? state.ModelData.Weapon(slot) + : state.ModelData.Armor(slot).ToWeapon(0); + var current = currentItem.Weapon(state.BaseData.Stain(slot)); + if (model.Value == current.Value || !_items.ItemData.TryGetValue(item, EquipSlot.MainHand, out var changedItem)) continue; var changed = changedItem.Weapon(stain); - if (current.Value == changed.Value && state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) + var itemChanged = current.Skeleton == changed.Skeleton + && current.Variant == changed.Variant + && current.Weapon == changed.Weapon + && !state.Sources[slot, false].IsFixed(); + + var stainChanged = current.Stains == changed.Stains && !state.Sources[slot, true].IsFixed(); + + switch (itemChanged, stainChanged) { - _manager.ChangeItem(state, slot, currentItem, StateChanged.Source.Game); - _manager.ChangeStain(state, slot, current.Stain, StateChanged.Source.Game); - switch (slot) - { - case EquipSlot.MainHand: - case EquipSlot.OffHand: - _applier.ChangeWeapon(objects, slot, currentItem, stain); - break; - default: - _applier.ChangeArmor(objects, slot, current.ToArmor(), state[slot, false] is not StateChanged.Source.Ipc, + case (true, true): + _manager.ChangeEquip(state, slot, currentItem, current.Stains, ApplySettings.Game); + if (slot is EquipSlot.MainHand or EquipSlot.OffHand) + _applier.ChangeWeapon(objects, slot, currentItem, current.Stains); + else + _applier.ChangeArmor(objects, slot, current.ToArmor(), !state.Sources[slot, false].IsFixed(), state.ModelData.IsHatVisible()); - break; - } + break; + case (true, false): + _manager.ChangeItem(state, slot, currentItem, ApplySettings.Game); + if (slot is EquipSlot.MainHand or EquipSlot.OffHand) + _applier.ChangeWeapon(objects, slot, currentItem, model.Stains); + else + _applier.ChangeArmor(objects, slot, current.ToArmor(model.Stains), !state.Sources[slot, false].IsFixed(), + state.ModelData.IsHatVisible()); + break; + case (false, true): + _manager.ChangeStains(state, slot, current.Stains, ApplySettings.Game); + _applier.ChangeStain(objects, slot, current.Stains); + break; } } } @@ -285,73 +336,80 @@ public class StateListener : IDisposable /// Update base data, apply or update model data. /// Verify consistent weapon types. /// - private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref weapon) + private void OnWeaponLoading(Actor actor, EquipSlot slot, ref CharacterWeapon weapon) { if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) return; // Fist weapon gauntlet hack. - if (slot is EquipSlot.OffHand && weapon.Value.Variant == 0 && weapon.Value.Set.Id != 0 && _lastFistOffhand.Set.Id != 0) - weapon.Value = _lastFistOffhand; + if (slot is EquipSlot.OffHand + && weapon.Variant == 0 + && weapon.Weapon.Id != 0 + && _fistOffhands.TryGetValue(actor, out var lastFistOffhand)) + { + Glamourer.Log.Verbose($"Applying stored fist weapon offhand {lastFistOffhand} for 0x{actor.Address:X}."); + weapon = lastFistOffhand; + } - if (!actor.Identifier(_actors.AwaitedService, out var identifier) + if (!actor.Identifier(_actors, out var identifier) || !_manager.TryGetValue(identifier, out var state)) return; - ref var actorWeapon = ref weapon.Value; - var baseType = state.BaseData.Item(slot).Type; - var apply = false; - switch (UpdateBaseData(actor, state, slot, actorWeapon)) + var apply = false; + switch (UpdateBaseData(actor, state, slot, weapon)) { // Do nothing. But this usually can not happen because the hooked function also writes to game objects later. case UpdateState.Transformed: break; case UpdateState.Change: - if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) - _manager.ChangeItem(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game); + if (!state.Sources[slot, false].IsFixed()) + _manager.ChangeItem(state, slot, state.BaseData.Item(slot), ApplySettings.Game); else apply = true; - if (state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) - _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game); + if (!state.Sources[slot, true].IsFixed()) + _manager.ChangeStains(state, slot, state.BaseData.Stain(slot), ApplySettings.Game); else apply = true; - - break; - case UpdateState.NoChange: - apply = true; break; + case UpdateState.NoChange: apply = true; break; } + var baseType = slot is EquipSlot.OffHand ? state.BaseData.MainhandType.Offhand() : state.BaseData.MainhandType; + var modelType = state.ModelData.Item(slot).Type; if (apply) { // Only allow overwriting identical weapons + var canApply = baseType == modelType + || _gPose.InGPose && actor.IsGPoseOrCutscene; var newWeapon = state.ModelData.Weapon(slot); - if (baseType is FullEquipType.Unknown || baseType == state.ModelData.Item(slot).Type || _gPose.InGPose && actor.IsGPoseOrCutscene) - actorWeapon = newWeapon; - else if (actorWeapon.Set.Id != 0) - actorWeapon = actorWeapon.With(newWeapon.Stain); + if (canApply) + { + weapon = newWeapon; + } + else + { + if (weapon.Skeleton.Id != 0) + weapon = weapon.With(newWeapon.Stains); + // Force unlock if necessary. + _manager.ChangeItem(state, slot, state.BaseData.Item(slot), ApplySettings.Game with { Key = state.Combination }); + } } // Fist Weapon Offhand hack. - if (slot is EquipSlot.MainHand && weapon.Value.Set.Id is > 1600 and < 1651) - _lastFistOffhand = new CharacterWeapon((SetId)(weapon.Value.Set.Id + 50), weapon.Value.Type, weapon.Value.Variant, - weapon.Value.Stain); + if (slot is EquipSlot.MainHand && weapon.Skeleton.Id is > 1600 and < 1651) + { + lastFistOffhand = new CharacterWeapon((PrimaryId)(weapon.Skeleton.Id + 50), weapon.Weapon, weapon.Variant, + weapon.Stains); + _fistOffhands[actor] = lastFistOffhand; + Glamourer.Log.Excessive($"Storing fist weapon offhand {lastFistOffhand} for 0x{actor.Address:X}."); + } - _funModule.ApplyFun(actor, ref weapon.Value, slot); + _funModule.ApplyFunToWeapon(actor, ref weapon, slot); } /// Update base data for a single changed equipment slot. private UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterArmor armor) { - bool FistWeaponGauntletHack() - { - if (slot is not EquipSlot.Hands) - return false; - - var offhand = actor.GetOffhand(); - return offhand.Variant == 0 && offhand.Set.Id != 0 && armor.Set.Id == offhand.Set.Id; - } - var actorArmor = actor.GetArmor(slot); var fistWeapon = FistWeaponGauntletHack(); @@ -359,12 +417,20 @@ public class StateListener : IDisposable if (actorArmor.Value != armor.Value) { // Update base data in case hat visibility is off. - if (slot is EquipSlot.Head && armor.Value == 0 && actorArmor.Value != state.BaseData.Armor(EquipSlot.Head).Value) + if (slot is EquipSlot.Head && armor.Value == 0) { - var item = _items.Identify(slot, actorArmor.Set, actorArmor.Variant); - state.BaseData.SetItem(EquipSlot.Head, item); - state.BaseData.SetStain(EquipSlot.Head, actorArmor.Stain); - return UpdateState.Change; + if (actor.IsTransformed) + return UpdateState.Transformed; + + if (actorArmor.Value != state.BaseData.Armor(EquipSlot.Head).Value) + { + var item = _items.Identify(slot, actorArmor.Set, actorArmor.Variant); + state.BaseData.SetItem(EquipSlot.Head, item); + state.BaseData.SetStain(EquipSlot.Head, actorArmor.Stains); + return UpdateState.Change; + } + + return UpdateState.HatHack; } if (!fistWeapon) @@ -372,21 +438,62 @@ public class StateListener : IDisposable } var baseData = state.BaseData.Armor(slot); - var change = UpdateState.NoChange; - if (baseData.Stain != armor.Stain) + + var change = UpdateState.NoChange; + if (baseData.Stains != armor.Stains) { - state.BaseData.SetStain(slot, armor.Stain); + if (_isPlayerNpc) + return UpdateState.Transformed; + + state.BaseData.SetStain(slot, armor.Stains); change = UpdateState.Change; } if (baseData.Set.Id != armor.Set.Id || baseData.Variant != armor.Variant && !fistWeapon) { + if (_isPlayerNpc) + return UpdateState.Transformed; + var item = _items.Identify(slot, armor.Set, armor.Variant); state.BaseData.SetItem(slot, item); change = UpdateState.Change; } return change; + + bool FistWeaponGauntletHack() + { + if (slot is not EquipSlot.Hands) + return false; + + var offhand = actor.GetOffhand(); + return offhand.Variant == 0 && offhand.Weapon.Id != 0 && armor.Set.Id == offhand.Skeleton.Id; + } + } + + private UpdateState UpdateBaseData(Actor actor, ActorState state, BonusItemFlag slot, CharacterArmor item) + { + var actorItemId = actor.GetBonusItem(slot); + if (!_items.IsBonusItemValid(slot, actorItemId, out var actorItem)) + return UpdateState.NoChange; + + // The actor item does not correspond to the model item, thus the actor is transformed. + if (actorItem.PrimaryId != item.Set || actorItem.Variant != item.Variant) + return UpdateState.Transformed; + + var baseData = state.BaseData.BonusItem(slot); + var change = UpdateState.NoChange; + if (baseData.Id != actorItem.Id || baseData.PrimaryId != item.Set || baseData.Variant != item.Variant) + { + if (_isPlayerNpc) + return UpdateState.Transformed; + + var identified = _items.Identify(slot, item.Set, item.Variant); + state.BaseData.SetBonusItem(slot, identified); + change = UpdateState.Change; + } + + return change; } /// Handle a full equip slot update for base data and model data. @@ -398,50 +505,110 @@ public class StateListener : IDisposable // Update model state if not on fixed design. case UpdateState.Change: var apply = false; - if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) - _manager.ChangeItem(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game); + if (!state.Sources[slot, false].IsFixed()) + _manager.ChangeItem(state, slot, state.BaseData.Item(slot), ApplySettings.Game); else apply = true; - if (state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc) - _manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game); + if (!state.Sources[slot, true].IsFixed()) + _manager.ChangeStains(state, slot, state.BaseData.Stain(slot), ApplySettings.Game); else apply = true; if (apply) armor = state.ModelData.ArmorWithState(slot); - break; // Use current model data. - // Transformed also handles invisible hat state. case UpdateState.NoChange: - case UpdateState.Transformed when slot is EquipSlot.Head && armor.Value is 0: + case UpdateState.HatHack: armor = state.ModelData.ArmorWithState(slot); break; case UpdateState.Transformed: break; } } - /// Update base data for a single changed weapon slot. - private UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterWeapon weapon) + private void OnCrestChange(Actor actor, CrestFlag slot, ref bool value) { + if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) + return; + + if (!actor.Identifier(_actors, out var identifier) + || !_manager.TryGetValue(identifier, out var state)) + return; + + switch (UpdateBaseCrest(actor, state, slot, value)) + { + case UpdateState.Change: + if (!state.Sources[slot].IsFixed()) + _manager.ChangeCrest(state, slot, state.BaseData.Crest(slot), ApplySettings.Game); + else + value = state.ModelData.Crest(slot); + break; + case UpdateState.NoChange: + case UpdateState.HatHack: + value = state.ModelData.Crest(slot); + break; + case UpdateState.Transformed: break; + } + } + + private void OnModelCrestSetup(Model model, CrestFlag slot, ref bool value) + { + var actor = _penumbra.GameObjectFromDrawObject(model); + if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) + return; + + if (!actor.Identifier(_actors, out var identifier) + || !_manager.TryGetValue(identifier, out var state)) + return; + + value = state.ModelData.Crest(slot); + } + + private static UpdateState UpdateBaseCrest(Actor actor, ActorState state, CrestFlag slot, bool visible) + { + if (actor.IsTransformed) + return UpdateState.Transformed; + + if (state.BaseData.Crest(slot) != visible) + { + state.BaseData.SetCrest(slot, visible); + return UpdateState.Change; + } + + return UpdateState.NoChange; + } + + /// Update base data for a single changed weapon slot. + private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterWeapon weapon) + { + if (actor.AsCharacter->CharacterData.TransformationId != 0) + { + var actorWeapon = slot is EquipSlot.MainHand ? actor.GetMainhand() : actor.GetOffhand(); + if (weapon.Value != actorWeapon.Value) + return UpdateState.Transformed; + } + var baseData = state.BaseData.Weapon(slot); var change = UpdateState.NoChange; // Fist weapon bug hack - if (slot is EquipSlot.OffHand && weapon.Value == 0 && actor.GetMainhand().Set.Id is > 1600 and < 1651) + if (slot is EquipSlot.OffHand && weapon.Value == 0 && actor.GetMainhand().Skeleton.Id is > 1600 and < 1651) return UpdateState.NoChange; - if (baseData.Stain != weapon.Stain) + if (baseData.Stains != weapon.Stains) { - state.BaseData.SetStain(slot, weapon.Stain); + state.BaseData.SetStain(slot, weapon.Stains); change = UpdateState.Change; } - if (baseData.Set.Id != weapon.Set.Id || baseData.Type.Id != weapon.Type.Id || baseData.Variant != weapon.Variant) + if (baseData.Skeleton.Id != weapon.Skeleton.Id || baseData.Weapon.Id != weapon.Weapon.Id || baseData.Variant != weapon.Variant) { - var item = _items.Identify(slot, weapon.Set, weapon.Type, weapon.Variant, - slot is EquipSlot.OffHand ? state.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown); + if (_isPlayerNpc) + return UpdateState.Transformed; + + var item = _items.Identify(slot, weapon.Skeleton, weapon.Weapon, weapon.Variant, + slot is EquipSlot.OffHand ? state.BaseData.MainhandType : FullEquipType.Unknown); state.BaseData.SetItem(slot, item); change = UpdateState.Change; } @@ -457,7 +624,7 @@ public class StateListener : IDisposable private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, uint modelId, nint customizeData, nint equipData) { // Model ID does not agree between game object and new draw object => Transformation. - if (modelId != (uint)actor.AsCharacter->CharacterData.ModelCharaId) + if (modelId != (uint)actor.AsCharacter->ModelContainer.ModelCharaId) return UpdateState.Transformed; // Model ID did not change to stored state. @@ -470,7 +637,7 @@ public class StateListener : IDisposable if (isHuman) state.BaseData = _manager.FromActor(actor, false, false); else - state.BaseData.LoadNonHuman(modelId, *(Customize*)customizeData, equipData); + state.BaseData.LoadNonHuman(modelId, *(CustomizeArray*)customizeData, equipData); return UpdateState.Change; } @@ -481,10 +648,14 @@ public class StateListener : IDisposable /// only if we kept track of state of someone who went to the aesthetician, /// or if they used other tools to change things. /// - private UpdateState UpdateBaseData(Actor actor, ActorState state, Customize customize, bool checkTransform) + private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, CustomizeArray customize, bool checkTransform) { // Customize array does not agree between game object and draw object => transformation. - if (checkTransform && !actor.GetCustomize().Equals(customize)) + if (checkTransform && !actor.Customize->Equals(customize)) + return UpdateState.Transformed; + + // Check for player NPCs with a different game state. + if (_isPlayerNpc && !actor.Customize->Equals(state.BaseData.Customize)) return UpdateState.Transformed; // Customize array did not change to stored state. @@ -492,25 +663,34 @@ public class StateListener : IDisposable return UpdateState.NoChange; // TODO: handle wrong base data. // Update customize base state. - state.BaseData.Customize.Load(customize); + state.BaseData.Customize = customize; return UpdateState.Change; } /// Handle visor state changes made by the game. - private void OnVisorChange(Model model, Ref value) + private unsafe void OnVisorChange(Model model, bool game, ref bool value) { // Skip updates when in customize update. - if (ChangeCustomizeService.InUpdate.IsValueCreated && ChangeCustomizeService.InUpdate.Value) + if (ChangeCustomizeService.InUpdate.InMethod) return; // Find appropriate actor and state. // We do not need to handle fixed designs, // since a fixed design would already have established state-tracking. var actor = _penumbra.GameObjectFromDrawObject(model); + if (!actor.IsCharacter) + return; + + // Only actually change anything if the actor state changed, + // when equipping headgear the method is called with the current draw object state, + // which corrupts Glamourer's assumed game state otherwise. + if (!game && actor.AsCharacter->DrawData.IsVisorToggled != value) + return; + if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) return; - if (!actor.Identifier(_actors.AwaitedService, out var identifier)) + if (!actor.Identifier(_actors, out var identifier)) return; if (!_manager.TryGetValue(identifier, out var state)) @@ -521,20 +701,58 @@ public class StateListener : IDisposable { // if base state changed, either overwrite the actual value if we have fixed values, // or overwrite the stored model state with the new one. - if (state[ActorState.MetaIndex.VisorState] is StateChanged.Source.Fixed or StateChanged.Source.Ipc) - value.Value = state.ModelData.IsVisorToggled(); + if (state.Sources[MetaIndex.VisorState].IsFixed()) + value = state.ModelData.IsVisorToggled(); else - _manager.ChangeVisorState(state, value, StateChanged.Source.Game); + _manager.ChangeMetaState(state, MetaIndex.VisorState, value, ApplySettings.Game); } else { // if base state did not change, overwrite the value with the model state one. - value.Value = state.ModelData.IsVisorToggled(); + value = state.ModelData.IsVisorToggled(); + } + } + + /// Handle visor state changes made by the game. + private void OnVieraEarChange(Actor actor, ref bool value) + { + // Value is inverted compared to our own handling. + + // Skip updates when in customize update. + if (ChangeCustomizeService.InUpdate.InMethod) + return; + + if (!actor.IsCharacter) + return; + + if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) + return; + + if (!actor.Identifier(_actors, out var identifier)) + return; + + if (!_manager.TryGetValue(identifier, out var state)) + return; + + // Update visor base state. + if (state.BaseData.SetEarsVisible(!value)) + { + // if base state changed, either overwrite the actual value if we have fixed values, + // or overwrite the stored model state with the new one. + if (state.Sources[MetaIndex.EarState].IsFixed()) + value = !state.ModelData.AreEarsVisible(); + else + _manager.ChangeMetaState(state, MetaIndex.EarState, !value, ApplySettings.Game); + } + else + { + // if base state did not change, overwrite the value with the model state one. + value = !state.ModelData.AreEarsVisible(); } } /// Handle Hat Visibility changes. These act on the game object. - private void OnHeadGearVisibilityChange(Actor actor, Ref value) + private void OnHeadGearVisibilityChange(Actor actor, ref bool value) { if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) return; @@ -543,7 +761,7 @@ public class StateListener : IDisposable // We do not need to handle fixed designs, // if there is no model that caused a fixed design to exist yet, // we also do not care about the invisible model. - if (!actor.Identifier(_actors.AwaitedService, out var identifier)) + if (!actor.Identifier(_actors, out var identifier)) return; if (!_manager.TryGetValue(identifier, out var state)) @@ -554,20 +772,20 @@ public class StateListener : IDisposable { // if base state changed, either overwrite the actual value if we have fixed values, // or overwrite the stored model state with the new one. - if (state[ActorState.MetaIndex.HatState] is StateChanged.Source.Fixed or StateChanged.Source.Ipc) - value.Value = state.ModelData.IsHatVisible(); + if (state.Sources[MetaIndex.HatState].IsFixed()) + value = state.ModelData.IsHatVisible(); else - _manager.ChangeHatState(state, value, StateChanged.Source.Game); + _manager.ChangeMetaState(state, MetaIndex.HatState, value, ApplySettings.Game); } else { // if base state did not change, overwrite the value with the model state one. - value.Value = state.ModelData.IsHatVisible(); + value = state.ModelData.IsHatVisible(); } } /// Handle Weapon Visibility changes. These act on the game object. - private void OnWeaponVisibilityChange(Actor actor, Ref value) + private void OnWeaponVisibilityChange(Actor actor, ref bool value) { if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) return; @@ -576,7 +794,7 @@ public class StateListener : IDisposable // We do not need to handle fixed designs, // if there is no model that caused a fixed design to exist yet, // we also do not care about the invisible model. - if (!actor.Identifier(_actors.AwaitedService, out var identifier)) + if (!actor.Identifier(_actors, out var identifier)) return; if (!_manager.TryGetValue(identifier, out var state)) @@ -587,15 +805,15 @@ public class StateListener : IDisposable { // if base state changed, either overwrite the actual value if we have fixed values, // or overwrite the stored model state with the new one. - if (state[ActorState.MetaIndex.WeaponState] is StateChanged.Source.Fixed or StateChanged.Source.Ipc) - value.Value = state.ModelData.IsWeaponVisible(); + if (state.Sources[MetaIndex.WeaponState].IsFixed()) + value = state.ModelData.IsWeaponVisible(); else - _manager.ChangeWeaponState(state, value, StateChanged.Source.Game); + _manager.ChangeMetaState(state, MetaIndex.WeaponState, value, ApplySettings.Game); } else { // if base state did not change, overwrite the value with the model state one. - value.Value = state.ModelData.IsWeaponVisible(); + value = state.ModelData.IsWeaponVisible(); } } @@ -619,29 +837,41 @@ public class StateListener : IDisposable { _penumbra.CreatingCharacterBase += OnCreatingCharacterBase; _penumbra.CreatedCharacterBase += OnCreatedCharacterBase; - _slotUpdating.Subscribe(OnSlotUpdating, SlotUpdating.Priority.StateListener); + _equipSlotUpdating.Subscribe(OnEquipSlotUpdating, EquipSlotUpdating.Priority.StateListener); + _bonusSlotUpdating.Subscribe(OnBonusSlotUpdating, BonusSlotUpdating.Priority.StateListener); + _gearsetDataLoaded.Subscribe(OnGearsetDataLoaded, GearsetDataLoaded.Priority.StateListener); _movedEquipment.Subscribe(OnMovedEquipment, MovedEquipment.Priority.StateListener); _weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener); _visorState.Subscribe(OnVisorChange, VisorStateChanged.Priority.StateListener); + _vieraEarState.Subscribe(OnVieraEarChange, VieraEarStateChanged.Priority.StateListener); _headGearVisibility.Subscribe(OnHeadGearVisibilityChange, HeadGearVisibilityChanged.Priority.StateListener); _weaponVisibility.Subscribe(OnWeaponVisibilityChange, WeaponVisibilityChanged.Priority.StateListener); _changeCustomizeService.Subscribe(OnCustomizeChange, ChangeCustomizeService.Priority.StateListener); + _crestService.Subscribe(OnCrestChange, CrestService.Priority.StateListener); + _crestService.ModelCrestSetup += OnModelCrestSetup; + _changeCustomizeService.Subscribe(OnCustomizeChanged, ChangeCustomizeService.Post.Priority.StateListener); } private void Unsubscribe() { _penumbra.CreatingCharacterBase -= OnCreatingCharacterBase; _penumbra.CreatedCharacterBase -= OnCreatedCharacterBase; - _slotUpdating.Unsubscribe(OnSlotUpdating); + _equipSlotUpdating.Unsubscribe(OnEquipSlotUpdating); + _bonusSlotUpdating.Unsubscribe(OnBonusSlotUpdating); + _gearsetDataLoaded.Unsubscribe(OnGearsetDataLoaded); _movedEquipment.Unsubscribe(OnMovedEquipment); _weaponLoading.Unsubscribe(OnWeaponLoading); _visorState.Unsubscribe(OnVisorChange); + _vieraEarState.Unsubscribe(OnVieraEarChange); _headGearVisibility.Unsubscribe(OnHeadGearVisibilityChange); _weaponVisibility.Unsubscribe(OnWeaponVisibilityChange); _changeCustomizeService.Unsubscribe(OnCustomizeChange); + _crestService.Unsubscribe(OnCrestChange); + _crestService.ModelCrestSetup -= OnModelCrestSetup; + _changeCustomizeService.Unsubscribe(OnCustomizeChanged); } - private void OnCreatedCharacterBase(nint gameObject, string _, nint drawObject) + private void OnCreatedCharacterBase(nint gameObject, Guid _, nint drawObject) { if (_condition[ConditionFlag.CreatingCharacter]) return; @@ -649,8 +879,77 @@ public class StateListener : IDisposable if (_creatingState == null) return; - _applier.ChangeHatState(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsHatVisible()); - _applier.ChangeWeaponState(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsWeaponVisible()); - _applier.ChangeWetness(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsWet()); + var data = new ActorData(gameObject, _creatingIdentifier.ToName()); + _applier.ChangeMetaState(data, MetaIndex.HatState, _creatingState.ModelData.IsHatVisible()); + _applier.ChangeMetaState(data, MetaIndex.Wetness, _creatingState.ModelData.IsWet()); + _applier.ChangeMetaState(data, MetaIndex.WeaponState, _creatingState.ModelData.IsWeaponVisible()); + + ApplyParameters(_creatingState, drawObject); + } + + private void OnCustomizeChanged(Model model) + { + if (_customizeState == null) + { + var actor = _penumbra.GameObjectFromDrawObject(model); + if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) + return; + + if (!actor.Identifier(_actors, out var identifier) + || !_manager.TryGetValue(identifier, out _customizeState)) + return; + } + + ApplyParameters(_customizeState, model); + _customizeState = null; + } + + private void ApplyParameters(ActorState state, Model model) + { + if (!model.IsHuman) + return; + + var data = model.GetParameterData(); + foreach (var flag in CustomizeParameterExtensions.AllFlags) + { + var newValue = data[flag]; + switch (state.Sources[flag]) + { + case StateSource.Game: + if (state.BaseData.Parameters.Set(flag, newValue)) + _manager.ChangeCustomizeParameter(state, flag, newValue, ApplySettings.Game); + break; + case StateSource.Manual: + if (state.BaseData.Parameters.Set(flag, newValue)) + _manager.ChangeCustomizeParameter(state, flag, newValue, ApplySettings.Game); + else + model.ApplySingleParameterData(flag, state.ModelData.Parameters); + break; + case StateSource.IpcManual: + if (state.BaseData.Parameters.Set(flag, newValue)) + _manager.ChangeCustomizeParameter(state, flag, newValue, ApplySettings.Game); + else + model.ApplySingleParameterData(flag, state.ModelData.Parameters); + break; + case StateSource.Fixed: + state.BaseData.Parameters.Set(flag, newValue); + model.ApplySingleParameterData(flag, state.ModelData.Parameters); + break; + case StateSource.IpcFixed: + state.BaseData.Parameters.Set(flag, newValue); + model.ApplySingleParameterData(flag, state.ModelData.Parameters); + break; + case StateSource.Pending: + state.BaseData.Parameters.Set(flag, newValue); + state.Sources[flag] = StateSource.Manual; + model.ApplySingleParameterData(flag, state.ModelData.Parameters); + break; + case StateSource.IpcPending: + state.BaseData.Parameters.Set(flag, newValue); + state.Sources[flag] = StateSource.IpcManual; + model.ApplySingleParameterData(flag, state.ModelData.Parameters); + break; + } + } } } diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 4a85112..e8926d6 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -1,47 +1,40 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Dalamud.Plugin.Services; -using Glamourer.Customization; +using Dalamud.Plugin.Services; +using Glamourer.Api.Enums; using Glamourer.Designs; +using Glamourer.Designs.Links; using Glamourer.Events; +using Glamourer.GameData; using Glamourer.Interop; +using Glamourer.Interop.Material; +using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; using Penumbra.GameData.Actors; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.GameData.Interop; namespace Glamourer.State; -public class StateManager : IReadOnlyDictionary +public sealed class StateManager( + ActorManager actors, + ItemManager items, + StateChanged changeEvent, + StateFinalized finalizeEvent, + StateApplier applier, + InternalStateEditor editor, + HumanModelList humans, + IClientState clientState, + Configuration config, + JobChangeState jobChange, + DesignMerger merger, + ModSettingApplier modApplier, + GPoseService gPose) + : StateEditor(editor, applier, changeEvent, finalizeEvent, jobChange, config, items, merger, modApplier, gPose), + IReadOnlyDictionary { - private readonly ActorService _actors; - private readonly ItemManager _items; - private readonly HumanModelList _humans; - private readonly StateChanged _event; - private readonly StateApplier _applier; - private readonly StateEditor _editor; - private readonly ICondition _condition; - private readonly IClientState _clientState; - - private readonly Dictionary _states = new(); - - public StateManager(ActorService actors, ItemManager items, StateChanged @event, StateApplier applier, StateEditor editor, - HumanModelList humans, ICondition condition, IClientState clientState) - { - _actors = actors; - _items = items; - _event = @event; - _applier = applier; - _editor = editor; - _humans = humans; - _condition = condition; - _clientState = clientState; - } + private readonly Dictionary _states = []; public IEnumerator> GetEnumerator() => _states.GetEnumerator(); @@ -69,7 +62,7 @@ public class StateManager : IReadOnlyDictionary /// public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state) - => GetOrCreate(actor.GetIdentifier(_actors.AwaitedService), actor, out state); + => GetOrCreate(actor.GetIdentifier(actors), actor, out state); /// Try to obtain or create a new state for an existing actor. Returns false if no state could be created. public unsafe bool GetOrCreate(ActorIdentifier identifier, Actor actor, [NotNullWhen(true)] out ActorState? state) @@ -77,16 +70,19 @@ public class StateManager : IReadOnlyDictionary if (TryGetValue(identifier, out state)) return true; + if (!actor.Valid) + return false; + try { // Initial Creation, use the actors data for the base data, // and the draw objects data for the model data (where possible). state = new ActorState(identifier) { - ModelData = FromActor(actor, true, false), + ModelData = FromActor(actor, true, false), BaseData = FromActor(actor, false, false), LastJob = (byte)(actor.IsCharacter ? actor.AsCharacter->CharacterData.ClassJob : 0), - LastTerritory = _clientState.TerritoryType, + LastTerritory = clientState.TerritoryType, }; // state.Identifier is owned. _states.Add(state.Identifier, state); @@ -110,7 +106,7 @@ public class StateManager : IReadOnlyDictionary // If the given actor is not a character, just return a default character. if (!actor.IsCharacter) { - ret.SetDefaultEquipment(_items); + ret.SetDefaultEquipment(Items); return ret; } @@ -119,14 +115,14 @@ public class StateManager : IReadOnlyDictionary // Model ID is only unambiguously contained in the game object. // The draw object only has the object type. // TODO reverse search model data to get model id from model. - if (!_humans.IsHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId)) + if (!humans.IsHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId)) { - ret.LoadNonHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData, - (nint)(&actor.AsCharacter->DrawData.Head)); + ret.LoadNonHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId, *(CustomizeArray*)&actor.AsCharacter->DrawData.CustomizeData, + (nint)Unsafe.AsPointer(ref actor.AsCharacter->DrawData.EquipmentModelIds[0])); return ret; } - ret.ModelId = (uint)actor.AsCharacter->CharacterData.ModelCharaId; + ret.ModelId = (uint)actor.AsCharacter->ModelContainer.ModelCharaId; ret.IsHuman = true; CharacterWeapon main; @@ -144,17 +140,17 @@ public class StateManager : IReadOnlyDictionary // We can not use the head slot data from the draw object if the hat is hidden. var head = ret.IsHatVisible() || ignoreHatState ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head); - var headItem = _items.Identify(EquipSlot.Head, head.Set, head.Variant); + var headItem = Items.Identify(EquipSlot.Head, head.Set, head.Variant); ret.SetItem(EquipSlot.Head, headItem); - ret.SetStain(EquipSlot.Head, head.Stain); + ret.SetStain(EquipSlot.Head, head.Stains); // The other slots can be used from the draw object. foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1)) { var armor = model.GetArmor(slot); - var item = _items.Identify(slot, armor.Set, armor.Variant); + var item = Items.Identify(slot, armor.Set, armor.Variant); ret.SetItem(slot, item); - ret.SetStain(slot, armor.Stain); + ret.SetStain(slot, armor.Stains); } // Weapons use the draw objects of the weapons, but require the game object either way. @@ -162,367 +158,387 @@ public class StateManager : IReadOnlyDictionary // Visor state is a flag on the game object, but we can see the actual state on the draw object. ret.SetVisor(VisorService.GetVisorState(model)); + ret.SetEarsVisible(model.VieraEarsVisible); + + foreach (var slot in CrestExtensions.AllRelevantSet) + ret.SetCrest(slot, CrestService.GetModelCrest(actor, slot)); + + foreach (var slot in BonusExtensions.AllFlags) + { + var data = model.GetBonus(slot); + var item = Items.Identify(slot, data.Set, data.Variant); + ret.SetBonusItem(slot, item); + } } else { // Obtain all data from the game object. - ret.Customize = actor.GetCustomize(); + ret.Customize = *actor.Customize; foreach (var slot in EquipSlotExtensions.EqdpSlots) { var armor = actor.GetArmor(slot); - var item = _items.Identify(slot, armor.Set, armor.Variant); + var item = Items.Identify(slot, armor.Set, armor.Variant); ret.SetItem(slot, item); - ret.SetStain(slot, armor.Stain); + ret.SetStain(slot, armor.Stains); } main = actor.GetMainhand(); off = actor.GetOffhand(); FistWeaponHack(ref ret, ref main, ref off); ret.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled); + ret.SetEarsVisible(actor.ShowVieraEars); + foreach (var slot in CrestExtensions.AllRelevantSet) + ret.SetCrest(slot, actor.GetCrest(slot)); + + foreach (var slot in BonusExtensions.AllFlags) + { + var id = actor.GetBonusItem(slot); + var item = Items.Resolve(slot, id); + ret.SetBonusItem(slot, item); + } } // Set the weapons regardless of source. - var mainItem = _items.Identify(EquipSlot.MainHand, main.Set, main.Type, main.Variant); - var offItem = _items.Identify(EquipSlot.OffHand, off.Set, off.Type, off.Variant, mainItem.Type); + var mainItem = Items.Identify(EquipSlot.MainHand, main.Skeleton, main.Weapon, main.Variant); + var offItem = Items.Identify(EquipSlot.OffHand, off.Skeleton, off.Weapon, off.Variant, mainItem.Type); ret.SetItem(EquipSlot.MainHand, mainItem); - ret.SetStain(EquipSlot.MainHand, main.Stain); + ret.SetStain(EquipSlot.MainHand, main.Stains); ret.SetItem(EquipSlot.OffHand, offItem); - ret.SetStain(EquipSlot.OffHand, off.Stain); + ret.SetStain(EquipSlot.OffHand, off.Stains); // Wetness can technically only be set in GPose or via external tools. // It is only available in the game object. - ret.SetIsWet(actor.AsCharacter->IsGPoseWet); + ret.SetIsWet(actor.IsGPoseWet); // Weapon visibility could technically be inferred from the weapon draw objects, // but since we use hat visibility from the game object we can also use weapon visibility from it. ret.SetWeaponVisible(!actor.AsCharacter->DrawData.IsWeaponHidden); + ret.Parameters = model.GetParameterData(); + return ret; } /// This is hardcoded in the game. private void FistWeaponHack(ref DesignData ret, ref CharacterWeapon mainhand, ref CharacterWeapon offhand) { - if (mainhand.Set.Id is < 1601 or >= 1651) + if (mainhand.Skeleton.Id is < 1601 or >= 1651) return; - var gauntlets = _items.Identify(EquipSlot.Hands, offhand.Set, (Variant) offhand.Type.Id); - offhand.Set = (SetId)(mainhand.Set.Id + 50); - offhand.Variant = mainhand.Variant; - offhand.Type = mainhand.Type; + var gauntlets = Items.Identify(EquipSlot.Hands, offhand.Skeleton, (Variant)offhand.Weapon.Id); + offhand.Skeleton = (PrimaryId)(mainhand.Skeleton.Id + 50); + offhand.Variant = mainhand.Variant; + offhand.Weapon = mainhand.Weapon; ret.SetItem(EquipSlot.Hands, gauntlets); - ret.SetStain(EquipSlot.Hands, mainhand.Stain); + ret.SetStain(EquipSlot.Hands, mainhand.Stains); } - #region Change Values - /// Turn an actor human. - public void TurnHuman(ActorState state, StateChanged.Source source, uint key = 0) - => ChangeModelId(state, 0, Customize.Default, nint.Zero, source, key); + public void TurnHuman(ActorState state, StateSource source, uint key = 0) + => ChangeModelId(state, 0, CustomizeArray.Default, nint.Zero, source, key); - /// Turn an actor to. - public void ChangeModelId(ActorState state, uint modelId, Customize customize, nint equipData, StateChanged.Source source, - uint key = 0) - { - if (!_editor.ChangeModelId(state, modelId, customize, equipData, source, out var old, key)) - return; - - var actors = _applier.ForceRedraw(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set model id in state {state.Identifier.Incognito(null)} from {old} to {modelId}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Model, source, state, actors, (old, modelId)); - } - - /// Change a customization value. - public void ChangeCustomize(ActorState state, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeCustomize(state, idx, value, source, out var old, key)) - return; - - var actors = _applier.ChangeCustomize(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set {idx.ToDefaultName()} customizations in state {state.Identifier.Incognito(null)} from {old.Value} to {value.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Customize, source, state, actors, (old, value, idx)); - } - - /// Change an entire customization array according to flags. - public void ChangeCustomize(ActorState state, in Customize customizeInput, CustomizeFlag apply, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeHumanCustomize(state, customizeInput, apply, source, out var old, out var applied, key)) - return; - - var actors = _applier.ChangeCustomize(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set {applied} customizations in state {state.Identifier.Incognito(null)} from {old} to {customizeInput}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.EntireCustomize, source, state, actors, (old, applied)); - } - - /// Change a single piece of equipment without stain. - /// Do not use this in the same frame as ChangeStain, use instead. - public void ChangeItem(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeItem(state, slot, item, source, out var old, key)) - return; - - var type = slot.ToIndex() < 10 ? StateChanged.Type.Equip : StateChanged.Type.Weapon; - var actors = type is StateChanged.Type.Equip - ? _applier.ChangeArmor(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc) - : _applier.ChangeWeapon(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc, - item.Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType)); - Glamourer.Log.Verbose( - $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}). [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(type, source, state, actors, (old, item, slot)); - } - - /// Change a single piece of equipment including stain. - public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeEquip(state, slot, item, stain, source, out var old, out var oldStain, key)) - return; - - var type = slot.ToIndex() < 10 ? StateChanged.Type.Equip : StateChanged.Type.Weapon; - var actors = type is StateChanged.Type.Equip - ? _applier.ChangeArmor(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc) - : _applier.ChangeWeapon(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc, - item.Type != (slot is EquipSlot.MainHand ? state.BaseData.MainhandType : state.BaseData.OffhandType)); - Glamourer.Log.Verbose( - $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}) and its stain from {oldStain.Id} to {stain.Id}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(type, source, state, actors, (old, item, slot)); - _event.Invoke(StateChanged.Type.Stain, source, state, actors, (oldStain, stain, slot)); - } - - /// Change only the stain of an equipment piece. - /// Do not use this in the same frame as ChangeEquip, use instead. - public void ChangeStain(ActorState state, EquipSlot slot, StainId stain, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeStain(state, slot, stain, source, out var old, key)) - return; - - var actors = _applier.ChangeStain(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set {slot.ToName()} stain in state {state.Identifier.Incognito(null)} from {old.Id} to {stain.Id}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Stain, source, state, actors, (old, stain, slot)); - } - - /// Change hat visibility. - public void ChangeHatState(ActorState state, bool value, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeMetaState(state, ActorState.MetaIndex.HatState, value, source, out var old, key)) - return; - - var actors = _applier.ChangeHatState(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set Head Gear Visibility in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, source, state, actors, (old, value, ActorState.MetaIndex.HatState)); - } - - /// Change weapon visibility. - public void ChangeWeaponState(ActorState state, bool value, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeMetaState(state, ActorState.MetaIndex.WeaponState, value, source, out var old, key)) - return; - - var actors = _applier.ChangeWeaponState(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set Weapon Visibility in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, source, state, actors, (old, value, ActorState.MetaIndex.WeaponState)); - } - - /// Change visor state. - public void ChangeVisorState(ActorState state, bool value, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeMetaState(state, ActorState.MetaIndex.VisorState, value, source, out var old, key)) - return; - - var actors = _applier.ChangeVisor(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc); - Glamourer.Log.Verbose( - $"Set Visor State in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, source, state, actors, (old, value, ActorState.MetaIndex.VisorState)); - } - - /// Set GPose Wetness. - public void ChangeWetness(ActorState state, bool value, StateChanged.Source source, uint key = 0) - { - if (!_editor.ChangeMetaState(state, ActorState.MetaIndex.Wetness, value, source, out var old, key)) - return; - - var actors = _applier.ChangeWetness(state, true); - Glamourer.Log.Verbose( - $"Set Wetness in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Other, state[ActorState.MetaIndex.Wetness], state, actors, (old, value, ActorState.MetaIndex.Wetness)); - } - - #endregion - - public void ApplyDesign(DesignBase design, ActorState state, StateChanged.Source source, uint key = 0) - { - void HandleEquip(EquipSlot slot, bool applyPiece, bool applyStain) - { - var unused = (applyPiece, applyStain) switch - { - (false, false) => false, - (true, false) => _editor.ChangeItem(state, slot, design.DesignData.Item(slot), source, out _, key), - (false, true) => _editor.ChangeStain(state, slot, design.DesignData.Stain(slot), source, out _, key), - (true, true) => _editor.ChangeEquip(state, slot, design.DesignData.Item(slot), design.DesignData.Stain(slot), source, out _, - out _, key), - }; - } - - if (!_editor.ChangeModelId(state, design.DesignData.ModelId, design.DesignData.Customize, design.DesignData.GetEquipmentPtr(), source, - out var oldModelId, key)) - return; - - var redraw = oldModelId != design.DesignData.ModelId || !design.DesignData.IsHuman; - if (design.DoApplyWetness()) - _editor.ChangeMetaState(state, ActorState.MetaIndex.Wetness, design.DesignData.IsWet(), source, out _, key); - - if (state.ModelData.IsHuman) - { - if (design.DoApplyHatVisible()) - _editor.ChangeMetaState(state, ActorState.MetaIndex.HatState, design.DesignData.IsHatVisible(), source, out _, key); - if (design.DoApplyWeaponVisible()) - _editor.ChangeMetaState(state, ActorState.MetaIndex.WeaponState, design.DesignData.IsWeaponVisible(), source, out _, key); - if (design.DoApplyVisorToggle()) - _editor.ChangeMetaState(state, ActorState.MetaIndex.VisorState, design.DesignData.IsVisorToggled(), source, out _, key); - - var flags = state.AllowsRedraw(_condition) - ? design.ApplyCustomize - : design.ApplyCustomize & ~CustomizeFlagExtensions.RedrawRequired; - _editor.ChangeHumanCustomize(state, design.DesignData.Customize, flags, source, out _, out var applied, key); - redraw |= applied.RequiresRedraw(); - - foreach (var slot in EquipSlotExtensions.FullSlots) - HandleEquip(slot, design.DoApplyEquip(slot), design.DoApplyStain(slot)); - } - - var actors = ApplyAll(state, redraw, false); - Glamourer.Log.Verbose( - $"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Design, state[ActorState.MetaIndex.Wetness], state, actors, design); - } - - private ActorData ApplyAll(ActorState state, bool redraw, bool withLock) - { - var actors = _applier.ChangeWetness(state, true); - if (redraw) - { - if (withLock) - state.TempLock(); - _applier.ForceRedraw(actors); - } - else - { - _applier.ChangeCustomize(actors, state.ModelData.Customize); - foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - _applier.ChangeArmor(actors, slot, state.ModelData.Armor(slot), state[slot, false] is not StateChanged.Source.Ipc, - state.ModelData.IsHatVisible()); - } - - var mainhandActors = state.ModelData.MainhandType != state.BaseData.MainhandType ? actors.OnlyGPose() : actors; - _applier.ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand)); - var offhandActors = state.ModelData.OffhandType != state.BaseData.OffhandType ? actors.OnlyGPose() : actors; - _applier.ChangeOffhand(offhandActors, state.ModelData.Item(EquipSlot.OffHand), state.ModelData.Stain(EquipSlot.OffHand)); - } - - if (state.ModelData.IsHuman) - { - _applier.ChangeHatState(actors, state.ModelData.IsHatVisible()); - _applier.ChangeWeaponState(actors, state.ModelData.IsWeaponVisible()); - _applier.ChangeVisor(actors, state.ModelData.IsVisorToggled()); - } - - return actors; - } - - public void ResetState(ActorState state, StateChanged.Source source, uint key = 0) + public void ResetState(ActorState state, StateSource source, uint key = 0, bool isFinal = false) { if (!state.Unlock(key)) return; var redraw = state.ModelData.ModelId != state.BaseData.ModelId || !state.ModelData.IsHuman - || Customize.Compare(state.ModelData.Customize, state.BaseData.Customize).RequiresRedraw(); + || CustomizeArray.Compare(state.ModelData.Customize, state.BaseData.Customize).RequiresRedraw(); + state.ModelData = state.BaseData; state.ModelData.SetIsWet(false); foreach (var index in Enum.GetValues()) - state[index] = StateChanged.Source.Game; + state.Sources[index] = StateSource.Game; foreach (var slot in EquipSlotExtensions.FullSlots) { - state[slot, true] = StateChanged.Source.Game; - state[slot, false] = StateChanged.Source.Game; + state.Sources[slot, true] = StateSource.Game; + state.Sources[slot, false] = StateSource.Game; } - foreach (var type in Enum.GetValues()) - state[type] = StateChanged.Source.Game; + foreach (var slot in BonusExtensions.AllFlags) + state.Sources[slot] = StateSource.Game; + + foreach (var type in Enum.GetValues()) + state.Sources[type] = StateSource.Game; + + foreach (var slot in CrestExtensions.AllRelevantSet) + state.Sources[slot] = StateSource.Game; + + foreach (var flag in CustomizeParameterExtensions.AllFlags) + state.Sources[flag] = StateSource.Game; + + state.Materials.Clear(); + + var objects = ActorData.Invalid; + if (source is not StateSource.Game) + objects = Applier.ApplyAll(state, redraw, true); - var actors = ActorData.Invalid; - if (source is StateChanged.Source.Manual or StateChanged.Source.Ipc) - actors = ApplyAll(state, redraw, true); Glamourer.Log.Verbose( - $"Reset entire state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); - _event.Invoke(StateChanged.Type.Reset, StateChanged.Source.Manual, state, actors, null); + $"Reset entire state of {state.Identifier.Incognito(null)} to game base. [Affecting {objects.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Reset, source, state, objects, null); + // only invoke if we define this reset call as the final call in our state update. + if (isFinal) + StateFinalized.Invoke(StateFinalizationType.Revert, objects); } - public void ResetStateFixed(ActorState state, uint key = 0) + public void ResetAdvancedDyes(ActorState state, StateSource source, uint key = 0) + { + if (!state.Unlock(key) || !state.ModelData.IsHuman) + return; + + state.ModelData.Parameters = state.BaseData.Parameters; + + foreach (var flag in CustomizeParameterExtensions.AllFlags) + state.Sources[flag] = StateSource.Game; + + var objects = Applier.GetData(state); + if (source is not StateSource.Game) + foreach (var (idx, mat) in state.Materials.Values) + Applier.ChangeMaterialValue(state, objects, MaterialValueIndex.FromKey(idx), mat.Game); + + state.Materials.Clear(); + + Glamourer.Log.Verbose( + $"Reset advanced dye state of {state.Identifier.Incognito(null)} to game base. [Affecting {objects.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Reset, source, state, objects, null); + // Update that we have completed a full operation. (We can do this directly as nothing else is linked) + StateFinalized.Invoke(StateFinalizationType.RevertAdvanced, objects); + } + + public void ResetAdvancedCustomizations(ActorState state, StateSource source, uint key = 0) + { + if (!state.Unlock(key) || !state.ModelData.IsHuman) + return; + + state.ModelData.Parameters = state.BaseData.Parameters; + + foreach (var flag in CustomizeParameterExtensions.AllFlags) + state.Sources[flag] = StateSource.Game; + + var objects = ActorData.Invalid; + if (source is not StateSource.Game) + objects = Applier.ChangeParameters(state, CustomizeParameterExtensions.All, true); + + state.Materials.Clear(); + + Glamourer.Log.Verbose( + $"Reset advanced customization and dye state of {state.Identifier.Incognito(null)} to game base. [Affecting {objects.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Reset, source, state, objects, null); + // Update that we have completed a full operation. (We can do this directly as nothing else is linked) + StateFinalized.Invoke(StateFinalizationType.RevertAdvanced, objects); + } + + public void ResetAdvancedState(ActorState state, StateSource source, uint key = 0) + { + if (!state.Unlock(key) || !state.ModelData.IsHuman) + return; + + state.ModelData.Parameters = state.BaseData.Parameters; + + foreach (var flag in CustomizeParameterExtensions.AllFlags) + state.Sources[flag] = StateSource.Game; + + var actors = ActorData.Invalid; + if (source is not StateSource.Game) + { + actors = Applier.ChangeParameters(state, CustomizeParameterExtensions.All, true); + foreach (var (idx, mat) in state.Materials.Values) + Applier.ChangeMaterialValue(state, actors, MaterialValueIndex.FromKey(idx), mat.Game); + } + + state.Materials.Clear(); + + Glamourer.Log.Verbose( + $"Reset advanced customization and dye state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); + StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null); + // Update that we have completed a full operation. (We can do this directly as nothing else is linked) + StateFinalized.Invoke(StateFinalizationType.RevertAdvanced, actors); + } + + public void ResetCustomize(ActorState state, StateSource source, uint key = 0) + { + if (!state.Unlock(key) || !state.ModelData.IsHuman) + return; + + foreach (var flag in CustomizationExtensions.All) + state.Sources[flag] = StateSource.Game; + + state.ModelData.ModelId = state.BaseData.ModelId; + state.ModelData.Customize = state.BaseData.Customize; + var actors = ActorData.Invalid; + if (source is not StateSource.Game) + actors = Applier.ChangeCustomize(state, true); + Glamourer.Log.Verbose( + $"Reset customization state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); + // Update that we have completed a full operation. (We can do this directly as nothing else is linked) + StateFinalized.Invoke(StateFinalizationType.RevertCustomize, actors); + } + + public void ResetEquip(ActorState state, StateSource source, uint key = 0) { if (!state.Unlock(key)) return; - foreach (var index in Enum.GetValues().Where(i => state[i] is StateChanged.Source.Fixed)) + foreach (var slot in EquipSlotExtensions.FullSlots) { - state[index] = StateChanged.Source.Game; + state.Sources[slot, true] = StateSource.Game; + state.Sources[slot, false] = StateSource.Game; + if (source is not StateSource.Game) + { + state.ModelData.SetItem(slot, state.BaseData.Item(slot)); + state.ModelData.SetStain(slot, state.BaseData.Stain(slot)); + } + } + + foreach (var slot in BonusExtensions.AllFlags) + { + state.Sources[slot] = StateSource.Game; + if (source is not StateSource.Game) + state.ModelData.SetBonusItem(slot, state.BaseData.BonusItem(slot)); + } + + var actors = ActorData.Invalid; + if (source is not StateSource.Game) + { + actors = Applier.ChangeArmor(state, EquipSlotExtensions.EqdpSlots[0], true); + foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1)) + { + Applier.ChangeArmor(actors, slot, state.ModelData.Armor(slot), !state.Sources[slot, false].IsIpc(), + state.ModelData.IsHatVisible()); + } + + foreach (var slot in BonusExtensions.AllFlags) + { + var item = state.ModelData.BonusItem(slot); + Applier.ChangeBonusItem(actors, slot, item.PrimaryId, item.Variant); + } + + var mainhandActors = state.ModelData.MainhandType != state.BaseData.MainhandType ? actors.OnlyGPose() : actors; + Applier.ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand)); + var offhandActors = state.ModelData.OffhandType != state.BaseData.OffhandType ? actors.OnlyGPose() : actors; + Applier.ChangeOffhand(offhandActors, state.ModelData.Item(EquipSlot.OffHand), state.ModelData.Stain(EquipSlot.OffHand)); + } + + Glamourer.Log.Verbose( + $"Reset equipment state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); + // Update that we have completed a full operation. (We can do this directly as nothing else is linked) + StateFinalized.Invoke(StateFinalizationType.RevertEquipment, actors); + } + + public void ResetStateFixed(ActorState state, bool respectManualPalettes, uint key = 0) + { + if (!state.Unlock(key)) + return; + + foreach (var index in Enum.GetValues().Where(i => state.Sources[i] is StateSource.Fixed)) + { + state.Sources[index] = StateSource.Game; state.ModelData.Customize[index] = state.BaseData.Customize[index]; } foreach (var slot in EquipSlotExtensions.FullSlots) { - if (state[slot, true] is StateChanged.Source.Fixed) + if (state.Sources[slot, true] is StateSource.Fixed) { - state[slot, true] = StateChanged.Source.Game; + state.Sources[slot, true] = StateSource.Game; state.ModelData.SetStain(slot, state.BaseData.Stain(slot)); } - if (state[slot, false] is StateChanged.Source.Fixed) + if (state.Sources[slot, false] is StateSource.Fixed) { - state[slot, false] = StateChanged.Source.Game; + state.Sources[slot, false] = StateSource.Game; state.ModelData.SetItem(slot, state.BaseData.Item(slot)); } } - if (state[ActorState.MetaIndex.HatState] is StateChanged.Source.Fixed) + foreach (var slot in BonusExtensions.AllFlags) { - state[ActorState.MetaIndex.HatState] = StateChanged.Source.Game; - state.ModelData.SetHatVisible(state.BaseData.IsHatVisible()); + if (state.Sources[slot] is StateSource.Fixed) + { + state.Sources[slot] = StateSource.Game; + state.ModelData.SetBonusItem(slot, state.BaseData.BonusItem(slot)); + } } - if (state[ActorState.MetaIndex.VisorState] is StateChanged.Source.Fixed) + foreach (var slot in CrestExtensions.AllRelevantSet) { - state[ActorState.MetaIndex.VisorState] = StateChanged.Source.Game; - state.ModelData.SetVisor(state.BaseData.IsVisorToggled()); + if (state.Sources[slot] is StateSource.Fixed) + { + state.Sources[slot] = StateSource.Game; + state.ModelData.SetCrest(slot, state.BaseData.Crest(slot)); + } } - if (state[ActorState.MetaIndex.WeaponState] is StateChanged.Source.Fixed) + foreach (var flag in CustomizeParameterExtensions.AllFlags) { - state[ActorState.MetaIndex.WeaponState] = StateChanged.Source.Game; - state.ModelData.SetWeaponVisible(state.BaseData.IsWeaponVisible()); + switch (state.Sources[flag]) + { + case StateSource.Fixed: + case StateSource.Manual when !respectManualPalettes: + state.Sources[flag] = StateSource.Game; + state.ModelData.Parameters[flag] = state.BaseData.Parameters[flag]; + break; + } } - if (state[ActorState.MetaIndex.Wetness] is StateChanged.Source.Fixed) + foreach (var meta in MetaExtensions.AllRelevant.Where(f => state.Sources[f] is StateSource.Fixed)) { - state[ActorState.MetaIndex.Wetness] = StateChanged.Source.Game; - state.ModelData.SetIsWet(state.BaseData.IsWet()); + state.Sources[meta] = StateSource.Game; + state.ModelData.SetMeta(meta, state.BaseData.GetMeta(meta)); + } + + foreach (var (index, value) in state.Materials.Values.ToList()) + { + switch (value.Source) + { + case StateSource.Fixed: + case StateSource.Manual when !respectManualPalettes: + state.Materials.RemoveValue(index); + break; + } } } - public void ReapplyState(Actor actor) + public void ReapplyState(Actor actor, bool forceRedraw, StateSource source, bool isFinal = false) { if (!GetOrCreate(actor, out var state)) return; - ApplyAll(state, !actor.Model.IsHuman || Customize.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), - false); + ReapplyState(actor, state, forceRedraw, source, isFinal); + } + + public void ReapplyState(Actor actor, ActorState state, bool forceRedraw, StateSource source, bool isFinal) + { + var data = Applier.ApplyAll(state, + forceRedraw + || !actor.Model.IsHuman + || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); + StateChanged.Invoke(StateChangeType.Reapply, source, state, data, null); + if (isFinal) + StateFinalized.Invoke(StateFinalizationType.Reapply, data); + } + + /// Automation variant for reapply, to fire the correct StateUpdateType once reapplied. + public void ReapplyAutomationState(Actor actor, bool forceRedraw, bool wasReset, StateSource source) + { + if (!GetOrCreate(actor, out var state)) + return; + + ReapplyAutomationState(actor, state, forceRedraw, wasReset, source); + } + + /// Automation variant for reapply, to fire the correct StateUpdateType once reapplied. + public void ReapplyAutomationState(Actor actor, ActorState state, bool forceRedraw, bool wasReset, StateSource source) + { + var data = Applier.ApplyAll(state, + forceRedraw + || !actor.Model.IsHuman + || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); + StateChanged.Invoke(StateChangeType.Reapply, source, state, data, null); + // invoke the automation update based on what reset is. + StateFinalized.Invoke(wasReset ? StateFinalizationType.RevertAutomation : StateFinalizationType.ReapplyAutomation, data); } public void DeleteState(ActorIdentifier identifier) diff --git a/Glamourer/State/StateSource.cs b/Glamourer/State/StateSource.cs new file mode 100644 index 0000000..9a12214 --- /dev/null +++ b/Glamourer/State/StateSource.cs @@ -0,0 +1,122 @@ +using Penumbra.GameData.Enums; + +namespace Glamourer.State; + +public enum StateSource : byte +{ + Game, + Manual, + Fixed, + IpcFixed, + IpcManual, + + // Only used for CustomizeParameters and advanced dyes. + Pending, + IpcPending, +} + +public static class StateSourceExtensions +{ + public static StateSource Base(this StateSource source) + => source switch + { + StateSource.Manual or StateSource.Pending => StateSource.Manual, + StateSource.IpcManual or StateSource.IpcPending => StateSource.Manual, + StateSource.Fixed or StateSource.IpcFixed => StateSource.Fixed, + _ => StateSource.Game, + }; + + public static bool IsGame(this StateSource source) + => source.Base() is StateSource.Game; + + public static bool IsManual(this StateSource source) + => source.Base() is StateSource.Manual; + + public static bool IsFixed(this StateSource source) + => source.Base() is StateSource.Fixed; + + public static StateSource SetPending(this StateSource source) + => source switch + { + StateSource.Manual => StateSource.Pending, + StateSource.IpcManual => StateSource.IpcPending, + _ => source, + }; + + public static bool RequiresChange(this StateSource source) + => source switch + { + StateSource.Manual => true, + StateSource.IpcFixed => true, + StateSource.IpcManual => true, + _ => false, + }; + + public static bool IsIpc(this StateSource source) + => source is StateSource.IpcManual or StateSource.IpcFixed or StateSource.IpcPending; +} + +public unsafe struct StateSources +{ + public const int Size = (StateIndex.Size + 1) / 2; + private fixed byte _data[Size]; + + + public StateSources() + { } + + public StateSource this[StateIndex index] + { + get + { + var val = _data[index.Value / 2]; + return (StateSource)((index.Value & 1) == 1 ? val >> 4 : val & 0x0F); + } + set + { + var val = _data[index.Value / 2]; + if ((index.Value & 1) == 1) + val = (byte)((val & 0x0F) | ((byte)value << 4)); + else + val = (byte)((val & 0xF0) | (byte)value); + _data[index.Value / 2] = val; + } + } + + public StateSource this[EquipSlot slot, bool stain] + { + get => this[slot.ToState(stain)]; + set => this[slot.ToState(stain)] = value; + } + + public void RemoveFixedDesignSources() + { + for (var i = 0; i < Size; ++i) + { + var value = _data[i]; + switch (value) + { + case (byte)StateSource.Fixed | ((byte)StateSource.Fixed << 4): + _data[i] = (byte)StateSource.Manual | ((byte)StateSource.Manual << 4); + break; + + case (byte)StateSource.Game | ((byte)StateSource.Fixed << 4): + case (byte)StateSource.Manual | ((byte)StateSource.Fixed << 4): + case (byte)StateSource.IpcFixed | ((byte)StateSource.Fixed << 4): + case (byte)StateSource.Pending | ((byte)StateSource.Fixed << 4): + case (byte)StateSource.IpcPending | ((byte)StateSource.Fixed << 4): + case (byte)StateSource.IpcManual | ((byte)StateSource.Fixed << 4): + _data[i] = (byte)((value & 0x0F) | ((byte)StateSource.Manual << 4)); + break; + case (byte)StateSource.Fixed: + case ((byte)StateSource.Manual << 4) | (byte)StateSource.Fixed: + case ((byte)StateSource.IpcFixed << 4) | (byte)StateSource.Fixed: + case ((byte)StateSource.Pending << 4) | (byte)StateSource.Fixed: + case ((byte)StateSource.IpcPending << 4) | (byte)StateSource.Fixed: + case ((byte)StateSource.IpcManual << 4) | (byte)StateSource.Fixed: + _data[i] = (byte)((value & 0xF0) | (byte)StateSource.Manual); + break; + } + } + } +} diff --git a/Glamourer/State/WeaponState.cs b/Glamourer/State/WeaponState.cs deleted file mode 100644 index a06ff79..0000000 --- a/Glamourer/State/WeaponState.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Glamourer.Events; -using Glamourer.Services; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Glamourer.State; - -/// Currently unused. -public unsafe struct WeaponState -{ - private fixed ulong _weapons[FullEquipTypeExtensions.NumWeaponTypes]; - private fixed byte _sources[FullEquipTypeExtensions.NumWeaponTypes]; - - public CustomItemId? this[FullEquipType type] - { - get - { - if (!ToIndex(type, out var idx)) - return null; - - var weapon = _weapons[idx]; - if (weapon == 0) - return null; - - return new CustomItemId(weapon); - } - } - - public EquipItem Get(ItemManager items, EquipItem value) - { - var id = this[value.Type]; - if (id == null) - return value; - - var item = items.Resolve(value.Type, id.Value); - return item.Type != value.Type ? value : item; - } - - public void Set(FullEquipType type, EquipItem value, StateChanged.Source source) - { - if (!ToIndex(type, out var idx)) - return; - - _weapons[idx] = value.Id.Id; - _sources[idx] = (byte)source; - } - - public void RemoveFixedDesignSources() - { - for (var i = 0; i < FullEquipTypeExtensions.NumWeaponTypes; ++i) - { - if (_sources[i] is (byte) StateChanged.Source.Fixed) - _sources[i] = (byte) StateChanged.Source.Manual; - } - } - - private static bool ToIndex(FullEquipType type, out int index) - { - index = ToIndex(type); - return index is >= 0 and < FullEquipTypeExtensions.NumWeaponTypes; - } - - private static int ToIndex(FullEquipType type) - => (int)type - FullEquipTypeExtensions.WeaponTypesOffset; -} diff --git a/Glamourer/State/WorldSets.cs b/Glamourer/State/WorldSets.cs index 3f694ee..958a2ed 100644 --- a/Glamourer/State/WorldSets.cs +++ b/Glamourer/State/WorldSets.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using Glamourer.Interop.Structs; -using Glamourer.Structs; -using Penumbra.GameData.Enums; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; namespace Glamourer.State; @@ -25,7 +22,7 @@ public class WorldSets [(Gender.Male, Race.AuRa)] = FunEquipSet.Group.FullSetWithoutHat(0257, 2), [(Gender.Female, Race.AuRa)] = FunEquipSet.Group.FullSetWithoutHat(0258, 2), [(Gender.Male, Race.Hrothgar)] = FunEquipSet.Group.FullSetWithoutHat(0597, 1), - [(Gender.Female, Race.Hrothgar)] = FunEquipSet.Group.FullSetWithoutHat(0000, 0), // TODO Hrothgar Female + [(Gender.Female, Race.Hrothgar)] = FunEquipSet.Group.FullSetWithoutHat(0829, 1), [(Gender.Male, Race.Viera)] = FunEquipSet.Group.FullSetWithoutHat(0744, 1), [(Gender.Female, Race.Viera)] = FunEquipSet.Group.FullSetWithoutHat(0581, 1), }; @@ -73,6 +70,8 @@ public class WorldSets (CharacterWeapon.Int(2601, 13, 01), CharacterWeapon.Int(2651, 13, 1)), // DNC, High Steel Chakrams (CharacterWeapon.Int(2802, 13, 01), CharacterWeapon.Empty), // RPR, Deepgold War Scythe (CharacterWeapon.Int(2702, 08, 01), CharacterWeapon.Empty), // SGE, Stonegold Milpreves + (CharacterWeapon.Int(3101, 04, 03), CharacterWeapon.Int(3151, 04, 3)), // VPR, High Durium Twinfangs + (CharacterWeapon.Int(2901, 25, 01), CharacterWeapon.Int(2951, 25, 1)), // PCT, Chocobo Brush }; private static readonly (FunEquipSet.Group, CharacterWeapon, CharacterWeapon)[] _50Artifact = @@ -118,6 +117,8 @@ public class WorldSets (FunEquipSet.Group.FullSet(204, 4), CharacterWeapon.Int(2601, 13, 1), CharacterWeapon.Int(2651, 13, 1)), // DNC, Softstepper, High Steel Chakrams (new FunEquipSet.Group(206, 7, 303, 3, 23, 109, 303, 3, 262, 7), CharacterWeapon.Int(2802, 13, 1), CharacterWeapon.Empty), // RPR, Muzhik, Deepgold War Scythe (new FunEquipSet.Group(20, 46, 289, 6, 342, 3, 120, 9, 342, 3), CharacterWeapon.Int(2702, 08, 1), CharacterWeapon.Empty), // SGE, Bookwyrm, Stonegold Milpreves + (new FunEquipSet.Group(491, 3, 288, 5, 288, 5, 288, 5, 262, 8), CharacterWeapon.Int(3101, 06, 1), CharacterWeapon.Int(3151, 6, 1)), // VPR, Snakebite, Twinfangs + (new FunEquipSet.Group(595, 3, 372, 3, 290, 6, 315, 3, 290, 6), CharacterWeapon.Int(2901, 02, 1), CharacterWeapon.Int(2951, 2, 1)), // PCT, Painter's, Round Brush }; private static readonly (FunEquipSet.Group, CharacterWeapon, CharacterWeapon)[] _60Artifact = @@ -163,6 +164,8 @@ public class WorldSets (FunEquipSet.Group.FullSet(204, 4), CharacterWeapon.Int(2601, 13, 1), CharacterWeapon.Int(2651, 13, 01)), // DNC, Softstepper, High Steel Chakrams (new FunEquipSet.Group(206, 7, 303, 3, 23, 109, 303, 3, 262, 7), CharacterWeapon.Int(2802, 13, 1), CharacterWeapon.Empty), // RPR, Muzhik, Deepgold War Scythe (new FunEquipSet.Group(20, 46, 289, 6, 342, 3, 120, 9, 342, 3), CharacterWeapon.Int(2702, 08, 1), CharacterWeapon.Empty), // SGE, Bookwyrm, Stonegold Milpreves + (new FunEquipSet.Group(491, 3, 288, 5, 288, 5, 288, 5, 262, 8), CharacterWeapon.Int(3101, 06, 1), CharacterWeapon.Int(3151, 6, 1)), // VPR, Snakebite, Twinfangs + (new FunEquipSet.Group(595, 3, 372, 3, 290, 6, 315, 3, 290, 6), CharacterWeapon.Int(2901, 02, 1), CharacterWeapon.Int(2951, 2, 1)), // PCT, Painter's, Round Brush }; private static readonly (FunEquipSet.Group, CharacterWeapon, CharacterWeapon)[] _70Artifact = @@ -208,6 +211,8 @@ public class WorldSets (FunEquipSet.Group.FullSet(204, 4), CharacterWeapon.Int(2601, 13, 1), CharacterWeapon.Int(2651, 13, 01)), // DNC, Softstepper, High Steel Chakrams (new FunEquipSet.Group(206, 7, 303, 3, 23, 109, 303, 3, 262, 7), CharacterWeapon.Int(2802, 13, 1), CharacterWeapon.Empty), // RPR, Muzhik, Deepgold War Scythe (new FunEquipSet.Group(20, 46, 289, 6, 342, 3, 120, 9, 342, 3), CharacterWeapon.Int(2702, 08, 1), CharacterWeapon.Empty), // SGE, Bookwyrm, Stonegold Milpreves + (new FunEquipSet.Group(491, 3, 288, 5, 288, 5, 288, 5, 262, 8), CharacterWeapon.Int(3101, 06, 1), CharacterWeapon.Int(3151, 6, 1)), // VPR, Snakebite, Twinfangs + (new FunEquipSet.Group(595, 3, 372, 3, 290, 6, 315, 3, 290, 6), CharacterWeapon.Int(2901, 02, 1), CharacterWeapon.Int(2951, 2, 1)), // PCT, Painter's, Round Brush }; private static readonly (FunEquipSet.Group, CharacterWeapon, CharacterWeapon)[] _80Artifact = @@ -253,6 +258,8 @@ public class WorldSets (FunEquipSet.Group.FullSet(543, 1), CharacterWeapon.Int(2601, 001, 1), CharacterWeapon.Int(2651, 01, 001)), // DNC, Dancer, Krishna (new FunEquipSet.Group(206, 7, 303, 3, 23, 109, 303, 3, 262, 7), CharacterWeapon.Int(2802, 013, 1), CharacterWeapon.Empty), // RPR, Harvester's, Demon Slicer (new FunEquipSet.Group(20, 46, 289, 6, 342, 3, 120, 9, 342, 3), CharacterWeapon.Int(2702, 008, 1), CharacterWeapon.Empty), // SGE, Therapeute's, Horkos + (new FunEquipSet.Group(491, 3, 288, 5, 288, 5, 288, 5, 262, 8), CharacterWeapon.Int(3101, 6, 1), CharacterWeapon.Int(3151, 6, 1)), // VPR, Snakebite, Twinfangs + (new FunEquipSet.Group(595, 3, 372, 3, 290, 6, 315, 3, 290, 6), CharacterWeapon.Int(2901, 2, 1), CharacterWeapon.Int(2951, 2, 1)), // PCT, Painter's, Round Brush }; private static readonly (FunEquipSet.Group, CharacterWeapon, CharacterWeapon)[] _90Artifact = @@ -298,29 +305,80 @@ public class WorldSets (FunEquipSet.Group.FullSet(694, 1), CharacterWeapon.Int(2607, 001, 1), CharacterWeapon.Int(2657, 001, 001)), // DNC, Etoile, Terpsichore (FunEquipSet.Group.FullSet(695, 1), CharacterWeapon.Int(2801, 001, 1), CharacterWeapon.Empty), // RPR, Reaper, Death Sickle (FunEquipSet.Group.FullSet(696, 1), CharacterWeapon.Int(2701, 006, 1), CharacterWeapon.Empty), // SGE, Didact, Hagneia + (new FunEquipSet.Group(491, 3, 288, 5, 288, 5, 288, 5, 262, 8), CharacterWeapon.Int(3101, 006, 1), CharacterWeapon.Int(3151, 006, 001)), // VPR, Snakebite, Twinfangs + (new FunEquipSet.Group(595, 3, 372, 3, 290, 6, 315, 3, 290, 6), CharacterWeapon.Int(2901, 002, 1), CharacterWeapon.Int(2951, 002, 001)), // PCT, Painter's, Round Brush + }; + + private static readonly (FunEquipSet.Group, CharacterWeapon, CharacterWeapon)[] _100Artifact = + { + (FunEquipSet.Group.FullSet(000, 0), CharacterWeapon.Empty, CharacterWeapon.Empty), // ADV, Nothing + (FunEquipSet.Group.FullSet(772, 1), CharacterWeapon.Int(0220, 002, 01), CharacterWeapon.Int(0114, 002, 001)), // GLA, Caballarius, Clarent, Galahad + (FunEquipSet.Group.FullSet(773, 1), CharacterWeapon.Int(0338, 001, 01), CharacterWeapon.Int(0388, 001, 001)), // PGL, Hesychast's, Suwaiyas + (FunEquipSet.Group.FullSet(774, 1), CharacterWeapon.Int(0417, 001, 01), CharacterWeapon.Empty), // MRD, Agoge, Ferocity + (FunEquipSet.Group.FullSet(775, 1), CharacterWeapon.Int(0528, 001, 01), CharacterWeapon.Empty), // LNC, Heavensbound, Gae Assail + (FunEquipSet.Group.FullSet(776, 1), CharacterWeapon.Int(0635, 001, 01), CharacterWeapon.Int(0698, 130, 001)), // ARC, Bihu, Gastraphetes + (FunEquipSet.Group.FullSet(777, 1), CharacterWeapon.Int(0831, 002, 01), CharacterWeapon.Empty), // CNJ, Theophany, Xoanon + (FunEquipSet.Group.FullSet(778, 1), CharacterWeapon.Int(1033, 002, 01), CharacterWeapon.Empty), // THM, Archmage's, Gridarvor + (FunEquipSet.Group.FullSet(791, 1), CharacterWeapon.Int(5004, 001, 16), CharacterWeapon.Int(5041, 001, 016)), // CRP, Millrise + (FunEquipSet.Group.FullSet(792, 1), CharacterWeapon.Int(5103, 001, 01), CharacterWeapon.Int(5141, 001, 017)), // BSM, Forgerise + (FunEquipSet.Group.FullSet(793, 1), CharacterWeapon.Int(5201, 011, 01), CharacterWeapon.Int(5241, 001, 017)), // ARM, Hammerrise + (FunEquipSet.Group.FullSet(794, 1), CharacterWeapon.Int(5301, 011, 01), CharacterWeapon.Int(5341, 001, 001)), // GSM, Gemrise + (FunEquipSet.Group.FullSet(795, 1), CharacterWeapon.Int(5405, 001, 01), CharacterWeapon.Int(5441, 001, 016)), // LTW, Hiderise + (FunEquipSet.Group.FullSet(796, 1), CharacterWeapon.Int(5503, 001, 01), CharacterWeapon.Int(5571, 001, 001)), // WVR, Boltrise + (FunEquipSet.Group.FullSet(797, 1), CharacterWeapon.Int(5603, 008, 01), CharacterWeapon.Int(5641, 001, 017)), // ALC, Cauldronrise + (FunEquipSet.Group.FullSet(798, 1), CharacterWeapon.Int(5701, 012, 01), CharacterWeapon.Int(5741, 001, 017)), // CUL, Galleyrise + (FunEquipSet.Group.FullSet(799, 1), CharacterWeapon.Int(7004, 001, 01), CharacterWeapon.Int(7051, 001, 017)), // MIN, Minerise + (FunEquipSet.Group.FullSet(800, 1), CharacterWeapon.Int(7101, 012, 01), CharacterWeapon.Int(7151, 001, 017)), // BTN, Fieldrise + (FunEquipSet.Group.FullSet(801, 1), CharacterWeapon.Int(7202, 001, 01), CharacterWeapon.Int(7255, 001, 001)), // FSH, Tacklerise + (FunEquipSet.Group.FullSet(772, 1), CharacterWeapon.Int(0220, 002, 01), CharacterWeapon.Int(0114, 002, 001)), // PLD, Caballarius, Clarent, Galahad + (FunEquipSet.Group.FullSet(773, 1), CharacterWeapon.Int(0338, 001, 01), CharacterWeapon.Int(0388, 001, 001)), // MNK, Hesychast's, Suwaiyas + (FunEquipSet.Group.FullSet(774, 1), CharacterWeapon.Int(0417, 001, 01), CharacterWeapon.Empty), // WAR, Agoge, Ferocity + (FunEquipSet.Group.FullSet(775, 1), CharacterWeapon.Int(0528, 001, 01), CharacterWeapon.Empty), // DRG, Heavensbound, Gae Assail + (FunEquipSet.Group.FullSet(776, 1), CharacterWeapon.Int(0635, 001, 01), CharacterWeapon.Int(0698, 130, 001)), // BRD, Bihu, Gastraphetes + (FunEquipSet.Group.FullSet(777, 1), CharacterWeapon.Int(0831, 002, 01), CharacterWeapon.Empty), // WHM, Theophany, Xoanon + (FunEquipSet.Group.FullSet(778, 1), CharacterWeapon.Int(1033, 002, 01), CharacterWeapon.Empty), // BLM, Archmage's, Gridarvor + (FunEquipSet.Group.FullSet(779, 1), CharacterWeapon.Int(1752, 001, 01), CharacterWeapon.Empty), // ACN, Glyphic, The Grand Grimoire + (FunEquipSet.Group.FullSet(779, 1), CharacterWeapon.Int(1752, 001, 01), CharacterWeapon.Empty), // SMN, Glyphic, The Grand Grimoire + (FunEquipSet.Group.FullSet(780, 1), CharacterWeapon.Int(1753, 001, 01), CharacterWeapon.Empty), // SCH, Pedagogy, Eclecticism + (FunEquipSet.Group.FullSet(781, 1), CharacterWeapon.Int(1801, 128, 01), CharacterWeapon.Int(1851, 128, 001)), // ROG, Momochi, Shiranui + (FunEquipSet.Group.FullSet(781, 1), CharacterWeapon.Int(1801, 128, 01), CharacterWeapon.Int(1851, 128, 001)), // NIN, Momochi, Shiranui + (FunEquipSet.Group.FullSet(783, 1), CharacterWeapon.Int(2026, 002, 01), CharacterWeapon.Int(2099, 001, 001)), // MCH, Forerider's, Sthalmann Special + (FunEquipSet.Group.FullSet(782, 1), CharacterWeapon.Int(1519, 002, 01), CharacterWeapon.Empty), // DRK, Fallen's, Maleficus + (FunEquipSet.Group.FullSet(784, 1), CharacterWeapon.Int(2136, 082, 01), CharacterWeapon.Int(2199, 001, 188)), // AST, Ephemerist's, Metis + (FunEquipSet.Group.FullSet(785, 1), CharacterWeapon.Int(2215, 001, 01), CharacterWeapon.Int(2259, 003, 001)), // SAM, Sakonji, Kogarasumaru + (FunEquipSet.Group.FullSet(786, 1), CharacterWeapon.Int(2301, 097, 01), CharacterWeapon.Int(2380, 001, 001)), // RDM, Roseblood, Colada + (FunEquipSet.Group.FullSet(811, 1), CharacterWeapon.Int(2401, 005, 01), CharacterWeapon.Empty), // BLU, Phantasmal, Blue-eyes + (FunEquipSet.Group.FullSet(787, 1), CharacterWeapon.Int(2501, 064, 01), CharacterWeapon.Empty), // GNB, Bastion's, Chastiefol + (FunEquipSet.Group.FullSet(788, 1), CharacterWeapon.Int(2611, 002, 01), CharacterWeapon.Int(2661, 002, 001)), // DNC, Horos, Soma + (FunEquipSet.Group.FullSet(790, 1), CharacterWeapon.Int(2816, 001, 01), CharacterWeapon.Empty), // RPR, Assassin's, Vendetta + (FunEquipSet.Group.FullSet(789, 1), CharacterWeapon.Int(2701, 026, 01), CharacterWeapon.Empty), // SGE, Metanoia, Asklepian + (FunEquipSet.Group.FullSet(840, 1), CharacterWeapon.Int(3101, 001, 01), CharacterWeapon.Int(3151, 001, 001)), // VPR, Viper's, Sargatanas + (FunEquipSet.Group.FullSet(841, 1), CharacterWeapon.Int(2901, 001, 01), CharacterWeapon.Int(2951, 001, 001)), // PCT, Pictomancer's, Angel Brush }; // @formatter:on private (FunEquipSet.Group, CharacterWeapon, CharacterWeapon)? GetGroup(byte level, byte job, Race race, Gender gender, Random rng) { - const int weight50 = 1200; - const int weight60 = 1500; - const int weight70 = 1700; - const int weight80 = 1800; - const int weight90 = 1900; - const int weight100 = 2000; + const int weight50 = 200; + const int weight60 = 500; + const int weight70 = 700; + const int weight80 = 800; + const int weight90 = 900; + const int weight100 = 1000; + const int weight110 = 1100; if (job >= StarterWeapons.Length) return null; var maxWeight = level switch { - < 50 => weight50, - < 60 => weight60, - < 70 => weight70, - < 80 => weight80, - < 90 => weight90, - _ => weight100, + < 50 => weight50, + < 60 => weight60, + < 70 => weight70, + < 80 => weight80, + < 90 => weight90, + < 100 => weight100, + _ => weight110, }; var weight = rng.Next(0, maxWeight + 1); @@ -335,11 +393,12 @@ public class WorldSets var list = weight switch { - < weight60 => _50Artifact, - < weight70 => _60Artifact, - < weight80 => _70Artifact, - < weight90 => _80Artifact, - _ => _90Artifact, + < weight60 => _50Artifact, + < weight70 => _60Artifact, + < weight80 => _70Artifact, + < weight90 => _80Artifact, + < weight100 => _90Artifact, + _ => _100Artifact, }; Glamourer.Log.Verbose($"Chose weight {weight}/{maxWeight} for World set [Character: {level} {job} {race} {gender}]."); @@ -347,10 +406,10 @@ public class WorldSets } - private unsafe (byte, byte, Race, Gender) GetData(Actor actor) + private static unsafe (byte, byte, Race, Gender) GetData(Actor actor) { - var customize = actor.GetCustomize(); - return (actor.AsCharacter->CharacterData.Level, actor.Job, customize.Race, customize.Gender); + var customize = actor.Customize; + return (actor.AsCharacter->CharacterData.Level, actor.Job, customize->Race, customize->Gender); } public void Apply(Actor actor, Random rng, Span armor) @@ -359,7 +418,7 @@ public class WorldSets Apply(level, job, race, gender, rng, armor); } - public void Apply(byte level, byte job, Race race, Gender gender, Random rng, Span armor) + private void Apply(byte level, byte job, Race race, Gender gender, Random rng, Span armor) { var opt = GetGroup(level, job, race, gender, rng); if (opt == null) @@ -378,7 +437,7 @@ public class WorldSets Apply(level, job, race, gender, rng, ref armor, slot); } - public void Apply(byte level, byte job, Race race, Gender gender, Random rng, ref CharacterArmor armor, EquipSlot slot) + private void Apply(byte level, byte job, Race race, Gender gender, Random rng, ref CharacterArmor armor, EquipSlot slot) { var opt = GetGroup(level, job, race, gender, rng); if (opt == null) @@ -401,7 +460,7 @@ public class WorldSets Apply(level, job, race, gender, rng, ref weapon, slot); } - public void Apply(byte level, byte job, Race race, Gender gender, Random rng, ref CharacterWeapon weapon, EquipSlot slot) + private void Apply(byte level, byte job, Race race, Gender gender, Random rng, ref CharacterWeapon weapon, EquipSlot slot) { var opt = GetGroup(level, job, race, gender, rng); if (opt == null) diff --git a/Glamourer/Unlocks/CustomizeUnlockManager.cs b/Glamourer/Unlocks/CustomizeUnlockManager.cs index 19a3f6c..bd13f99 100644 --- a/Glamourer/Unlocks/CustomizeUnlockManager.cs +++ b/Glamourer/Unlocks/CustomizeUnlockManager.cs @@ -1,26 +1,24 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud; +using Dalamud.Game; using Dalamud.Hooking; using Dalamud.Plugin.Services; -using Dalamud.Utility; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.UI; -using Glamourer.Customization; +using Glamourer.GameData; using Glamourer.Events; using Glamourer.Services; -using Lumina.Excel.GeneratedSheets; +using Lumina.Excel.Sheets; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; namespace Glamourer.Unlocks; public class CustomizeUnlockManager : IDisposable, ISavable { - private readonly SaveService _saveService; - private readonly IClientState _clientState; - private readonly ObjectUnlocked _event; - + private readonly SaveService _saveService; + private readonly IClientState _clientState; + private readonly ObjectUnlocked _event; + private readonly ActorObjectManager _objects; private readonly Dictionary _unlocked = new(); public readonly IReadOnlyDictionary Unlockable; @@ -28,13 +26,14 @@ public class CustomizeUnlockManager : IDisposable, ISavable public IReadOnlyDictionary Unlocked => _unlocked; - public CustomizeUnlockManager(SaveService saveService, CustomizationService customizations, IDataManager gameData, - IClientState clientState, ObjectUnlocked @event, IGameInteropProvider interop) + public CustomizeUnlockManager(SaveService saveService, CustomizeService customizations, IDataManager gameData, + IClientState clientState, ObjectUnlocked @event, IGameInteropProvider interop, ActorObjectManager objects) { interop.InitializeFromAttributes(this); _saveService = saveService; _clientState = clientState; _event = @event; + _objects = objects; Unlockable = CreateUnlockableCustomizations(customizations, gameData); Load(); _setUnlockLinkValueHook.Enable(); @@ -96,7 +95,7 @@ public class CustomizeUnlockManager : IDisposable, ISavable /// Scan and update all unlockable customizations for their current game state. public unsafe void Scan() { - if (_clientState.LocalPlayer == null) + if (!_objects.Player.Valid) return; Glamourer.Log.Debug("[UnlockManager] Scanning for new unlocked customizations."); @@ -131,7 +130,7 @@ public class CustomizeUnlockManager : IDisposable, ISavable private delegate void SetUnlockLinkValueDelegate(nint uiState, uint data, byte value); - [Signature("48 83 EC ?? 8B C2 44 8B D2", DetourName = nameof(SetUnlockLinkValueDetour))] + [Signature(Sigs.SetUnlockLinkValue, DetourName = nameof(SetUnlockLinkValueDetour))] private readonly Hook _setUnlockLinkValueHook = null!; private void SetUnlockLinkValueDetour(nint uiState, uint data, byte value) @@ -172,39 +171,36 @@ public class CustomizeUnlockManager : IDisposable, ISavable => UnlockDictionaryHelpers.Load(ToFilename(_saveService.FileNames), _unlocked, id => Unlockable.Any(c => c.Value.Data == id), "customization"); - /// Create a list of all unlockable hairstyles and facepaints. - private static Dictionary CreateUnlockableCustomizations(CustomizationService customizations, + /// Create a list of all unlockable hairstyles and face paints. + private static Dictionary CreateUnlockableCustomizations(CustomizeService customizations, IDataManager gameData) { var ret = new Dictionary(); - var sheet = gameData.GetExcelSheet(ClientLanguage.English)!; - foreach (var clan in customizations.AwaitedService.Clans) + var sheet = gameData.GetExcelSheet(ClientLanguage.English); + foreach (var (clan, gender) in CustomizeManager.AllSets()) { - foreach (var gender in customizations.AwaitedService.Genders) + var list = customizations.Manager.GetSet(clan, gender); + foreach (var hair in list.HairStyles) { - var list = customizations.AwaitedService.GetList(clan, gender); - foreach (var hair in list.HairStyles) + var x = sheet.FirstOrNull(f => f.FeatureID == hair.Value.Value); + if (x?.IsPurchasable == true) { - var x = sheet.FirstOrDefault(f => f.FeatureID == hair.Value.Value); - if (x?.IsPurchasable == true) - { - var name = x.FeatureID == 61 - ? "Eternal Bond" - : x.HintItem.Value?.Name.ToDalamudString().ToString().Replace("Modern Aesthetics - ", string.Empty) - ?? string.Empty; - ret.TryAdd(hair, (x.Data, name)); - } + var name = x.Value.FeatureID == 61 + ? "Eternal Bond" + : x.Value.HintItem.ValueNullable?.Name.ExtractText().Replace("Modern Aesthetics - ", string.Empty) + ?? string.Empty; + ret.TryAdd(hair, (x.Value.UnlockLink, name)); } + } - foreach (var paint in list.FacePaints) + foreach (var paint in list.FacePaints) + { + var x = sheet.FirstOrNull(f => f.FeatureID == paint.Value.Value); + if (x?.IsPurchasable == true) { - var x = sheet.FirstOrDefault(f => f.FeatureID == paint.Value.Value); - if (x?.IsPurchasable == true) - { - var name = x.HintItem.Value?.Name.ToDalamudString().ToString().Replace("Modern Cosmetics - ", string.Empty) - ?? string.Empty; - ret.TryAdd(paint, (x.Data, name)); - } + var name = x.Value.HintItem.ValueNullable?.Name.ExtractText().Replace("Modern Cosmetics - ", string.Empty) + ?? string.Empty; + ret.TryAdd(paint, (x.Value.UnlockLink, name)); } } } diff --git a/Glamourer/Unlocks/FavoriteManager.cs b/Glamourer/Unlocks/FavoriteManager.cs index 4cd98c8..01a2507 100644 --- a/Glamourer/Unlocks/FavoriteManager.cs +++ b/Glamourer/Unlocks/FavoriteManager.cs @@ -1,20 +1,31 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Glamourer.Services; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using OtterGui.Classes; +using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; namespace Glamourer.Unlocks; public class FavoriteManager : ISavable { - private readonly SaveService _saveService; - private readonly HashSet _favorites = new(); + private readonly record struct FavoriteHairStyle(Gender Gender, SubRace Race, CustomizeIndex Type, CustomizeValue Id) + { + public uint ToValue() + => Id.Value | ((uint)Type << 8) | ((uint)Race << 16) | ((uint)Gender << 24); + + public FavoriteHairStyle(uint value) + : this((Gender)((value >> 24) & 0xFF), (SubRace)((value >> 16) & 0xFF), (CustomizeIndex)((value >> 8) & 0xFF), + (CustomizeValue)(value & 0xFF)) + { } + } + + private const int CurrentVersion = 1; + private readonly SaveService _saveService; + private readonly HashSet _favorites = []; + private readonly HashSet _favoriteColors = []; + private readonly HashSet _favoriteHairStyles = []; + private readonly HashSet _favoriteBonusItems = []; public FavoriteManager(SaveService saveService) { @@ -22,6 +33,14 @@ public class FavoriteManager : ISavable Load(); } + public static bool TypeAllowed(CustomizeIndex type) + => type switch + { + CustomizeIndex.Hairstyle => true, + CustomizeIndex.FacePaint => true, + _ => false, + }; + private void Load() { var file = _saveService.FileNames.FavoriteFile; @@ -30,9 +49,26 @@ public class FavoriteManager : ISavable try { - var text = File.ReadAllText(file); - var array = JsonConvert.DeserializeObject(text) ?? Array.Empty(); - _favorites.UnionWith(array.Select(i => (ItemId)i)); + var text = File.ReadAllText(file); + if (text.StartsWith('[')) + { + LoadV0(text); + } + else + { + var load = JsonConvert.DeserializeObject(text); + switch (load?.Version ?? 0) + { + case 1: + _favorites.UnionWith(load!.FavoriteItems.Select(i => (ItemId)i)); + _favoriteColors.UnionWith(load.FavoriteColors.Select(i => (StainId)i)); + _favoriteHairStyles.UnionWith(load.FavoriteHairStyles.Select(t => new FavoriteHairStyle(t))); + _favoriteBonusItems.UnionWith(load.FavoriteBonusItems.Select(b => new BonusItemId(b))); + break; + + default: throw new Exception($"Unknown Version {load?.Version ?? 0}"); + } + } } catch (Exception ex) { @@ -40,6 +76,13 @@ public class FavoriteManager : ISavable } } + private void LoadV0(string text) + { + var array = JsonConvert.DeserializeObject(text) ?? []; + _favorites.UnionWith(array.Select(i => (ItemId)i)); + Save(); + } + public string ToFilename(FilenameService fileNames) => fileNames.FavoriteFile; @@ -48,18 +91,47 @@ public class FavoriteManager : ISavable public void Save(StreamWriter writer) { - using var j = new JsonTextWriter(writer) - { - Formatting = Formatting.Indented, - }; + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + j.WriteStartObject(); + + j.WritePropertyName(nameof(LoadIntermediary.Version)); + j.WriteValue(CurrentVersion); + + j.WritePropertyName(nameof(LoadIntermediary.FavoriteItems)); j.WriteStartArray(); foreach (var item in _favorites) j.WriteValue(item.Id); j.WriteEndArray(); + + j.WritePropertyName(nameof(LoadIntermediary.FavoriteColors)); + j.WriteStartArray(); + foreach (var stain in _favoriteColors) + j.WriteValue(stain.Id); + j.WriteEndArray(); + + j.WritePropertyName(nameof(LoadIntermediary.FavoriteHairStyles)); + j.WriteStartArray(); + foreach (var hairStyle in _favoriteHairStyles) + j.WriteValue(hairStyle.ToValue()); + j.WriteEndArray(); + + j.WritePropertyName(nameof(LoadIntermediary.FavoriteBonusItems)); + j.WriteStartArray(); + foreach (var item in _favoriteBonusItems) + j.WriteValue(item.Id); + j.WriteEndArray(); + + j.WriteEndObject(); } public bool TryAdd(EquipItem item) - => TryAdd(item.ItemId); + { + if (item.Id.IsBonusItem) + return TryAdd(item.Id.BonusItem); + + return TryAdd(item.ItemId); + } public bool TryAdd(ItemId item) { @@ -70,8 +142,39 @@ public class FavoriteManager : ISavable return true; } + public bool TryAdd(BonusItemId item) + { + if (item.Id == 0 || !_favoriteBonusItems.Add(item)) + return false; + + Save(); + return true; + } + + public bool TryAdd(StainId stain) + { + if (stain.Id == 0 || !_favoriteColors.Add(stain)) + return false; + + Save(); + return true; + } + + public bool TryAdd(Gender gender, SubRace race, CustomizeIndex type, CustomizeValue value) + { + if (!TypeAllowed(type) || !_favoriteHairStyles.Add(new FavoriteHairStyle(gender, race, type, value))) + return false; + + Save(); + return true; + } + public bool Remove(EquipItem item) - => Remove(item.ItemId); + { + if (item.Id.IsBonusItem) + Remove(item.Id.BonusItem); + return Remove(item.ItemId); + } public bool Remove(ItemId item) { @@ -82,15 +185,59 @@ public class FavoriteManager : ISavable return true; } - public IEnumerator GetEnumerator() - => _favorites.GetEnumerator(); + public bool Remove(BonusItemId item) + { + if (!_favoriteBonusItems.Remove(item)) + return false; - public int Count - => _favorites.Count; + Save(); + return true; + } + + public bool Remove(StainId stain) + { + if (!_favoriteColors.Remove(stain)) + return false; + + Save(); + return true; + } + + public bool Remove(Gender gender, SubRace race, CustomizeIndex type, CustomizeValue value) + { + if (!_favoriteHairStyles.Remove(new FavoriteHairStyle(gender, race, type, value))) + return false; + + Save(); + return true; + } public bool Contains(EquipItem item) - => _favorites.Contains(item.ItemId); + { + if (item.Id.IsBonusItem) + return _favoriteBonusItems.Contains(item.Id.BonusItem); - public bool Contains(ItemId item) - => _favorites.Contains(item); + return _favorites.Contains(item.ItemId); + } + + public bool Contains(StainId stain) + => _favoriteColors.Contains(stain); + + public bool Contains(ItemId itemId) + => _favorites.Contains(itemId); + + public bool Contains(BonusItemId bonusItemId) + => _favoriteBonusItems.Contains(bonusItemId); + + public bool Contains(Gender gender, SubRace race, CustomizeIndex type, CustomizeValue value) + => _favoriteHairStyles.Contains(new FavoriteHairStyle(gender, race, type, value)); + + private class LoadIntermediary + { + public int Version = CurrentVersion; + public uint[] FavoriteItems = []; + public byte[] FavoriteColors = []; + public uint[] FavoriteHairStyles = []; + public ushort[] FavoriteBonusItems = []; + } } diff --git a/Glamourer/Unlocks/ItemUnlockManager.cs b/Glamourer/Unlocks/ItemUnlockManager.cs index 972a393..6708267 100644 --- a/Glamourer/Unlocks/ItemUnlockManager.cs +++ b/Glamourer/Unlocks/ItemUnlockManager.cs @@ -1,17 +1,13 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.UI; using Glamourer.Events; using Glamourer.Services; -using Lumina.Excel.GeneratedSheets; +using Lumina.Excel.Sheets; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Cabinet = Lumina.Excel.GeneratedSheets.Cabinet; +using Cabinet = Lumina.Excel.Sheets.Cabinet; namespace Glamourer.Unlocks; @@ -22,7 +18,7 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionary _unlocked = new(); @@ -45,7 +41,7 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionary Unlockable; public ItemUnlockManager(SaveService saveService, ItemManager items, IClientState clientState, IDataManager gameData, IFramework framework, - ObjectUnlocked @event, IdentifierService identifier, IGameInteropProvider interop) + ObjectUnlocked @event, ObjectIdentification identifier, IGameInteropProvider interop) { interop.InitializeFromAttributes(this); _saveService = saveService; @@ -100,12 +96,12 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionary(mirageManager->PrismBoxItemIds, 800); + var span = mirageManager->PrismBoxItemIds; foreach (var item in span) changes |= AddItem(item, time); } @@ -158,10 +153,9 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionaryGlamourPlatesSpan) + foreach (var plate in mirageManager->GlamourPlates) { - // TODO: Make independent from hardcoded value - var span = new ReadOnlySpan(plate.ItemIds, 12); + var span = plate.ItemIds; foreach (var item in span) changes |= AddItem(item, time); } @@ -174,14 +168,14 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionaryGetInventoryContainer(type); - if (container != null && container->Loaded != 0 && _currentInventoryIndex < container->Size) + if (container != null && container->IsLoaded && _currentInventoryIndex < container->Size) { Glamourer.Log.Excessive($"[UnlockScanner] Scanning {_currentInventory} {type} {_currentInventoryIndex}/{container->Size}."); var item = container->GetInventorySlot(_currentInventoryIndex++); if (item != null) { - changes |= AddItem(item->ItemID, time); - changes |= AddItem(item->GlamourID, time); + changes |= AddItem(item->ItemId, time); + changes |= AddItem(item->GlamourId, time); } } else @@ -198,7 +192,7 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionary= _items.ItemSheet.RowCount) + if (itemId.Id >= (uint) _items.ItemSheet.Count) { time = DateTimeOffset.MinValue; return true; @@ -272,39 +266,38 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionary _items.ItemService.AwaitedService.TryGetValue(id, EquipSlot.MainHand, out _), "item"); + id => _items.ItemData.TryGetValue(id, EquipSlot.MainHand, out _), "item"); UpdateModels(version); } private static Dictionary CreateUnlockData(IDataManager gameData, ItemManager items) { var ret = new Dictionary(); - var cabinet = gameData.GetExcelSheet()!; + var cabinet = gameData.GetExcelSheet(); foreach (var row in cabinet) { - if (items.ItemService.AwaitedService.TryGetValue(row.Item.Row, EquipSlot.MainHand, out var item)) + if (items.ItemData.TryGetValue(row.Item.RowId, EquipSlot.MainHand, out var item)) ret.TryAdd(item.ItemId, new UnlockRequirements(row.RowId, 0, 0, 0, UnlockType.Cabinet)); } - var gilShopItem = gameData.GetExcelSheet()!; - var gilShop = gameData.GetExcelSheet()!; - foreach (var row in gilShopItem) + var gilShopItem = gameData.GetSubrowExcelSheet(); + var gilShop = gameData.GetExcelSheet(); + foreach (var row in gilShopItem.SelectMany(g => g)) { - if (!items.ItemService.AwaitedService.TryGetValue(row.Item.Row, EquipSlot.MainHand, out var item)) + if (!items.ItemData.TryGetValue(row.Item.RowId, EquipSlot.MainHand, out var item)) continue; - var quest1 = row.QuestRequired[0].Row; - var quest2 = row.QuestRequired[1].Row; - var achievement = row.AchievementRequired.Row; + var quest1 = row.QuestRequired[0].RowId; + var quest2 = row.QuestRequired[1].RowId; + var achievement = row.AchievementRequired.RowId; var state = row.StateRequired; - var shop = gilShop.GetRow(row.RowId); - if (shop != null && shop.Quest.Row != 0) + if (gilShop.TryGetRow(row.RowId, out var shop) && shop.Quest.RowId != 0) { if (quest1 == 0) - quest1 = shop.Quest.Row; + quest1 = shop.Quest.RowId; else if (quest2 == 0) - quest2 = shop.Quest.Row; + quest2 = shop.Quest.RowId; } var type = (quest1 != 0 ? UnlockType.Quest1 : 0) @@ -323,10 +316,10 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionary