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 040c476..0000000 --- a/Glamourer.GameData/Customization/CustomName.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Glamourer.Customization; - -// Localization from the game files directly. -public enum CustomName -{ - 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 254af2e..0000000 --- a/Glamourer.GameData/Customization/CustomizationNpcOptions.cs +++ /dev/null @@ -1,362 +0,0 @@ -using System; -using Dalamud.Plugin.Services; -using Lumina.Excel; -using Lumina.Excel.GeneratedSheets; -using OtterGui; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using Dalamud.Utility; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Penumbra.GameData; - -namespace Glamourer.Customization; - -public static class CustomizationNpcOptions -{ - public unsafe struct NpcData - { - public string Name; - public Customize Customize; - private fixed byte _equip[40]; - public CharacterWeapon Mainhand; - public CharacterWeapon Offhand; - public uint Id; - public bool VisorToggled; - public ObjectKind Kind; - - public ReadOnlySpan Equip - { - get - { - fixed (byte* ptr = _equip) - { - return new ReadOnlySpan((CharacterArmor*)ptr, 10); - } - } - } - - 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")); - sb.Append('-'); - sb.Append(span[i].Variant.Id.ToString("D3")); - sb.Append('-'); - sb.Append(span[i].Stain.Id.ToString("D3")); - sb.Append(", "); - } - - sb.Append(Mainhand.Set.Id.ToString("D4")); - sb.Append('-'); - sb.Append(Mainhand.Type.Id.ToString("D4")); - sb.Append('-'); - sb.Append(Mainhand.Variant.Id.ToString("D3")); - sb.Append('-'); - sb.Append(Mainhand.Stain.Id.ToString("D4")); - sb.Append(", "); - sb.Append(Offhand.Set.Id.ToString("D4")); - sb.Append('-'); - sb.Append(Offhand.Type.Id.ToString("D4")); - sb.Append('-'); - sb.Append(Offhand.Variant.Id.ToString("D3")); - sb.Append('-'); - sb.Append(Offhand.Stain.Id.ToString("D3")); - return sb.ToString(); - } - - internal void Set(int idx, uint value) - { - fixed (byte* ptr = _equip) - { - ((uint*)ptr)[idx] = value; - } - } - - public bool DataEquals(in NpcData other) - { - 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)); - } - } - } - - private static void ApplyNpcEquip(ref NpcData data, NpcEquip row) - { - data.Set(0, row.ModelHead | (row.DyeHead.Row << 24)); - data.Set(1, row.ModelBody | (row.DyeBody.Row << 24)); - data.Set(2, row.ModelHands | (row.DyeHands.Row << 24)); - data.Set(3, row.ModelLegs | (row.DyeLegs.Row << 24)); - data.Set(4, row.ModelFeet | (row.DyeFeet.Row << 24)); - data.Set(5, row.ModelEars | (row.DyeEars.Row << 24)); - data.Set(6, row.ModelNeck | (row.DyeNeck.Row << 24)); - data.Set(7, row.ModelWrists | (row.DyeWrists.Row << 24)); - data.Set(8, row.ModelRightRing | (row.DyeRightRing.Row << 24)); - data.Set(9, row.ModelLeftRing | (row.DyeLeftRing.Row << 24)); - data.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.Row << 48)); - data.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.Row << 48)); - data.VisorToggled = row.Visor; - } - - public static unsafe IReadOnlyList CreateNpcData(IReadOnlyDictionary eNpcs, - IReadOnlyDictionary bnpcNames, IObjectIdentifier identifier, IDataManager data) - { - var enpcSheet = data.GetExcelSheet()!; - var bnpcSheet = data.GetExcelSheet()!; - var list = new List(eNpcs.Count + (int)bnpcSheet.RowCount); - foreach (var (id, name) in eNpcs) - { - var row = enpcSheet.GetRow(id); - if (row == null || name.IsNullOrWhitespace()) - continue; - - var (valid, customize) = FromEnpcBase(row); - if (!valid) - continue; - - var ret = new NpcData - { - Name = name, - Customize = customize, - Id = id, - Kind = ObjectKind.EventNpc, - }; - - if (row.NpcEquip.Row != 0 && row.NpcEquip.Value is { } equip) - { - ApplyNpcEquip(ref ret, equip); - } - else - { - ret.Set(0, row.ModelHead | (row.DyeHead.Row << 24)); - ret.Set(1, row.ModelBody | (row.DyeBody.Row << 24)); - ret.Set(2, row.ModelHands | (row.DyeHands.Row << 24)); - ret.Set(3, row.ModelLegs | (row.DyeLegs.Row << 24)); - ret.Set(4, row.ModelFeet | (row.DyeFeet.Row << 24)); - ret.Set(5, row.ModelEars | (row.DyeEars.Row << 24)); - ret.Set(6, row.ModelNeck | (row.DyeNeck.Row << 24)); - ret.Set(7, row.ModelWrists | (row.DyeWrists.Row << 24)); - ret.Set(8, row.ModelRightRing | (row.DyeRightRing.Row << 24)); - ret.Set(9, row.ModelLeftRing | (row.DyeLeftRing.Row << 24)); - ret.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.Row << 48)); - ret.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.Row << 48)); - ret.VisorToggled = row.Visor; - } - - list.Add(ret); - } - - foreach (var baseRow in bnpcSheet) - { - if (baseRow.ModelChara.Value!.Type != 1) - continue; - - var bnpcNameIds = identifier.GetBnpcNames(baseRow.RowId); - if (bnpcNameIds.Count == 0) - continue; - - var (valid, customize) = FromBnpcCustomize(baseRow.BNpcCustomize.Value!); - if (!valid) - continue; - - var equip = baseRow.NpcEquip.Value!; - var ret = new NpcData - { - Customize = customize, - Id = baseRow.RowId, - Kind = ObjectKind.BattleNpc, - }; - ApplyNpcEquip(ref ret, equip); - foreach (var bnpcNameId in bnpcNameIds) - { - if (bnpcNames.TryGetValue(bnpcNameId.Id, out var name) && !name.IsNullOrWhitespace()) - list.Add(ret with { Name = name }); - } - } - - var groups = list.GroupBy(d => d.Name).ToDictionary(g => g.Key, g => g.ToList()); - list.Clear(); - foreach (var (name, duplicates) in groups.OrderBy(kvp => kvp.Key)) - { - for (var i = 0; i < duplicates.Count; ++i) - { - var current = duplicates[i]; - var add = true; - for (var j = 0; j < i; ++j) - { - if (current.DataEquals(duplicates[j])) - { - duplicates.RemoveAt(i--); - break; - } - } - } - - if (duplicates.Count == 1) - list.Add(duplicates[0]); - else - list.AddRange(duplicates - .Select(duplicate => duplicate with { Name = $"{name} ({(duplicate.Kind is ObjectKind.BattleNpc ? 'B' : 'E')}{duplicate.Id})" })); - } - - var lastWeird = list.FindIndex(d => char.IsAsciiLetterOrDigit(d.Name[0])); - if (lastWeird != -1) - { - list.AddRange(list.Take(lastWeird)); - list.RemoveRange(0, lastWeird); - } - - return list; - } - - - 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 fdbac65..0000000 --- a/Glamourer.GameData/Customization/CustomizationOptions.cs +++ /dev/null @@ -1,526 +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.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(ClientLanguage.English)!; - _bnpcCustomize = gameData.GetExcelSheet(ClientLanguage.English)!; - _enpcBase = gameData.GetExcelSheet(ClientLanguage.English)!; - Lobby = gameData.GetExcelSheet(ClientLanguage.English)!; - 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(); - } - - // 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] = 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(); - 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 38aca21..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 => "Enable Highlights", - CustomizeIndex.SkinColor => "Skin Color", - CustomizeIndex.EyeColorRight => "Left Eye", // inverted due to compatibility fuckup. - CustomizeIndex.HairColor => "Hair Color", - CustomizeIndex.HighlightsColor => "Highlights Color", - CustomizeIndex.TattooColor => "Tattoo Color", - CustomizeIndex.Eyebrows => "Eyebrow Style", - CustomizeIndex.EyeColorLeft => "Right Eye", // inverted due to compatibility fuckup. - 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 71151d9..0000000 --- a/Glamourer.GameData/Offsets.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Glamourer; - -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"; -} diff --git a/Glamourer.GameData/Structs/CrestFlag.cs b/Glamourer.GameData/Structs/CrestFlag.cs deleted file mode 100644 index 61ccc7e..0000000 --- a/Glamourer.GameData/Structs/CrestFlag.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Penumbra.GameData.Enums; - -namespace Glamourer.Structs; - -[Flags] -public enum CrestFlag : ushort -{ - OffHand = 0x0001, - Head = 0x0002, - Body = 0x0004, - Hands = 0x0008, - Legs = 0x0010, - Feet = 0x0020, - Ears = 0x0040, - Neck = 0x0080, - Wrists = 0x0100, - RFinger = 0x0200, - LFinger = 0x0400, - MainHand = 0x0800, -} - -public enum CrestType : byte -{ - None, - Human, - Mainhand, - Offhand, -}; - -public static class CrestExtensions -{ - public const CrestFlag All = (CrestFlag)(((ulong)EquipFlag.Mainhand << 1) - 1); - public const CrestFlag AllRelevant = CrestFlag.Head | CrestFlag.Body | CrestFlag.OffHand; - - public static readonly IReadOnlyList AllRelevantSet = Enum.GetValues().Where(f => AllRelevant.HasFlag(f)).ToArray(); - - public static int ToInternalIndex(this CrestFlag flag) - => flag switch - { - CrestFlag.Head => 0, - CrestFlag.Body => 1, - CrestFlag.OffHand => 2, - _ => -1, - }; - - public static (CrestType Type, byte Index) ToIndex(this CrestFlag flag) - => flag switch - { - CrestFlag.Head => (CrestType.Human, 0), - CrestFlag.Body => (CrestType.Human, 1), - CrestFlag.Hands => (CrestType.Human, 2), - CrestFlag.Legs => (CrestType.Human, 3), - CrestFlag.Feet => (CrestType.Human, 4), - CrestFlag.Ears => (CrestType.None, 0), - CrestFlag.Neck => (CrestType.None, 0), - CrestFlag.Wrists => (CrestType.None, 0), - CrestFlag.RFinger => (CrestType.None, 0), - CrestFlag.LFinger => (CrestType.None, 0), - CrestFlag.MainHand => (CrestType.None, 0), - CrestFlag.OffHand => (CrestType.Offhand, 0), - _ => (CrestType.None, 0), - }; - - public static CrestFlag ToCrestFlag(this EquipSlot slot) - => slot switch - { - EquipSlot.MainHand => CrestFlag.MainHand, - EquipSlot.OffHand => CrestFlag.OffHand, - EquipSlot.Head => CrestFlag.Head, - EquipSlot.Body => CrestFlag.Body, - EquipSlot.Hands => CrestFlag.Hands, - EquipSlot.Legs => CrestFlag.Legs, - EquipSlot.Feet => CrestFlag.Feet, - EquipSlot.Ears => CrestFlag.Ears, - EquipSlot.Neck => CrestFlag.Neck, - EquipSlot.Wrists => CrestFlag.Wrists, - EquipSlot.RFinger => CrestFlag.RFinger, - EquipSlot.LFinger => CrestFlag.LFinger, - _ => 0, - }; - - public static string ToLabel(this CrestFlag flag) - => flag switch - { - CrestFlag.Head => "Head", - CrestFlag.Body => "Chest", - CrestFlag.Hands => "Gauntlets", - CrestFlag.Legs => "Pants", - CrestFlag.Feet => "Boot", - CrestFlag.Ears => "Earrings", - CrestFlag.Neck => "Necklace", - CrestFlag.Wrists => "Bracelet", - CrestFlag.RFinger => "Right Ring", - CrestFlag.LFinger => "Left Ring", - CrestFlag.MainHand => "Weapon", - CrestFlag.OffHand => "Shield", - _ => string.Empty, - }; -} 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..9acdd6c 100644 --- a/Glamourer.sln +++ b/Glamourer.sln @@ -9,8 +9,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution repo.json = repo.json 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}" @@ -27,10 +25,6 @@ Global 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 diff --git a/Glamourer/Api/GlamourerIpc.GetCustomization.cs b/Glamourer/Api/GlamourerIpc.GetCustomization.cs index 3e8794c..cdec349 100644 --- a/Glamourer/Api/GlamourerIpc.GetCustomization.cs +++ b/Glamourer/Api/GlamourerIpc.GetCustomization.cs @@ -1,11 +1,7 @@ -using System.Buffers.Text; -using System.Collections.Generic; +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; diff --git a/Glamourer/Api/GlamourerIpc.Set.cs b/Glamourer/Api/GlamourerIpc.Set.cs new file mode 100644 index 0000000..5a6405d --- /dev/null +++ b/Glamourer/Api/GlamourerIpc.Set.cs @@ -0,0 +1,92 @@ +using System.Linq; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin; +using Glamourer.Events; +using Glamourer.Services; +using Penumbra.Api.Helpers; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Api; + +public partial class GlamourerIpc +{ + public enum GlamourerErrorCode + { + Success, + ActorNotFound, + ActorNotHuman, + ItemInvalid, + } + + public const string LabelSetItem = "Glamourer.SetItem"; + public const string LabelSetItemByActorName = "Glamourer.SetItemByActorName"; + + + private readonly FuncProvider _setItemProvider; + private readonly FuncProvider _setItemByActorNameProvider; + + public static FuncSubscriber SetItemSubscriber(DalamudPluginInterface pi) + => new(pi, LabelSetItem); + + public static FuncSubscriber SetItemByActorNameSubscriber(DalamudPluginInterface pi) + => new(pi, LabelSetItemByActorName); + + private GlamourerErrorCode SetItem(Character? character, EquipSlot slot, CustomItemId itemId, StainId stainId, uint key) + { + if (itemId.Id == 0) + itemId = ItemManager.NothingId(slot); + + var item = _items.Resolve(slot, itemId); + if (!item.Valid) + return GlamourerErrorCode.ItemInvalid; + + var identifier = _actors.FromObject(character, false, false, false); + if (!identifier.IsValid) + return GlamourerErrorCode.ActorNotFound; + + if (!_stateManager.TryGetValue(identifier, out var state)) + { + _objects.Update(); + var data = _objects[identifier]; + if (!data.Valid || !_stateManager.GetOrCreate(identifier, data.Objects[0], out state)) + return GlamourerErrorCode.ActorNotFound; + } + + if (!state.ModelData.IsHuman) + return GlamourerErrorCode.ActorNotHuman; + + _stateManager.ChangeEquip(state, slot, item, stainId, StateChanged.Source.Ipc, key); + return GlamourerErrorCode.Success; + } + + private GlamourerErrorCode SetItemByActorName(string name, EquipSlot slot, CustomItemId itemId, StainId stainId, uint key) + { + if (itemId.Id == 0) + itemId = ItemManager.NothingId(slot); + + var item = _items.Resolve(slot, itemId); + if (!item.Valid) + return GlamourerErrorCode.ItemInvalid; + + var found = false; + _objects.Update(); + foreach (var identifier in FindActorsRevert(name).Distinct()) + { + if (!_stateManager.TryGetValue(identifier, out var state)) + { + var data = _objects[identifier]; + if (!data.Valid || !_stateManager.GetOrCreate(identifier, data.Objects[0], out state)) + continue; + } + + if (!state.ModelData.IsHuman) + return GlamourerErrorCode.ActorNotHuman; + + _stateManager.ChangeEquip(state, slot, item, stainId, StateChanged.Source.Ipc, key); + found = true; + } + + return found ? GlamourerErrorCode.Success : GlamourerErrorCode.ActorNotFound; + } +} diff --git a/Glamourer/Api/GlamourerIpc.cs b/Glamourer/Api/GlamourerIpc.cs index 7e8781e..b4c29b4 100644 --- a/Glamourer/Api/GlamourerIpc.cs +++ b/Glamourer/Api/GlamourerIpc.cs @@ -11,31 +11,34 @@ using Glamourer.Services; using Glamourer.State; using Penumbra.Api.Helpers; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; using Penumbra.String; namespace Glamourer.Api; -public partial class GlamourerIpc : IDisposable +public sealed 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 ActorManager _actors; private readonly DesignConverter _designConverter; private readonly AutoDesignApplier _autoDesignApplier; private readonly DesignManager _designManager; + private readonly ItemManager _items; - public GlamourerIpc(DalamudPluginInterface pi, StateManager stateManager, ObjectManager objects, ActorService actors, + public GlamourerIpc(DalamudPluginInterface pi, StateManager stateManager, ObjectManager objects, ActorManager actors, DesignConverter designConverter, StateChanged stateChangedEvent, GPoseService gPose, AutoDesignApplier autoDesignApplier, - DesignManager designManager) + DesignManager designManager, ItemManager items) { _stateManager = stateManager; _objects = objects; _actors = actors; _designConverter = designConverter; _autoDesignApplier = autoDesignApplier; + _items = items; _gPose = gPose; _stateChangedEvent = stateChangedEvent; _designManager = designManager; @@ -82,6 +85,11 @@ public partial class GlamourerIpc : IDisposable _stateChangedProvider = new EventProvider>(pi, LabelStateChanged); _gPoseChangedProvider = new EventProvider(pi, LabelGPoseChanged); + _setItemProvider = new FuncProvider(pi, LabelSetItem, + (idx, slot, item, stain, key) => (int)SetItem(idx, (EquipSlot)slot, item, stain, key)); + _setItemByActorNameProvider = new FuncProvider(pi, LabelSetItemByActorName, + (name, slot, item, stain, key) => (int)SetItemByActorName(name, (EquipSlot)slot, item, stain, key)); + _stateChangedEvent.Subscribe(OnStateChanged, StateChanged.Priority.GlamourerIpc); _gPose.Subscribe(OnGPoseChanged, GPoseService.Priority.GlamourerIpc); @@ -126,6 +134,9 @@ public partial class GlamourerIpc : IDisposable _gPoseChangedProvider.Dispose(); _getDesignListProvider.Dispose(); + + _setItemProvider.Dispose(); + _setItemByActorNameProvider.Dispose(); } private IEnumerable FindActors(string actorName) @@ -154,7 +165,7 @@ public partial class GlamourerIpc : IDisposable private IEnumerable FindActors(Character? character) { - var id = _actors.AwaitedService.FromObject(character, true, true, false); + var id = _actors.FromObject(character, true, true, false); if (!id.IsValid) yield break; diff --git a/Glamourer/Automation/AutoDesign.cs b/Glamourer/Automation/AutoDesign.cs index 0a26759..029a2ec 100644 --- a/Glamourer/Automation/AutoDesign.cs +++ b/Glamourer/Automation/AutoDesign.cs @@ -1,10 +1,10 @@ using System; -using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Interop.Structs; using Glamourer.State; -using Glamourer.Structs; using Newtonsoft.Json.Linq; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Glamourer.Automation; @@ -74,7 +74,7 @@ public class AutoDesign var ret = new JObject { ["Gearset"] = GearsetIndex, - ["JobGroup"] = Jobs.Id, + ["JobGroup"] = Jobs.Id.Id, }; return ret; diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index deb9e43..83ce3c4 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -4,18 +4,16 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.UI.Misc; -using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Events; using Glamourer.Interop; using Glamourer.Interop.Structs; using Glamourer.Services; using Glamourer.State; -using Glamourer.Structs; using Glamourer.Unlocks; using OtterGui.Classes; using Penumbra.GameData.Actors; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -28,8 +26,8 @@ public class AutoDesignApplier : IDisposable private readonly StateManager _state; private readonly JobService _jobs; private readonly EquippedGearset _equippedGearset; - private readonly ActorService _actors; - private readonly CustomizationService _customizations; + private readonly ActorManager _actors; + private readonly CustomizeService _customizations; private readonly CustomizeUnlockManager _customizeUnlocks; private readonly ItemUnlockManager _itemUnlocks; private readonly AutomationChanged _event; @@ -50,7 +48,7 @@ public class AutoDesignApplier : IDisposable } public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, - CustomizationService customizations, ActorService actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks, + CustomizeService customizations, ActorManager actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks, AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState, EquippedGearset equippedGearset) { @@ -87,7 +85,7 @@ public class AutoDesignApplier : IDisposable if (_jobChangeState == null || !_config.EnableAutoDesigns) return; - var id = actor.GetIdentifier(_actors.AwaitedService); + var id = actor.GetIdentifier(_actors); if (id == _jobChangeState.Identifier) { var current = _jobChangeState.BaseData.Item(slot); @@ -161,7 +159,7 @@ public class AutoDesignApplier : IDisposable { foreach (var actor in data.Objects) { - var specificId = actor.GetIdentifier(_actors.AwaitedService); + var specificId = actor.GetIdentifier(_actors); if (_state.GetOrCreate(specificId, actor, out var state)) { Reduce(actor, state, newSet, false, false); @@ -203,13 +201,13 @@ public class AutoDesignApplier : IDisposable 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; } @@ -312,13 +310,13 @@ public class AutoDesignApplier : IDisposable if (_manager.EnabledSets.TryGetValue(identifier, out set)) return true; - identifier = _actors.AwaitedService.CreatePlayer(identifier.PlayerName, ushort.MaxValue); + identifier = _actors.CreatePlayer(identifier.PlayerName, ushort.MaxValue); 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); + identifier = _actors.CreateNpc(identifier.Kind, identifier.DataId); return _manager.EnabledSets.TryGetValue(identifier, out set); default: set = null; @@ -470,7 +468,7 @@ public class AutoDesignApplier : IDisposable totalCustomizeFlags |= CustomizeFlag.Face; } - var set = _customizations.AwaitedService.GetList(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender); + var set = _customizations.Manager.GetSet(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender); var face = state.ModelData.Customize.Face; foreach (var index in Enum.GetValues()) { @@ -479,7 +477,7 @@ public class AutoDesignApplier : IDisposable continue; var value = design.Customize[index]; - if (CustomizationService.IsCustomizationValid(set, face, index, value, out var data)) + if (CustomizeService.IsCustomizationValid(set, face, index, value, out var data)) { if (data.HasValue && _config.UnlockedItemMode && !_customizeUnlocks.IsUnlocked(data.Value, out _)) continue; diff --git a/Glamourer/Automation/AutoDesignManager.cs b/Glamourer/Automation/AutoDesignManager.cs index d3fba5c..e205784 100644 --- a/Glamourer/Automation/AutoDesignManager.cs +++ b/Glamourer/Automation/AutoDesignManager.cs @@ -10,13 +10,14 @@ using Glamourer.Designs; 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.Filesystem; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Glamourer.Automation; @@ -28,17 +29,17 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos private readonly JobService _jobs; private readonly DesignManager _designs; - private readonly ActorService _actors; + private readonly ActorManager _actors; private readonly AutomationChanged _event; private readonly DesignChanged _designEvent; - 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, + public AutoDesignManager(JobService jobs, ActorManager actors, SaveService saveService, DesignManager designs, AutomationChanged @event, FixedDesignMigrator migrator, DesignFileSystem fileSystem, DesignChanged designEvent) { _jobs = jobs; @@ -419,7 +420,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); @@ -514,7 +515,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos 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.", @@ -562,9 +563,9 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos 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, @@ -580,12 +581,12 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos }, IdentifierType.Retainer => new[] { - _actors.AwaitedService.CreateRetainer(identifier.PlayerName, + _actors.CreateRetainer(identifier.PlayerName, identifier.Retainer == ActorIdentifier.RetainerType.Mannequin ? ActorIdentifier.RetainerType.Mannequin : ActorIdentifier.RetainerType.Bell).CreatePermanent(), }, - IdentifierType.Npc => CreateNpcs(_actors.AwaitedService, identifier), + IdentifierType.Npc => CreateNpcs(_actors, identifier), _ => Array.Empty(), }; } diff --git a/Glamourer/Automation/FixedDesignMigrator.cs b/Glamourer/Automation/FixedDesignMigrator.cs index 0dc20b4..4ed98d3 100644 --- a/Glamourer/Automation/FixedDesignMigrator.cs +++ b/Glamourer/Automation/FixedDesignMigrator.cs @@ -3,59 +3,51 @@ using System.Linq; using Dalamud.Interface.Internal.Notifications; using Glamourer.Designs; using Glamourer.Interop; -using Glamourer.Services; -using Glamourer.Structs; using Newtonsoft.Json.Linq; using 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) { @@ -96,7 +88,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/Designs/Design.cs b/Glamourer/Designs/Design.cs index d10fe29..7382fce 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -15,7 +15,7 @@ public sealed class Design : DesignBase, ISavable { #region Data - internal Design(CustomizationService customize, ItemManager items) + internal Design(CustomizeService customize, ItemManager items) : base(customize, items) { } @@ -98,7 +98,7 @@ public sealed class Design : DesignBase, ISavable #region Deserialization - public static Design LoadDesign(CustomizationService customizations, ItemManager items, JObject json) + public static Design LoadDesign(CustomizeService customizations, ItemManager items, JObject json) { var version = json["FileVersion"]?.ToObject() ?? 0; return version switch @@ -108,7 +108,7 @@ public sealed class Design : DesignBase, ISavable }; } - private static Design LoadDesignV1(CustomizationService customizations, ItemManager items, JObject json) + private static Design LoadDesignV1(CustomizeService customizations, ItemManager items, JObject json) { static string[] ParseTags(JObject json) { diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs index d859e8e..aeb48e0 100644 --- a/Glamourer/Designs/DesignBase.cs +++ b/Glamourer/Designs/DesignBase.cs @@ -1,14 +1,13 @@ using Dalamud.Interface.Internal.Notifications; -using Glamourer.Customization; +using Glamourer.GameData; 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 System; using System.Linq; +using Penumbra.GameData.DataContainers; namespace Glamourer.Designs; @@ -26,35 +25,35 @@ public class DesignBase public ref DesignData GetDesignDataRef() => ref _designData; - internal DesignBase(CustomizationService customize, ItemManager items) + internal DesignBase(CustomizeService customize, ItemManager items) { _designData.SetDefaultEquipment(items); - CustomizationSet = SetCustomizationSet(customize); + CustomizeSet = SetCustomizationSet(customize); } - internal DesignBase(CustomizationService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags) + internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags) { - _designData = designData; - ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant; - ApplyEquip = equipFlags & EquipFlagExtensions.All; - _designFlags = 0; - CustomizationSet = SetCustomizationSet(customize); + _designData = designData; + ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant; + ApplyEquip = equipFlags & EquipFlagExtensions.All; + _designFlags = 0; + CustomizeSet = SetCustomizationSet(customize); } internal DesignBase(DesignBase clone) { - _designData = clone._designData; - CustomizationSet = clone.CustomizationSet; - ApplyCustomize = clone.ApplyCustomizeRaw; - ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All; - _designFlags = clone._designFlags & (DesignFlags)0x0F; + _designData = clone._designData; + CustomizeSet = clone.CustomizeSet; + ApplyCustomize = clone.ApplyCustomizeRaw; + ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All; + _designFlags = clone._designFlags & (DesignFlags)0x0F; } /// Ensure that the customization set is updated when the design data changes. - internal void SetDesignData(CustomizationService customize, in DesignData other) + internal void SetDesignData(CustomizeService customize, in DesignData other) { - _designData = other; - CustomizationSet = SetCustomizationSet(customize); + _designData = other; + CustomizeSet = SetCustomizationSet(customize); } #region Application Data @@ -69,15 +68,18 @@ public class DesignBase WriteProtected = 0x10, } - private CustomizeFlag _applyCustomize = CustomizeFlagExtensions.AllRelevant; - public CustomizationSet CustomizationSet { get; private set; } + private CustomizeFlag _applyCustomize = CustomizeFlagExtensions.AllRelevant; + public CustomizeSet CustomizeSet { get; private set; } internal CustomizeFlag ApplyCustomize { - get => _applyCustomize.FixApplication(CustomizationSet); - set => _applyCustomize = value & CustomizeFlagExtensions.AllRelevant; + get => _applyCustomize.FixApplication(CustomizeSet); + set => _applyCustomize = (value & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType; } + internal CustomizeFlag ApplyCustomizeExcludingBodyType + => _applyCustomize.FixApplication(CustomizeSet) & ~CustomizeFlag.BodyType; + internal CustomizeFlag ApplyCustomizeRaw => _applyCustomize; @@ -85,13 +87,13 @@ public class DesignBase internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant; private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible; - public bool SetCustomize(CustomizationService customizationService, Customize customize) + public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize) { if (customize.Equals(_designData.Customize)) return false; - _designData.Customize.Load(customize); - CustomizationSet = customizationService.AwaitedService.GetList(customize.Clan, customize.Gender); + _designData.Customize = customize; + CustomizeSet = customizeService.Manager.GetSet(customize.Clan, customize.Gender); return true; } @@ -241,10 +243,10 @@ public class DesignBase } } - private CustomizationSet SetCustomizationSet(CustomizationService customize) + private CustomizeSet SetCustomizationSet(CustomizeService customize) => !_designData.IsHuman - ? customize.AwaitedService.GetList(SubRace.Midlander, Gender.Male) - : customize.AwaitedService.GetList(_designData.Customize.Clan, _designData.Customize.Gender); + ? customize.Manager.GetSet(SubRace.Midlander, Gender.Male) + : customize.Manager.GetSet(_designData.Customize.Clan, _designData.Customize.Gender); #endregion @@ -331,7 +333,7 @@ public class DesignBase #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 @@ -341,7 +343,7 @@ 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(customizations, items); LoadCustomize(customizations, json["Customize"], ret, "Temporary Design", false, true); @@ -436,14 +438,14 @@ public class DesignBase 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.SetCustomize(customizations, Customize.Default); + design.SetCustomize(customizations, CustomizeArray.Default); Glamourer.Messager.NotificationMessage("The loaded design does not contain any customization data, reset to default.", NotificationType.Warning); return; @@ -474,7 +476,7 @@ public class DesignBase { var arrayText = json["Array"]?.ToObject() ?? string.Empty; design._designData.Customize.LoadBase64(arrayText); - design.CustomizationSet = design.SetCustomizationSet(customizations); + design.CustomizeSet = design.SetCustomizationSet(customizations); return; } @@ -486,18 +488,18 @@ public class DesignBase design._designData.Customize.Race = race; design._designData.Customize.Clan = clan; design._designData.Customize.Gender = gender; - design.CustomizationSet = design.SetCustomizationSet(customizations); + 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); - var set = design.CustomizationSet; + var set = design.CustomizeSet; foreach (var idx in CustomizationExtensions.AllBasic) { var tok = json[idx.ToString()]; var data = (CustomizeValue)(tok?["Value"]?.ToObject() ?? 0); - if (set.IsAvailable(idx)) - PrintWarning(CustomizationService.ValidateCustomizeValue(set, design._designData.Customize.Face, idx, data, out data, + 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; @@ -505,7 +507,7 @@ public class DesignBase } } - public void MigrateBase64(CustomizationService customize, ItemManager items, HumanModelList humans, string base64) + public void MigrateBase64(CustomizeService customize, ItemManager items, HumanModelList humans, string base64) { try { @@ -519,7 +521,7 @@ public class DesignBase SetApplyVisorToggle(applyVisor); SetApplyWeaponVisible(applyWeapon); SetApplyWetness(true); - CustomizationSet = SetCustomizationSet(customize); + CustomizeSet = SetCustomizationSet(customize); } catch (Exception ex) { diff --git a/Glamourer/Designs/DesignBase64Migration.cs b/Glamourer/Designs/DesignBase64Migration.cs index 18cef7a..d4352e6 100644 --- a/Glamourer/Designs/DesignBase64Migration.cs +++ b/Glamourer/Designs/DesignBase64Migration.cs @@ -1,9 +1,7 @@ using System; -using Glamourer.Customization; using Glamourer.Services; -using Glamourer.Structs; using OtterGui; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -62,7 +60,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,7 +71,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; default: throw new Exception($"Can not parse Base64 string into design for migration:\n\tInvalid Version {bytes[0]}."); } @@ -102,11 +100,11 @@ public class DesignBase64Migration 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 +119,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 +133,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 +145,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) @@ -187,7 +185,7 @@ 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); + save.Customize.Write(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)); foreach (var slot in EquipSlotExtensions.EqdpSlots) diff --git a/Glamourer/Designs/DesignColors.cs b/Glamourer/Designs/DesignColors.cs index dc36e78..fa257c6 100644 --- a/Glamourer/Designs/DesignColors.cs +++ b/Glamourer/Designs/DesignColors.cs @@ -26,9 +26,9 @@ public class DesignColorUi public DesignColorUi(DesignColors colors, DesignManager designs, Configuration config) { - _colors = colors; - _designs = designs; - _config = config; + _colors = colors; + _designs = designs; + _config = config; } public void Draw() @@ -78,7 +78,7 @@ public class DesignColorUi { using var id = ImRaii.PushId(idx); ImGui.TableNextColumn(); - + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), buttonSize, tt, disabled, true)) { changeString = name; @@ -119,7 +119,7 @@ public class DesignColorUi changeString = _newName; changeValue = 0xFFFFFFFF; } - + if (changeString.Length > 0) { @@ -139,6 +139,7 @@ public class DesignColorUi newColor = color; return false; } + ImGuiUtil.HoverTooltip(tooltip); newColor = ImGui.ColorConvertFloat4ToU32(vec); @@ -148,12 +149,12 @@ public class DesignColorUi public class DesignColors : ISavable, IReadOnlyDictionary { - public const string AutomaticName = "Automatic"; + public const string AutomaticName = "Automatic"; public const string MissingColorName = "Missing Color"; public const uint MissingColorDefault = 0xFF0000D0; private readonly SaveService _saveService; - private readonly Dictionary _colors = new(); + private readonly Dictionary _colors = []; public uint MissingColor { get; private set; } = MissingColorDefault; public event Action? ColorChanged; @@ -281,7 +282,7 @@ public class DesignColors : ISavable, IReadOnlyDictionary public static uint AutoColor(DesignBase design) { - var customize = design.ApplyCustomize == 0; + var customize = design.ApplyCustomizeExcludingBodyType == 0; var equip = design.ApplyEquip == 0; return (customize, equip) switch { diff --git a/Glamourer/Designs/DesignConverter.cs b/Glamourer/Designs/DesignConverter.cs index f1ee47c..cd8924f 100644 --- a/Glamourer/Designs/DesignConverter.cs +++ b/Glamourer/Designs/DesignConverter.cs @@ -2,20 +2,18 @@ using System.Collections.Generic; using System.Diagnostics; using System.Text; -using Glamourer.Customization; 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.Structs; namespace Glamourer.Designs; -public class DesignConverter(ItemManager _items, DesignManager _designs, CustomizationService _customize, HumanModelList _humans) +public class DesignConverter(ItemManager _items, DesignManager _designs, CustomizeService _customize, HumanModelList _humans) { public const byte Version = 6; @@ -41,12 +39,18 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi => ShareBase64(state, EquipFlagExtensions.All, CustomizeFlagExtensions.All, CrestExtensions.All); public string ShareBase64(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags) + => ShareBase64(state.ModelData, equipFlags, customizeFlags, crestFlags); + + public string ShareBase64(in DesignData data, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags) { - var design = Convert(state, equipFlags, customizeFlags, crestFlags); + var design = Convert(data, equipFlags, customizeFlags, crestFlags); return ShareBase64(ShareJObject(design)); } public DesignBase Convert(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags) + => Convert(state.ModelData, equipFlags, customizeFlags, crestFlags); + + public DesignBase Convert(in DesignData data, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags) { var design = _designs.CreateTemporary(); design.ApplyEquip = equipFlags & EquipFlagExtensions.All; @@ -56,7 +60,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi design.SetApplyVisorToggle(design.DoApplyEquip(EquipSlot.Head)); design.SetApplyWeaponVisible(design.DoApplyEquip(EquipSlot.MainHand) || design.DoApplyEquip(EquipSlot.OffHand)); design.SetApplyWetness(true); - design.SetDesignData(_customize, state.ModelData); + design.SetDesignData(_customize, data); return design; } @@ -146,7 +150,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi } public IEnumerable<(EquipSlot Slot, EquipItem Item, StainId Stain)> FromDrawData(IReadOnlyList armors, - CharacterWeapon mainhand, CharacterWeapon offhand) + CharacterWeapon mainhand, CharacterWeapon offhand, bool skipWarnings) { if (armors.Count != 10) throw new ArgumentException("Invalid length of armor array."); @@ -158,15 +162,16 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi var item = _items.Identify(slot, armor.Set, armor.Variant); if (!item.Valid) { - Glamourer.Log.Warning($"Appearance data {armor} for slot {slot} invalid, item could not be identified."); + 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.Stain); } - var mh = _items.Identify(EquipSlot.MainHand, mainhand.Set, mainhand.Type, mainhand.Variant, FullEquipType.Unknown); - if (!mh.Valid) + 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; @@ -174,8 +179,8 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi yield return (EquipSlot.MainHand, mh, mainhand.Stain); - var oh = _items.Identify(EquipSlot.OffHand, offhand.Set, offhand.Type, offhand.Variant, mh.Type); - if (!oh.Valid) + 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); diff --git a/Glamourer/Designs/DesignData.cs b/Glamourer/Designs/DesignData.cs index 4b0d53b..9e324ae 100644 --- a/Glamourer/Designs/DesignData.cs +++ b/Glamourer/Designs/DesignData.cs @@ -1,42 +1,39 @@ using System; using System.Runtime.CompilerServices; -using Glamourer.Customization; using Glamourer.Services; -using Glamourer.Structs; 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; - public CrestFlag CrestVisibility; - private WeaponType _secondaryMainhand; - private WeaponType _secondaryOffhand; - private FullEquipType _typeMainhand; - private FullEquipType _typeOffhand; - private byte _states; - public bool IsHuman = true; + 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 CustomizeArray Customize = CustomizeArray.Default; + public uint ModelId; + public CrestFlag CrestVisibility; + private SecondaryId _secondaryMainhand; + private SecondaryId _secondaryOffhand; + private FullEquipType _typeMainhand; + private FullEquipType _typeOffhand; + private byte _states; + public bool IsHuman = true; public DesignData() { } @@ -75,18 +72,18 @@ public unsafe struct DesignData => slot.ToIndex() 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 ), + 0 => EquipItem.FromIds((ItemId)_itemIds[ 0], (IconId)_iconIds[ 0], (PrimaryId)(_equipmentBytes[ 0] | (_equipmentBytes[ 1] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[ 2], FullEquipType.Head, name: _nameHead ), + 1 => EquipItem.FromIds((ItemId)_itemIds[ 1], (IconId)_iconIds[ 1], (PrimaryId)(_equipmentBytes[ 4] | (_equipmentBytes[ 5] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[ 6], FullEquipType.Body, name: _nameBody ), + 2 => EquipItem.FromIds((ItemId)_itemIds[ 2], (IconId)_iconIds[ 2], (PrimaryId)(_equipmentBytes[ 8] | (_equipmentBytes[ 9] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[10], FullEquipType.Hands, name: _nameHands ), + 3 => EquipItem.FromIds((ItemId)_itemIds[ 3], (IconId)_iconIds[ 3], (PrimaryId)(_equipmentBytes[12] | (_equipmentBytes[13] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[14], FullEquipType.Legs, name: _nameLegs ), + 4 => EquipItem.FromIds((ItemId)_itemIds[ 4], (IconId)_iconIds[ 4], (PrimaryId)(_equipmentBytes[16] | (_equipmentBytes[17] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[18], FullEquipType.Feet, name: _nameFeet ), + 5 => EquipItem.FromIds((ItemId)_itemIds[ 5], (IconId)_iconIds[ 5], (PrimaryId)(_equipmentBytes[20] | (_equipmentBytes[21] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[22], FullEquipType.Ears, name: _nameEars ), + 6 => EquipItem.FromIds((ItemId)_itemIds[ 6], (IconId)_iconIds[ 6], (PrimaryId)(_equipmentBytes[24] | (_equipmentBytes[25] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[26], FullEquipType.Neck, name: _nameNeck ), + 7 => EquipItem.FromIds((ItemId)_itemIds[ 7], (IconId)_iconIds[ 7], (PrimaryId)(_equipmentBytes[28] | (_equipmentBytes[29] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[30], FullEquipType.Wrists, name: _nameWrists ), + 8 => EquipItem.FromIds((ItemId)_itemIds[ 8], (IconId)_iconIds[ 8], (PrimaryId)(_equipmentBytes[32] | (_equipmentBytes[33] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[34], FullEquipType.Finger, name: _nameRFinger ), + 9 => EquipItem.FromIds((ItemId)_itemIds[ 9], (IconId)_iconIds[ 9], (PrimaryId)(_equipmentBytes[36] | (_equipmentBytes[37] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[38], FullEquipType.Finger, name: _nameLFinger ), + 10 => EquipItem.FromIds((ItemId)_itemIds[10], (IconId)_iconIds[10], (PrimaryId)(_equipmentBytes[40] | (_equipmentBytes[41] << 8)), _secondaryMainhand, (Variant)_equipmentBytes[42], _typeMainhand, name: _nameMainhand), + 11 => EquipItem.FromIds((ItemId)_itemIds[11], (IconId)_iconIds[11], (PrimaryId)(_equipmentBytes[44] | (_equipmentBytes[45] << 8)), _secondaryOffhand, (Variant)_equipmentBytes[46], _typeOffhand, name: _nameOffhand ), _ => new EquipItem(), // @formatter:on }; @@ -129,8 +126,8 @@ public unsafe struct DesignData _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 + 0] = (byte)item.PrimaryId.Id; + _equipmentBytes[4 * index + 1] = (byte)(item.PrimaryId.Id >> 8); _equipmentBytes[4 * index + 2] = item.Variant.Id; switch (index) { @@ -148,12 +145,12 @@ public unsafe struct DesignData // @formatter:on case 10: _nameMainhand = item.Name; - _secondaryMainhand = item.WeaponType; + _secondaryMainhand = item.SecondaryId; _typeMainhand = item.Type; return true; case 11: _nameOffhand = item.Name; - _secondaryOffhand = item.WeaponType; + _secondaryOffhand = item.SecondaryId; _typeOffhand = item.Type; return true; } @@ -256,11 +253,11 @@ public unsafe struct DesignData } - 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); @@ -294,8 +291,8 @@ public unsafe struct DesignData 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); } diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index 392301f..b46e4e0 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -1,18 +1,20 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Dalamud.Utility; -using Glamourer.Customization; using Glamourer.Events; using Glamourer.Interop.Penumbra; using Glamourer.Services; using Glamourer.State; -using Glamourer.Structs; 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; @@ -20,18 +22,18 @@ namespace Glamourer.Designs; public class DesignManager { - private readonly CustomizationService _customizations; + private readonly CustomizeService _customizations; private readonly ItemManager _items; private readonly HumanModelList _humans; private readonly SaveService _saveService; private readonly DesignChanged _event; - private readonly List _designs = new(); - private readonly Dictionary _undoStore = new(); + private readonly List _designs = []; + private readonly Dictionary _undoStore = []; public IReadOnlyList Designs => _designs; - public DesignManager(SaveService saveService, ItemManager items, CustomizationService customizations, + public DesignManager(SaveService saveService, ItemManager items, CustomizeService customizations, DesignChanged @event, HumanModelList humans) { _saveService = saveService; @@ -50,29 +52,44 @@ public class DesignManager /// public void LoadDesigns() { + _humans.Awaiter.Wait(); + _customizations.Awaiter.Wait(); + _items.ItemData.Awaiter.Wait(); + + var stopwatch = Stopwatch.StartNew(); _designs.Clear(); - List<(Design, string)> invalidNames = new(); - var skipped = 0; - foreach (var file in _saveService.FileNames.Designs()) + 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); + 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.Any(d => d.Identifier == 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); @@ -81,7 +98,7 @@ 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)}"); + $"Loaded {_designs.Count} designs in {stopwatch.ElapsedMilliseconds} ms.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}"); _event.Invoke(DesignChanged.Type.ReloadedAll, null!); } @@ -193,10 +210,10 @@ public class DesignManager public void ChangeColor(Design design, string newColor) { var oldColor = design.Color; - if (oldColor == newColor) + if (oldColor == newColor) return; - design.Color = newColor; + design.Color = newColor; design.LastEdit = DateTimeOffset.UtcNow; _saveService.QueueSave(design); Glamourer.Log.Debug($"Changed color of design {design.Identifier}."); @@ -298,7 +315,7 @@ public class DesignManager return; case CustomizeIndex.Clan: { - var customize = new Customize(design.DesignData.Customize.Data.Clone()); + var customize = design.DesignData.Customize; if (_customizations.ChangeClan(ref customize, (SubRace)value.Value) == 0) return; if (!design.SetCustomize(_customizations, customize)) @@ -308,7 +325,7 @@ public class DesignManager } case CustomizeIndex.Gender: { - var customize = new Customize(design.DesignData.Customize.Data.Clone()); + var customize = design.DesignData.Customize; if (_customizations.ChangeGender(ref customize, (Gender)(value.Value + 1)) == 0) return; if (!design.SetCustomize(_customizations, customize)) diff --git a/Glamourer.GameData/Customization/CharaMakeParams.cs b/Glamourer/GameData/CharaMakeParams.cs similarity index 94% rename from Glamourer.GameData/Customization/CharaMakeParams.cs rename to Glamourer/GameData/CharaMakeParams.cs index 63806c0..4db5825 100644 --- a/Glamourer.GameData/Customization/CharaMakeParams.cs +++ b/Glamourer/GameData/CharaMakeParams.cs @@ -2,9 +2,9 @@ using Lumina.Excel; using Lumina.Excel.GeneratedSheets; -namespace Glamourer.Customization; +namespace Glamourer.GameData; -// A custom version of CharaMakeParams that is easier to parse. +/// A custom version of CharaMakeParams that is easier to parse. [Sheet("CharaMakeParams")] public class CharaMakeParams : ExcelRow { @@ -64,7 +64,7 @@ public class CharaMakeParams : ExcelRow Race = new LazyRow(gameData, parser.ReadColumn(0), language); Tribe = new LazyRow(gameData, parser.ReadColumn(1), language); Gender = parser.ReadColumn(2); - var currentOffset = 0; + int currentOffset; for (var i = 0; i < NumMenus; ++i) { currentOffset = 3 + i; diff --git a/Glamourer/GameData/ColorParameters.cs b/Glamourer/GameData/ColorParameters.cs new file mode 100644 index 0000000..975e003 --- /dev/null +++ b/Glamourer/GameData/ColorParameters.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections; +using System.Collections.Generic; +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 52% rename from Glamourer.GameData/Customization/CustomizeData.cs rename to Glamourer/GameData/CustomizeData.cs index 8e9f914..4d4e2fd 100644 --- a/Glamourer.GameData/Customization/CustomizeData.cs +++ b/Glamourer/GameData/CustomizeData.cs @@ -1,28 +1,38 @@ 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 +42,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..a78cdc0 --- /dev/null +++ b/Glamourer/GameData/CustomizeManager.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Dalamud.Interface.Internal; +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 IDalamudTextureWrap GetIcon(uint id) + => _icons.LoadIcon(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 IconStorage(textures, gameData); + 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 IconStorage _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/Customization/CustomizationSet.cs b/Glamourer/GameData/CustomizeSet.cs similarity index 91% rename from Glamourer.GameData/Customization/CustomizationSet.cs rename to Glamourer/GameData/CustomizeSet.cs index b958fdc..7c98903 100644 --- a/Glamourer.GameData/Customization/CustomizationSet.cs +++ b/Glamourer/GameData/CustomizeSet.cs @@ -4,14 +4,17 @@ using System.Linq; using System.Runtime.CompilerServices; using OtterGui; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; -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) + internal CustomizeSet(SubRace clan, Gender gender) { Gender = gender; Clan = clan; @@ -23,6 +26,8 @@ public class CustomizationSet 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,7 +37,7 @@ 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]; @@ -94,68 +99,6 @@ public class CustomizationSet { var type = Types[(int)index]; - int GetInteger0(out CustomizeData? custom) - { - if (value < Count(index)) - { - custom = new CustomizeData(index, value, 0, value.Value); - return value.Value; - } - - custom = null; - return -1; - } - - int GetInteger1(out CustomizeData? custom) - { - if (value > 0 && value < Count(index) + 1) - { - custom = new CustomizeData(index, value, 0, (ushort)(value.Value - 1)); - return value.Value; - } - - custom = null; - return -1; - } - - static int GetBool(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom) - { - if (value == CustomizeValue.Zero) - { - 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 = 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), @@ -193,6 +136,68 @@ public class CustomizationSet CharaMakeParams.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; + return -1; + } + + int GetInteger1(out CustomizeData? custom) + { + if (value > 0 && value < Count(index) + 1) + { + custom = new CustomizeData(index, value, 0, (ushort)(value.Value - 1)); + return value.Value; + } + + custom = null; + return -1; + } + + int GetInteger0(out CustomizeData? custom) + { + if (value < Count(index)) + { + custom = new CustomizeData(index, value, 0, value.Value); + return value.Value; + } + + custom = null; + return -1; + } } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] @@ -243,19 +248,6 @@ public class CustomizationSet public CharaMakeParams.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); @@ -300,3 +292,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..90cae8d --- /dev/null +++ b/Glamourer/GameData/CustomizeSetFactory.cs @@ -0,0 +1,473 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Dalamud; +using Dalamud.Plugin.Services; +using Dalamud.Utility; +using Lumina.Excel; +using Lumina.Excel.GeneratedSheets; +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Race = Penumbra.GameData.Enums.Race; + +namespace Glamourer.GameData; + +internal class CustomizeSetFactory( + IDataManager _gameData, + IPluginLog _log, + IconStorage _icons, + NpcCustomizeSet _npcCustomizeSet, + ColorParameters _colors) +{ + public CustomizeSetFactory(IDataManager gameData, IPluginLog log, IconStorage 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(race, gender) + { + Name = GetName(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 ? [] : _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, CharaMakeParams 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.FacePaintColor, + CustomizeIndex.HighlightsColor, + CustomizeIndex.HairColor, + CustomizeIndex.FacePaint, + CustomizeIndex.TattooColor, + CustomizeIndex.EyeColorLeft, + CustomizeIndex.EyeColorRight, + }; + + var npcCustomizations = new HashSet<(CustomizeIndex, CustomizeValue)>(); + _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)!; + 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 + .GetType() + .GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)? + .MakeGenericMethod(typeof(CharaMakeParams)) + .Invoke(_gameData.Excel, ["charamaketype", _gameData.Language.ToLumina(), null])! as ExcelSheet + ?? null!; + + /// 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.GetRow((uint)race)?.Masculine.ToDalamudString().TextValue ?? race.ToName(), + Gender.Female => _tribeSheet.GetRow((uint)race)?.Feminine.ToDalamudString().TextValue ?? 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 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 (_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(CharaMakeParams row) + => row.Menus.Cast() + .FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.TailShape.ToByteAndMask().ByteIdx)?.Values + .Select((v, i) => FromValueAndIndex(CustomizeIndex.TailShape, v, i)).ToArray() + ?? []; + + /// 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() + ?? []; + + /// 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() + ?? []; + + /// 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)!; + 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); + // Face paint Row from CustomizeSheet might not be set in case of unlockable face paints. + 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)]; + } + + /// 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 generic 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); + } + + /// 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(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 string.Intern(_lobbySheet.GetRow(237)?.Text.ToDalamudString().ToString() ?? "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. + var textRow = _lobbySheet.GetRow(menu.Value.Id); + return string.Intern(textRow?.Text.ToDalamudString().ToString() ?? 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 CharaMakeParams.MenuType[] GetMenuTypes(CharaMakeParams 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 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 the availability of options according to actual availability. + private static void SetAvailability(CustomizeSet set, CharaMakeParams row) + { + // TODO: Hrothgar female + if (set is { Race: Race.Hrothgar, Gender: Gender.Female }) + return; + + 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.GetRow(c.CustomizeId)?.Unknown6 ?? 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, CharaMakeParams 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.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]; + 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/NpcCustomizeSet.cs b/Glamourer/GameData/NpcCustomizeSet.cs new file mode 100644 index 0000000..d364c1e --- /dev/null +++ b/Glamourer/GameData/NpcCustomizeSet.cs @@ -0,0 +1,319 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Dalamud.Plugin.Services; +using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Lumina.Excel.GeneratedSheets; +using OtterGui.Services; +using Penumbra.GameData.DataContainers; +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 = []; + + /// 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; + }); + } + + /// 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) + { + var row = enpcSheet.GetRow(id.Id); + // We only accept NPCs with valid names. + if (row == null || 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, + 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, otherwise use the own. + if (row.NpcEquip.Row != 0 && row.NpcEquip.Value is { } equip) + { + ApplyNpcEquip(ref ret, equip); + } + else + { + ret.Set(0, row.ModelHead | (row.DyeHead.Row << 24)); + ret.Set(1, row.ModelBody | (row.DyeBody.Row << 24)); + ret.Set(2, row.ModelHands | (row.DyeHands.Row << 24)); + ret.Set(3, row.ModelLegs | (row.DyeLegs.Row << 24)); + ret.Set(4, row.ModelFeet | (row.DyeFeet.Row << 24)); + ret.Set(5, row.ModelEars | (row.DyeEars.Row << 24)); + ret.Set(6, row.ModelNeck | (row.DyeNeck.Row << 24)); + ret.Set(7, row.ModelWrists | (row.DyeWrists.Row << 24)); + ret.Set(8, row.ModelRightRing | (row.DyeRightRing.Row << 24)); + ret.Set(9, row.ModelLeftRing | (row.DyeLeftRing.Row << 24)); + ret.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.Row << 48)); + ret.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.Row << 48)); + ret.VisorToggled = row.Visor; + } + + 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((int)bnpcSheet.RowCount); + + // 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, + 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]; + 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.Row << 24)); + data.Set(1, row.ModelBody | (row.DyeBody.Row << 24)); + data.Set(2, row.ModelHands | (row.DyeHands.Row << 24)); + data.Set(3, row.ModelLegs | (row.DyeLegs.Row << 24)); + data.Set(4, row.ModelFeet | (row.DyeFeet.Row << 24)); + data.Set(5, row.ModelEars | (row.DyeEars.Row << 24)); + data.Set(6, row.ModelNeck | (row.DyeNeck.Row << 24)); + data.Set(7, row.ModelWrists | (row.DyeWrists.Row << 24)); + data.Set(8, row.ModelRightRing | (row.DyeRightRing.Row << 24)); + data.Set(9, row.ModelLeftRing | (row.DyeLeftRing.Row << 24)); + data.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.Row << 48)); + data.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.Row << 48)); + 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.Row); + 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.Row); + 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.Value?.Type != 1) + return (false, CustomizeArray.Default); + + var customize = new CustomizeArray(); + customize.SetByIndex(0, (CustomizeValue)(byte)enpcBase.Race.Row); + 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.Row); + 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); + } + + /// + 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..2db8fb5 --- /dev/null +++ b/Glamourer/GameData/NpcData.cs @@ -0,0 +1,109 @@ +using System; +using System.Text; +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[40]; + + /// 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; + + /// 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")) + .Append('-') + .Append(span[i].Stain.Id.ToString("D3")) + .Append(", "); + } + + sb.Append(Mainhand.Skeleton.Id.ToString("D4")) + .Append('-') + .Append(Mainhand.Weapon.Id.ToString("D4")) + .Append('-') + .Append(Mainhand.Variant.Id.ToString("D3")) + .Append('-') + .Append(Mainhand.Stain.Id.ToString("D4")) + .Append(", ") + .Append(Offhand.Skeleton.Id.ToString("D4")) + .Append('-') + .Append(Offhand.Weapon.Id.ToString("D4")) + .Append('-') + .Append(Offhand.Variant.Id.ToString("D3")) + .Append('-') + .Append(Offhand.Stain.Id.ToString("D3")); + return sb.ToString(); + } + + /// Set an equipment piece to a given value. + internal void Set(int idx, uint value) + { + fixed (byte* ptr = _equip) + { + ((uint*)ptr)[idx] = value; + } + } + + /// Check if the appearance data, excluding ID and Name, of two NpcData is equal. + public bool DataEquals(in NpcData other) + { + 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 a31a696..00b94bb 100644 --- a/Glamourer/Glamourer.cs +++ b/Glamourer/Glamourer.cs @@ -8,6 +8,7 @@ using Glamourer.State; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; using OtterGui.Log; +using OtterGui.Services; namespace Glamourer; @@ -25,21 +26,21 @@ public class Glamourer : IDalamudPlugin public static readonly Logger Log = new(); public static MessageService Messager { get; private set; } = null!; - private readonly ServiceProvider _services; + private readonly ServiceManager _services; public Glamourer(DalamudPluginInterface pluginInterface) { try { - _services = ServiceManager.CreateProvider(pluginInterface, Log); - Messager = _services.GetRequiredService(); - _services.GetRequiredService(); - _services.GetRequiredService(); - _services.GetRequiredService(); - _services.GetRequiredService(); // Initialize State Listener. - _services.GetRequiredService(); // initialize ui. - _services.GetRequiredService(); // initialize commands. - _services.GetRequiredService(); // initialize IPC. + _services = ServiceManagerA.CreateProvider(pluginInterface, Log); + Messager = _services.GetService(); + _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 diff --git a/Glamourer/Glamourer.csproj b/Glamourer/Glamourer.csproj index 12ec3a7..0d2bb36 100644 --- a/Glamourer/Glamourer.csproj +++ b/Glamourer/Glamourer.csproj @@ -84,7 +84,6 @@ - @@ -109,8 +108,8 @@ - - + + diff --git a/Glamourer/Gui/Colors.cs b/Glamourer/Gui/Colors.cs index 873ec38..7addc45 100644 --- a/Glamourer/Gui/Colors.cs +++ b/Glamourer/Gui/Colors.cs @@ -28,6 +28,8 @@ public enum ColorId TriStateCheck, TriStateCross, TriStateNeutral, + BattleNpc, + EventNpc, } public static class Colors @@ -60,7 +62,9 @@ public static class Colors 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.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." ), _ => (0x00000000, string.Empty, string.Empty ), // @formatter:on }; diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs index 127d8c2..5fa28a6 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs @@ -1,11 +1,11 @@ using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Utility; -using Glamourer.Customization; +using Glamourer.GameData; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData; +using Penumbra.GameData.Enums; namespace Glamourer.Gui.Customization; @@ -15,12 +15,12 @@ public partial class CustomizationDrawer 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)) ImGui.OpenPopup(ColorPickerPopupName); @@ -39,7 +39,7 @@ public partial class CustomizationDrawer ImGui.SameLine(); - using (var group = ImRaii.Group()) + using (_ = ImRaii.Group()) { DataInputInt(current, npc); if (_withApply) @@ -89,7 +89,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 7866bda..a50424c 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs @@ -1,11 +1,12 @@ using System; using System.Linq; +using System.Numerics; using Dalamud.Interface; -using Glamourer.Customization; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Customization; @@ -75,4 +76,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 e4c15d4..9b35b92 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs @@ -1,10 +1,11 @@ using System; using System.Numerics; -using Dalamud.Interface.Utility; -using Glamourer.Customization; +using Glamourer.GameData; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Customization; @@ -14,7 +15,7 @@ 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; @@ -28,8 +29,8 @@ 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); + using (_ = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw)) { if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) ImGui.OpenPopup(IconSelectorPopup); @@ -38,7 +39,7 @@ public partial class CustomizationDrawer ImGuiUtil.HoverIconTooltip(icon, _iconSize); ImGui.SameLine(); - using (var group = ImRaii.Group()) + using (_ = ImRaii.Group()) { DataInputInt(current, npc); if (_lockedRedraw && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) @@ -68,7 +69,7 @@ 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()) { using var frameColor = ImRaii.PushColor(ImGuiCol.Button, Colors.SelectedRed, current == i); @@ -119,16 +120,16 @@ public partial class CustomizationDrawer ImGui.Dummy(new Vector2(ImGui.GetFrameHeight())); } - var oldValue = _customize.Data.At(_currentIndex.ToByteAndMask().ByteIdx); - var tmp = (int)oldValue; + 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) + if (tmp != oldValue.Value) { - _customize.Data.Set(_currentIndex.ToByteAndMask().ByteIdx, (byte)tmp); - var changes = (byte)tmp ^ oldValue; + _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) @@ -179,8 +180,8 @@ public partial class CustomizationDrawer 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); + ? _legacyTattoo ?? _service.Manager.GetIcon(feature.IconId) + : _service.Manager.GetIcon(feature.IconId); if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X, Vector4.Zero, enabled ? Vector4.One : _redTint)) { diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs index e9ce9e9..e970fb3 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs @@ -1,10 +1,10 @@ using System; using System.Numerics; -using Glamourer.Customization; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Customization; @@ -91,10 +91,10 @@ public partial class CustomizationDrawer 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)) + using (_ = ImRaii.Disabled(_locked)) { if (indexedBy1) { @@ -210,7 +210,7 @@ public partial class CustomizationDrawer } else { - using (var disabled = ImRaii.Disabled(_locked)) + using (_ = ImRaii.Disabled(_locked)) { if (ImGui.Checkbox("##toggle", ref tmp)) { diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.cs b/Glamourer/Gui/Customization/CustomizationDrawer.cs index fa725f5..b741f39 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.cs @@ -4,17 +4,17 @@ using System.Reflection; using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Plugin; -using Glamourer.Customization; +using Glamourer.GameData; using Glamourer.Services; using ImGuiNET; 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(DalamudPluginInterface pi, CustomizationService _service, CodeService _codes, Configuration _config) +public partial class CustomizationDrawer(DalamudPluginInterface pi, CustomizeService _service, CodeService _codes, Configuration _config) : IDisposable { private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f); @@ -22,13 +22,12 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio private Exception? _terminate; - private Customize _customize = Customize.Default; - 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; } @@ -47,18 +46,16 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio public void 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; 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; @@ -66,12 +63,12 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio 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; } @@ -116,11 +113,13 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio try { - if (_codes.EnabledArtisan) + if (_codes.Enabled(CodeService.CodeFlag.Artisan)) return DrawArtisan(); DrawRaceGenderSelector(); - _set = _service.AwaitedService.GetList(_customize.Clan, _customize.Gender); + DrawBodyType(); + + _set = _service.Manager.GetSet(_customize.Clan, _customize.Gender); foreach (var id in _set.Order[CharaMakeParams.MenuType.Percentage]) PercentageSelector(id); @@ -153,23 +152,23 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio private unsafe bool DrawArtisan() { - for (var i = 0; i < CustomizeData.Size; ++i) + for (var i = 0; i < CustomizeArray.Size; ++i) { using var id = ImRaii.PushId(i); - int value = _customize.Data.Data[i]; + int value = _customize.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]) + if (newValue != _customize.Data[i]) foreach (var flag in Enum.GetValues()) { - var (j, mask) = flag.ToByteAndMask(); + var (j, _) = flag.ToByteAndMask(); if (j == i) Changed |= flag.ToFlag(); } - _customize.Data.Data[i] = newValue; + _customize.Data[i] = newValue; } } diff --git a/Glamourer/Gui/DesignCombo.cs b/Glamourer/Gui/DesignCombo.cs index 19d73c4..d52be90 100644 --- a/Glamourer/Gui/DesignCombo.cs +++ b/Glamourer/Gui/DesignCombo.cs @@ -4,7 +4,7 @@ using System.Linq; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Glamourer.Automation; -using Glamourer.Customization; +using Glamourer.GameData; using Glamourer.Designs; using Glamourer.Events; using Glamourer.Services; @@ -13,6 +13,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Log; using OtterGui.Widgets; +using Penumbra.GameData.Enums; namespace Glamourer.Gui; @@ -179,7 +180,7 @@ public sealed class RevertDesignCombo : DesignComboBase, IDisposable private readonly AutoDesignManager _autoDesignManager; public RevertDesignCombo(DesignManager designs, DesignFileSystem fileSystem, TabSelected tabSelected, DesignColors designColors, - ItemManager items, CustomizationService customize, Logger log, DesignChanged designChanged, AutoDesignManager autoDesignManager, + ItemManager items, CustomizeService customize, Logger log, DesignChanged designChanged, AutoDesignManager autoDesignManager, EphemeralConfig config) : this(designs, fileSystem, tabSelected, designColors, CreateRevertDesign(customize, items), log, designChanged, autoDesignManager, config) @@ -209,7 +210,7 @@ public sealed class RevertDesignCombo : DesignComboBase, IDisposable _autoDesignManager.AddDesign(set, CurrentSelection!.Item1 == RevertDesign ? null : CurrentSelection!.Item1); } - private static Design CreateRevertDesign(CustomizationService customize, ItemManager items) + private static Design CreateRevertDesign(CustomizeService customize, ItemManager items) => new(customize, items) { Index = RevertDesignIndex, diff --git a/Glamourer/Gui/Equipment/EquipDrawData.cs b/Glamourer/Gui/Equipment/EquipDrawData.cs index 8ea3972..f94d997 100644 --- a/Glamourer/Gui/Equipment/EquipDrawData.cs +++ b/Glamourer/Gui/Equipment/EquipDrawData.cs @@ -1,6 +1,7 @@ using System; using Glamourer.Designs; using Glamourer.Events; +using Glamourer.Services; using Glamourer.State; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -28,7 +29,9 @@ public ref struct EquipDrawData(EquipSlot slot, in DesignData designData) public static EquipDrawData FromDesign(DesignManager manager, Design design, EquipSlot slot) => new(slot, design.DesignData) { - ItemSetter = slot.IsEquipmentPiece() ? i => manager.ChangeEquip(design, slot, i) : i => manager.ChangeWeapon(design, slot, i), + ItemSetter = slot.IsEquipment() || slot.IsAccessory() + ? i => manager.ChangeEquip(design, slot, i) + : i => manager.ChangeWeapon(design, slot, i), StainSetter = i => manager.ChangeStain(design, slot, i), ApplySetter = b => manager.ChangeApplyEquip(design, slot, b), ApplyStainSetter = b => manager.ChangeApplyStain(design, slot, b), diff --git a/Glamourer/Gui/Equipment/EquipmentDrawer.cs b/Glamourer/Gui/Equipment/EquipmentDrawer.cs index e2b44de..e8840bc 100644 --- a/Glamourer/Gui/Equipment/EquipmentDrawer.cs +++ b/Glamourer/Gui/Equipment/EquipmentDrawer.cs @@ -12,7 +12,7 @@ using Glamourer.Unlocks; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -24,7 +24,7 @@ public class EquipmentDrawer private readonly ItemManager _items; private readonly GlamourerColorCombo _stainCombo; - private readonly StainData _stainData; + private readonly DictStain _stainData; private readonly ItemCombo[] _itemCombo; private readonly Dictionary _weaponCombo; private readonly CodeService _codes; @@ -66,8 +66,8 @@ public class EquipmentDrawer _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; @@ -94,7 +94,7 @@ public class EquipmentDrawer if (_config.SmallEquip) DrawEquipSmall(equipDrawData); - else if (!equipDrawData.Locked && _codes.EnabledArtisan) + else if (!equipDrawData.Locked && _codes.Enabled(CodeService.CodeFlag.Artisan)) DrawEquipArtisan(equipDrawData); else DrawEquipNormal(equipDrawData); @@ -102,7 +102,7 @@ public class EquipmentDrawer public void DrawWeapons(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons) { - if (mainhand.CurrentItem.ModelId.Id == 0) + if (mainhand.CurrentItem.PrimaryId.Id == 0) return; if (_config.HideApplyCheckmarks) @@ -117,7 +117,7 @@ public class EquipmentDrawer if (_config.SmallEquip) DrawWeaponsSmall(mainhand, offhand, allWeapons); - else if (!mainhand.Locked && _codes.EnabledArtisan) + else if (!mainhand.Locked && _codes.Enabled(CodeService.CodeFlag.Artisan)) DrawWeaponsArtisan(mainhand, offhand); else DrawWeaponsNormal(mainhand, offhand, allWeapons); @@ -202,24 +202,24 @@ public class EquipmentDrawer void DrawWeapon(in EquipDrawData current) { - int setId = current.CurrentItem.ModelId.Id; - int type = current.CurrentItem.WeaponType.Id; + int setId = current.CurrentItem.PrimaryId.Id; + int type = current.CurrentItem.SecondaryId.Id; int variant = current.CurrentItem.Variant.Id; 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.CurrentItem.ModelId.Id) - current.ItemSetter(_items.Identify(current.Slot, newSetId, current.CurrentItem.WeaponType, current.CurrentItem.Variant)); + var newSetId = (PrimaryId)Math.Clamp(setId, 0, ushort.MaxValue); + if (newSetId.Id != current.CurrentItem.PrimaryId.Id) + current.ItemSetter(_items.Identify(current.Slot, newSetId, current.CurrentItem.SecondaryId, current.CurrentItem.Variant)); } 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.CurrentItem.WeaponType.Id) - current.ItemSetter(_items.Identify(current.Slot, current.CurrentItem.ModelId, newType, current.CurrentItem.Variant)); + var newType = (SecondaryId)Math.Clamp(type, 0, ushort.MaxValue); + if (newType.Id != current.CurrentItem.SecondaryId.Id) + current.ItemSetter(_items.Identify(current.Slot, current.CurrentItem.PrimaryId, newType, current.CurrentItem.Variant)); } ImGui.SameLine(); @@ -228,7 +228,8 @@ public class EquipmentDrawer { var newVariant = (Variant)Math.Clamp(variant, 0, byte.MaxValue); if (newVariant.Id != current.CurrentItem.Variant.Id) - current.ItemSetter(_items.Identify(current.Slot, current.CurrentItem.ModelId, current.CurrentItem.WeaponType, newVariant)); + current.ItemSetter(_items.Identify(current.Slot, current.CurrentItem.PrimaryId, current.CurrentItem.SecondaryId, + newVariant)); } } } @@ -249,13 +250,13 @@ public class EquipmentDrawer /// Draw an input for armor that can set arbitrary values instead of choosing items. private void DrawArmorArtisan(EquipDrawData data) { - int setId = data.CurrentItem.ModelId.Id; + int setId = data.CurrentItem.PrimaryId.Id; int variant = data.CurrentItem.Variant.Id; 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 != data.CurrentItem.ModelId.Id) + var newSetId = (PrimaryId)Math.Clamp(setId, 0, ushort.MaxValue); + if (newSetId.Id != data.CurrentItem.PrimaryId.Id) data.ItemSetter(_items.Identify(data.Slot, newSetId, data.CurrentItem.Variant)); } @@ -265,7 +266,7 @@ public class EquipmentDrawer { var newVariant = (byte)Math.Clamp(variant, 0, byte.MaxValue); if (newVariant != data.CurrentItem.Variant) - data.ItemSetter(_items.Identify(data.Slot, data.CurrentItem.ModelId, newVariant)); + data.ItemSetter(_items.Identify(data.Slot, data.CurrentItem.PrimaryId, newVariant)); } } @@ -454,7 +455,7 @@ public class EquipmentDrawer else if (combo.CustomVariant.Id > 0) data.ItemSetter(_items.Identify(data.Slot, combo.CustomSetId, combo.CustomVariant)); - if (!data.Locked && data.CurrentItem.ModelId.Id != 0) + if (!data.Locked && data.CurrentItem.PrimaryId.Id != 0) { if (clear || ImGui.IsItemClicked(ImGuiMouseButton.Right)) data.ItemSetter(ItemManager.NothingItem(data.Slot)); diff --git a/Glamourer/Gui/Equipment/GlamourerColorCombo.cs b/Glamourer/Gui/Equipment/GlamourerColorCombo.cs index 0bd55dd..2a3dcec 100644 --- a/Glamourer/Gui/Equipment/GlamourerColorCombo.cs +++ b/Glamourer/Gui/Equipment/GlamourerColorCombo.cs @@ -8,12 +8,12 @@ using Dalamud.Interface.Utility.Raii; using Glamourer.Unlocks; using ImGuiNET; using OtterGui.Widgets; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Structs; namespace Glamourer.Gui.Equipment; -public sealed class GlamourerColorCombo(float _comboWidth, StainData _stains, FavoriteManager _favorites) +public sealed class GlamourerColorCombo(float _comboWidth, DictStain _stains, FavoriteManager _favorites) : FilterComboColors(_comboWidth, CreateFunc(_stains, _favorites), Glamourer.Log) { protected override bool DrawSelectable(int globalIdx, bool selected) @@ -40,8 +40,9 @@ public sealed class GlamourerColorCombo(float _comboWidth, StainData _stains, Fa return base.DrawSelectable(globalIdx, selected); } - private static Func>> CreateFunc(StainData stains, + private static Func>> CreateFunc(DictStain stains, FavoriteManager favorites) - => () => stains.Data.Select(kvp => (kvp, favorites.Contains((StainId)kvp.Key))).OrderBy(p => !p.Item2).Select(p => p.kvp) - .Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(); + => () => 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 4ddbbaa..9e1790a 100644 --- a/Glamourer/Gui/Equipment/ItemCombo.cs +++ b/Glamourer/Gui/Equipment/ItemCombo.cs @@ -23,8 +23,8 @@ public sealed class ItemCombo : FilterComboCache private ItemId _currentItem; private float _innerWidth; - public SetId CustomSetId { get; private set; } - public Variant CustomVariant { get; private set; } + 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) @@ -83,7 +83,7 @@ public sealed class ItemCombo : FilterComboCache } protected override bool IsVisible(int globalIndex, LowerString filter) - => base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].ModelId.Id.ToString()); + => base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].PrimaryId.Id.ToString()); protected override string ToString(EquipItem obj) => obj.Name; @@ -111,7 +111,7 @@ public sealed class ItemCombo : FilterComboCache private static IReadOnlyList GetItems(FavoriteManager favorites, ItemManager items, EquipSlot slot) { var nothing = ItemManager.NothingItem(slot); - if (!items.ItemService.AwaitedService.TryGetValue(slot.ToEquipType(), out var list)) + if (!items.ItemData.ByType.TryGetValue(slot.ToEquipType(), out var list)) return new[] { nothing, diff --git a/Glamourer/Gui/Equipment/WeaponCombo.cs b/Glamourer/Gui/Equipment/WeaponCombo.cs index 5a1792e..027a211 100644 --- a/Glamourer/Gui/Equipment/WeaponCombo.cs +++ b/Glamourer/Gui/Equipment/WeaponCombo.cs @@ -60,12 +60,12 @@ public sealed class WeaponCombo : FilterComboCache 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})"); + ImGuiUtil.RightAlign($"({obj.PrimaryId.Id}-{obj.SecondaryId.Id}-{obj.Variant})"); 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) || filter.IsContained(Items[globalIndex].PrimaryId.Id.ToString()); protected override string ToString(EquipItem obj) => obj.Name; @@ -80,14 +80,14 @@ public sealed class WeaponCombo : FilterComboCache 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(); } - if (!items.ItemService.AwaitedService.TryGetValue(type, out var list)) + if (!items.ItemData.ByType.TryGetValue(type, out var list)) return Array.Empty(); if (type.AllowsNothing()) diff --git a/Glamourer/Gui/GenericPopupWindow.cs b/Glamourer/Gui/GenericPopupWindow.cs index f1aa9fe..ba16ab9 100644 --- a/Glamourer/Gui/GenericPopupWindow.cs +++ b/Glamourer/Gui/GenericPopupWindow.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System; +using System.Numerics; using Dalamud.Game.ClientState.Conditions; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; @@ -61,7 +62,7 @@ public class GenericPopupWindow : Window 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/MainWindow.cs b/Glamourer/Gui/MainWindow.cs index df50fb5..565ef84 100644 --- a/Glamourer/Gui/MainWindow.cs +++ b/Glamourer/Gui/MainWindow.cs @@ -10,6 +10,7 @@ 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.UnlocksTab; using ImGuiNET; using OtterGui.Custom; @@ -29,6 +30,7 @@ public class MainWindow : Window, IDisposable Automation = 4, Unlocks = 5, Messages = 6, + Npcs = 7, } private readonly Configuration _config; @@ -42,12 +44,14 @@ 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 MainWindow(DalamudPluginInterface pi, Configuration config, SettingsTab settings, ActorTab actors, DesignTab designs, - DebugTab debugTab, AutomationTab automation, UnlocksTab unlocks, TabSelected @event, MessagesTab messages, DesignQuickBar quickBar) + DebugTab debugTab, AutomationTab automation, UnlocksTab unlocks, TabSelected @event, MessagesTab messages, DesignQuickBar quickBar, + NpcTab npcs) : base(GetLabel()) { pi.UiBuilder.DisableGposeUiHide = true; @@ -65,17 +69,19 @@ public class MainWindow : Window, IDisposable _event = @event; Messages = messages; _quickBar = quickBar; + Npcs = npcs; _config = config; - _tabs = new ITab[] - { + _tabs = + [ settings, actors, designs, automation, unlocks, + npcs, messages, debugTab, - }; + ]; _event.Subscribe(OnTabSelected, TabSelected.Priority.MainWindow); IsOpen = _config.OpenWindowAtStart; } @@ -117,6 +123,7 @@ public class MainWindow : Window, IDisposable TabType.Automation => Automation.Label, TabType.Unlocks => Unlocks.Label, TabType.Messages => Messages.Label, + TabType.Npcs => Npcs.Label, _ => ReadOnlySpan.Empty, }; @@ -128,6 +135,7 @@ 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 diff --git a/Glamourer/Gui/PenumbraChangedItemTooltip.cs b/Glamourer/Gui/PenumbraChangedItemTooltip.cs index 8eb763e..b499bc1 100644 --- a/Glamourer/Gui/PenumbraChangedItemTooltip.cs +++ b/Glamourer/Gui/PenumbraChangedItemTooltip.cs @@ -7,7 +7,6 @@ using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.Services; using Glamourer.State; -using Glamourer.Structs; using ImGuiNET; using OtterGui.Raii; using Penumbra.Api.Enums; @@ -76,7 +75,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 +91,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) @@ -166,7 +165,7 @@ public class PenumbraChangedItemTooltip : IDisposable { case ChangedItemType.ItemOffhand: case ChangedItemType.Item: - if (!_items.ItemService.AwaitedService.TryGetValue(id, type is ChangedItemType.Item ? EquipSlot.MainHand : EquipSlot.OffHand, out var item)) + if (!_items.ItemData.TryGetValue(id, type is ChangedItemType.Item ? EquipSlot.MainHand : EquipSlot.OffHand, out var item)) return; CreateTooltip(item, "[Glamourer] ", false); @@ -177,7 +176,7 @@ public class PenumbraChangedItemTooltip : IDisposable private bool CanApplyWeapon(EquipSlot slot, EquipItem item) { var main = _objects.Player.GetMainhand(); - var mainItem = _items.Identify(slot, main.Set, main.Type, main.Variant); + var mainItem = _items.Identify(slot, main.Skeleton, main.Weapon, main.Variant); if (slot == EquipSlot.MainHand) return item.Type == mainItem.Type; @@ -197,7 +196,7 @@ public class PenumbraChangedItemTooltip : IDisposable if (!Player(out var state)) return; - if (!_items.ItemService.AwaitedService.TryGetValue(id, type is ChangedItemType.Item ? EquipSlot.MainHand : EquipSlot.OffHand, out var item)) + if (!_items.ItemData.TryGetValue(id, type is ChangedItemType.Item ? EquipSlot.MainHand : EquipSlot.OffHand, out var item)) return; ApplyItem(state, item); diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index a294b08..b56b314 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -6,29 +6,36 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Glamourer.Automation; -using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Events; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; using Glamourer.Interop; using Glamourer.Interop.Structs; -using Glamourer.Services; using Glamourer.State; -using Glamourer.Structs; using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData.Actors; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; namespace Glamourer.Gui.Tabs.ActorTab; -public class ActorPanel(ActorSelector _selector, StateManager _stateManager, CustomizationDrawer _customizationDrawer, - EquipmentDrawer _equipmentDrawer, IdentifierService _identification, AutoDesignApplier _autoDesignApplier, - Configuration _config, DesignConverter _converter, ObjectManager _objects, DesignManager _designManager, ImportService _importService, - ICondition _conditions) +public class ActorPanel( + ActorSelector _selector, + StateManager _stateManager, + CustomizationDrawer _customizationDrawer, + EquipmentDrawer _equipmentDrawer, + AutoDesignApplier _autoDesignApplier, + Configuration _config, + DesignConverter _converter, + ObjectManager _objects, + DesignManager _designManager, + ImportService _importService, + ICondition _conditions, + DictModelChara _modelChara) { private ActorIdentifier _identifier; private string _actorName = string.Empty; @@ -154,7 +161,7 @@ public class ActorPanel(ActorSelector _selector, StateManager _stateManager, Cus } var mainhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.MainHand); - var offhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.OffHand); + var offhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.OffHand); _equipmentDrawer.DrawWeapons(mainhand, offhand, GameMain.IsInGPose()); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); @@ -164,21 +171,21 @@ public class ActorPanel(ActorSelector _selector, StateManager _stateManager, Cus private void DrawEquipmentMetaToggles() { - using (var _ = ImRaii.Group()) + using (_ = ImRaii.Group()) { EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.HatState, _stateManager, _state!)); EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.Head, _stateManager, _state!)); } ImGui.SameLine(); - using (var _ = ImRaii.Group()) + using (_ = ImRaii.Group()) { EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.VisorState, _stateManager, _state!)); EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.Body, _stateManager, _state!)); } ImGui.SameLine(); - using (var _ = ImRaii.Group()) + using (_ = ImRaii.Group()) { EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.WeaponState, _stateManager, _state!)); EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.OffHand, _stateManager, _state!)); @@ -187,10 +194,10 @@ public class ActorPanel(ActorSelector _selector, StateManager _stateManager, Cus 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) @@ -202,14 +209,14 @@ public class ActorPanel(ActorSelector _selector, StateManager _stateManager, Cus 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(); @@ -223,11 +230,11 @@ public class ActorPanel(ActorSelector _selector, StateManager _stateManager, Cus 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}"); @@ -289,15 +296,15 @@ public class ActorPanel(ActorSelector _selector, StateManager _stateManager, Cus BorderColor = ColorId.ActorUnavailable.Value(), }; - private string _newName = string.Empty; - private DesignBase? _newDesign = null; + private string _newName = string.Empty; + private DesignBase? _newDesign; private void SaveDesignOpen() { ImGui.OpenPopup("Save as Design"); - _newName = _state!.Identifier.ToName(); + _newName = _state!.Identifier.ToName(); var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags(); - _newDesign = _converter.Convert(_state, applyGear, applyCustomize, applyCrest); + _newDesign = _converter.Convert(_state, applyGear, applyCustomize, applyCrest); } private void SaveDesignDrawPopup() diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs b/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs index b8d1ba4..934a19d 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorSelector.cs @@ -17,11 +17,11 @@ public class ActorSelector { private readonly EphemeralConfig _config; private readonly ObjectManager _objects; - private readonly ActorService _actors; + private readonly ActorManager _actors; private ActorIdentifier _identifier = ActorIdentifier.Invalid; - public ActorSelector(ObjectManager objects, ActorService actors, EphemeralConfig config) + public ActorSelector(ObjectManager objects, ActorManager actors, EphemeralConfig config) { _objects = objects; _actors = actors; @@ -93,7 +93,7 @@ public class ActorSelector if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth , "Select the local player character.", !_objects.Player, true)) - _identifier = _objects.Player.GetIdentifier(_actors.AwaitedService); + _identifier = _objects.Player.GetIdentifier(_actors); ImGui.SameLine(); var (id, data) = _objects.TargetData; diff --git a/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs b/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs index da4fda1..c3781fc 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/HumanNpcCombo.cs @@ -3,23 +3,23 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Utility; -using Glamourer.Services; using ImGuiNET; using OtterGui.Custom; using OtterGui.Log; using OtterGui.Widgets; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; 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), 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 +36,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 +62,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 +80,8 @@ 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))); + var nameIds = bNpcNames[id]; + ret.AddRange(nameIds.Select(nameId => (bNpcs[nameId], 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..8498bc1 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 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; @@ -21,11 +21,12 @@ public class IdentifierDrawer public ActorIdentifier RetainerIdentifier { get; private set; } = ActorIdentifier.Invalid; public ActorIdentifier MannequinIdentifier { 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) @@ -63,13 +64,13 @@ public class IdentifierDrawer { 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); } 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/SetPanel.cs b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs index 6b0e301..846823a 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs @@ -6,10 +6,8 @@ using System.Text; using Dalamud.Interface; using Dalamud.Interface.Utility; using Glamourer.Automation; -using Glamourer.Customization; using Glamourer.Interop; using Glamourer.Services; -using Glamourer.Structs; using Glamourer.Unlocks; using ImGuiNET; using OtterGui; @@ -17,44 +15,29 @@ using OtterGui.Log; using OtterGui.Raii; 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, + RevertDesignCombo _designCombo, + CustomizeUnlockManager _customizeUnlocks, + CustomizeService _customizations, + IdentifierDrawer _identifierDrawer, + Configuration _config) { - 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 RevertDesignCombo _designCombo; - private readonly JobGroupCombo _jobGroupCombo; - private readonly IdentifierDrawer _identifierDrawer; + private readonly JobGroupCombo _jobGroupCombo = new(_manager, _jobs, Glamourer.Log); private string? _tempName; private int _dragIndex = -1; private Action? _endAction; - public SetPanel(SetSelector selector, AutoDesignManager manager, JobService jobs, ItemUnlockManager itemUnlocks, - RevertDesignCombo 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!; @@ -77,7 +60,7 @@ public class SetPanel var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y }; - using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) + using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) { var enabled = Selection.Enabled; if (ImGui.Checkbox("##Enabled", ref enabled)) @@ -87,7 +70,7 @@ public class SetPanel } ImGui.SameLine(); - using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) + using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) { var useGame = _selector.Selection!.BaseState is AutoDesignSet.Base.Game; if (ImGui.Checkbox("##gameState", ref useGame)) @@ -98,7 +81,7 @@ public class SetPanel } ImGui.SameLine(); - using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) + using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) { var editing = _config.ShowAutomationSetEditing; if (ImGui.Checkbox("##Show Editing", ref editing)) @@ -230,7 +213,7 @@ public class SetPanel if (_config.ShowUnlockedItemWarnings) { ImGui.TableNextColumn(); - DrawWarnings(design, idx); + DrawWarnings(design); } } @@ -278,7 +261,7 @@ public class SetPanel } } - private void DrawWarnings(AutoDesign design, int idx) + private void DrawWarnings(AutoDesign design) { if (design.Revert) return; @@ -301,27 +284,6 @@ public class SetPanel 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; @@ -333,7 +295,7 @@ public class SetPanel if (!design.Design.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(); @@ -355,6 +317,27 @@ 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); + } + + ImGuiUtil.HoverTooltip(sb.ToString()); + } + else + { + ImGuiUtil.DrawTextButton(string.Empty, size, 0); + ImGuiUtil.HoverTooltip(good); + } + } } private void DrawDragDrop(AutoDesignSet set, int index) @@ -394,7 +377,7 @@ public class SetPanel var newType = design.ApplicationType; 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; diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs b/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs index 5ca3adf..c1544b2 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetSelector.cs @@ -7,11 +7,11 @@ using Dalamud.Interface.Utility; using Glamourer.Automation; using Glamourer.Events; using Glamourer.Interop; -using Glamourer.Services; using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using Penumbra.GameData.Actors; using Penumbra.String; using ImGuiClip = OtterGui.ImGuiClip; @@ -22,9 +22,9 @@ public class SetSelector : IDisposable private readonly Configuration _config; private readonly AutoDesignManager _manager; private readonly AutomationChanged _event; - private readonly ActorService _actors; + private readonly ActorManager _actors; private readonly ObjectManager _objects; - private readonly List<(AutoDesignSet, int)> _list = new(); + private readonly List<(AutoDesignSet, int)> _list = []; public AutoDesignSet? Selection { get; private set; } public int SelectionIndex { get; private set; } = -1; @@ -44,7 +44,7 @@ public class SetSelector : IDisposable internal int _dragDesignIndex = -1; - public SetSelector(AutoDesignManager manager, AutomationChanged @event, Configuration config, ActorService actors, ObjectManager objects) + public SetSelector(AutoDesignManager manager, AutomationChanged @event, Configuration config, ActorManager actors, ObjectManager objects) { _manager = manager; _event = @event; @@ -289,9 +289,9 @@ public class SetSelector : IDisposable private void NewSetButton(Vector2 size) { - var id = _actors.AwaitedService.GetCurrentPlayer(); + var id = _actors.GetCurrentPlayer(); if (!id.IsValid) - id = _actors.AwaitedService.CreatePlayer(ByteString.FromSpanUnsafe("New Design"u8, true, false, true), ushort.MaxValue); + id = _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); diff --git a/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs index 71df7b9..ea99620 100644 --- a/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Numerics; using Dalamud.Interface; -using Glamourer.Customization; +using Glamourer.GameData; using Glamourer.Designs; using Glamourer.Events; using Glamourer.Interop; @@ -12,10 +12,11 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; namespace Glamourer.Gui.Tabs.DebugTab; -public class ActiveStatePanel(StateManager _stateManager, ObjectManager _objectManager) : IDebugTabTree +public class ActiveStatePanel(StateManager _stateManager, ObjectManager _objectManager) : IGameDataDrawer { public string Label => $"Active Actors ({_stateManager.Count})###Active Actors"; @@ -69,7 +70,7 @@ public class ActiveStatePanel(StateManager _stateManager, ObjectManager _objectM 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})"; + return $"{item.Name} ({item.PrimaryId.Id}{(item.SecondaryId != 0 ? $"-{item.SecondaryId.Id}" : string.Empty)}-{item.Variant})"; } PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state[ActorState.MetaIndex.ModelId]); diff --git a/Glamourer/Gui/Tabs/DebugTab/ActorServicePanel.cs b/Glamourer/Gui/Tabs/DebugTab/ActorServicePanel.cs deleted file mode 100644 index 42fc52b..0000000 --- a/Glamourer/Gui/Tabs/DebugTab/ActorServicePanel.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface.Utility; -using Glamourer.Services; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using ImGuiClip = OtterGui.ImGuiClip; - -namespace Glamourer.Gui.Tabs.DebugTab; - -public class ActorServicePanel(ActorService _actors, ItemManager _items) : IDebugTabTree -{ - public string Label - => "Actor Service"; - - public bool Disabled - => !_actors.Valid; - - 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; - - public void Draw() - { - DrawBnpcTable(); - DebugTab.DrawNameTable("ENPCs", ref _enpcFilter, _actors.AwaitedService.Data.ENpcs.Select(kvp => (kvp.Key, kvp.Value))); - DebugTab.DrawNameTable("Companions", ref _companionFilter, _actors.AwaitedService.Data.Companions.Select(kvp => (kvp.Key, kvp.Value))); - DebugTab.DrawNameTable("Mounts", ref _mountFilter, _actors.AwaitedService.Data.Mounts.Select(kvp => (kvp.Key, kvp.Value))); - DebugTab.DrawNameTable("Ornaments", ref _ornamentFilter, _actors.AwaitedService.Data.Ornaments.Select(kvp => (kvp.Key, kvp.Value))); - DebugTab.DrawNameTable("Worlds", ref _worldFilter, _actors.AwaitedService.Data.Worlds.Select(kvp => ((uint)kvp.Key, kvp.Value))); - } - - private void DrawBnpcTable() - { - using var _ = ImRaii.PushId(1); - using var tree = ImRaii.TreeNode("BNPCs"); - if (!tree) - return; - - var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref _bnpcFilter, 256); - var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - using var table = ImRaii.Table("##table", 3, 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.TableSetupColumn("3", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableNextColumn(); - var skips = ImGuiClip.GetNecessarySkips(height); - ImGui.TableNextRow(); - var data = _actors.AwaitedService.Data.BNpcs.Select(kvp => (kvp.Key, kvp.Key.ToString("D5"), kvp.Value)); - var remainder = ImGuiClip.FilteredClippedDraw(data, skips, - p => p.Item2.Contains(_bnpcFilter) || p.Item3.Contains(_bnpcFilter, StringComparison.OrdinalIgnoreCase), - p => - { - ImGuiUtil.DrawTableColumn(p.Item2); - ImGuiUtil.DrawTableColumn(p.Item3); - var bnpcs = _items.IdentifierService.AwaitedService.GetBnpcsFromName(p.Item1); - ImGuiUtil.DrawTableColumn(string.Join(", ", bnpcs.Select(b => b.Id.ToString()))); - }); - ImGuiClip.DrawEndDummy(remainder, height); - } -} diff --git a/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs b/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs index 8e29b8b..cbc0e40 100644 --- a/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/AutoDesignPanel.cs @@ -2,10 +2,11 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData.Gui.Debug; namespace Glamourer.Gui.Tabs.DebugTab; -public class AutoDesignPanel(AutoDesignManager _autoDesignManager) : IDebugTabTree +public class AutoDesignPanel(AutoDesignManager _autoDesignManager) : IGameDataDrawer { public string Label => "Auto Designs"; diff --git a/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs b/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs index 6088651..7ab089a 100644 --- a/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs @@ -1,36 +1,35 @@ using System; -using Glamourer.Customization; +using Glamourer.GameData; using Glamourer.Services; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; namespace Glamourer.Gui.Tabs.DebugTab; -public class CustomizationServicePanel(CustomizationService _customization) : IDebugTabTree +public class CustomizationServicePanel(CustomizeService customize) : IGameDataDrawer { public string Label => "Customization Service"; public bool Disabled - => !_customization.Valid; + => !customize.Finished; public void Draw() { - foreach (var clan in _customization.AwaitedService.Clans) + foreach (var (clan, gender) in CustomizeManager.AllSets()) { - foreach (var gender in _customization.AwaitedService.Genders) - { - var set = _customization.AwaitedService.GetList(clan, gender); - DrawCustomizationInfo(set); - DrawNpcCustomizationInfo(set); - } + var set = customize.Manager.GetSet(clan, gender); + DrawCustomizationInfo(set); + DrawNpcCustomizationInfo(set); } } - private void DrawCustomizationInfo(CustomizationSet set) + private void DrawCustomizationInfo(CustomizeSet set) { - using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender}"); + using var tree = ImRaii.TreeNode($"{customize.ClanName(set.Clan, set.Gender)} {set.Gender}"); if (!tree) return; @@ -48,9 +47,9 @@ public class CustomizationServicePanel(CustomizationService _customization) : ID } } - private void DrawNpcCustomizationInfo(CustomizationSet set) + private void DrawNpcCustomizationInfo(CustomizeSet set) { - using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender} (NPC Options)"); + using var tree = ImRaii.TreeNode($"{customize.ClanName(set.Clan, set.Gender)} {set.Gender} (NPC Options)"); if (!tree) return; diff --git a/Glamourer/Gui/Tabs/DebugTab/CustomizationUnlockPanel.cs b/Glamourer/Gui/Tabs/DebugTab/CustomizationUnlockPanel.cs index f253923..8703d8c 100644 --- a/Glamourer/Gui/Tabs/DebugTab/CustomizationUnlockPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/CustomizationUnlockPanel.cs @@ -1,14 +1,15 @@ using System; using System.Numerics; -using Glamourer.Customization; using Glamourer.Unlocks; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; namespace Glamourer.Gui.Tabs.DebugTab; -public class CustomizationUnlockPanel(CustomizeUnlockManager _customizeUnlocks) : IDebugTabTree +public class CustomizationUnlockPanel(CustomizeUnlockManager _customizeUnlocks) : IGameDataDrawer { public string Label => "Customizations"; diff --git a/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs b/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs index 85ba96c..b06e807 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DatFilePanel.cs @@ -1,14 +1,14 @@ using System.IO; using System.Numerics; -using Glamourer.Customization; using Glamourer.Interop; using ImGuiNET; using OtterGui; -using OtterGui.Raii; +using Penumbra.GameData.Files; +using Penumbra.GameData.Gui.Debug; namespace Glamourer.Gui.Tabs.DebugTab; -public class DatFilePanel(ImportService _importService) : IDebugTabTree +public class DatFilePanel(ImportService _importService) : IGameDataDrawer { public string Label => "Character Dat File"; @@ -35,7 +35,7 @@ public class DatFilePanel(ImportService _importService) : IDebugTabTree 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.Customize.ToString()); ImGui.TextUnformatted(_datFile.Value.Description); } } diff --git a/Glamourer/Gui/Tabs/DebugTab/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab/DebugTab.cs index 52c7b53..ffe6945 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DebugTab.cs @@ -1,20 +1,14 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface.Utility; using ImGuiNET; -using Microsoft.Extensions.DependencyInjection; -using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; -using ImGuiClip = Dalamud.Interface.Utility.ImGuiClip; namespace Glamourer.Gui.Tabs.DebugTab; -public unsafe class DebugTab(IServiceProvider _provider) : ITab +public unsafe class DebugTab(ServiceManager manager) : ITab { - private readonly Configuration _config = _provider.GetRequiredService(); + private readonly Configuration _config = manager.GetService(); public bool IsVisible => _config.DebugMode; @@ -24,11 +18,11 @@ public unsafe class DebugTab(IServiceProvider _provider) : ITab private readonly DebugTabHeader[] _headers = [ - DebugTabHeader.CreateInterop(_provider), - DebugTabHeader.CreateGameData(_provider), - DebugTabHeader.CreateDesigns(_provider), - DebugTabHeader.CreateState(_provider), - DebugTabHeader.CreateUnlocks(_provider), + DebugTabHeader.CreateInterop(manager.Provider!), + DebugTabHeader.CreateGameData(manager.Provider!), + DebugTabHeader.CreateDesigns(manager.Provider!), + DebugTabHeader.CreateState(manager.Provider!), + DebugTabHeader.CreateUnlocks(manager.Provider!), ]; public void DrawContent() @@ -37,55 +31,12 @@ public unsafe class DebugTab(IServiceProvider _provider) : ITab if (!child) return; + if (ImGui.CollapsingHeader("General")) + { + manager.Timers.Draw("Timers"); + } + foreach (var header in _headers) header.Draw(); } - - public static void DrawInputModelSet(bool withWeapon, ref int setId, ref int secondaryId, ref int variant) - { - 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); - } - - public 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); - } } diff --git a/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs b/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs index e010b2a..b61b4a6 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DebugTabHeader.cs @@ -3,21 +3,14 @@ using System.Collections.Generic; using ImGuiNET; using Microsoft.Extensions.DependencyInjection; using OtterGui.Raii; +using Penumbra.GameData.Gui.Debug; namespace Glamourer.Gui.Tabs.DebugTab; -public interface IDebugTabTree +public class DebugTabHeader(string label, params IGameDataDrawer[] subTrees) { - public string Label { get; } - public void Draw(); - - public bool Disabled { get; } -} - -public class DebugTabHeader(string label, params IDebugTabTree[] subTrees) -{ - public string Label { get; } = label; - public IReadOnlyList SubTrees { get; } = subTrees; + public string Label { get; } = label; + public IReadOnlyList SubTrees { get; } = subTrees; public void Draw() { @@ -26,14 +19,13 @@ public class DebugTabHeader(string label, params IDebugTabTree[] subTrees) foreach (var subTree in SubTrees) { - using (var disabled = ImRaii.Disabled(subTree.Disabled)) + using var disabled = ImRaii.Disabled(subTree.Disabled); + using var tree = ImRaii.TreeNode(subTree.Label); + if (tree) { - using var tree = ImRaii.TreeNode(subTree.Label); - if (!tree) - continue; + disabled.Dispose(); + subTree.Draw(); } - - subTree.Draw(); } } @@ -52,13 +44,15 @@ public class DebugTabHeader(string label, params IDebugTabTree[] subTrees) => new ( "Game Data", - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), - provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService() ); diff --git a/Glamourer/Gui/Tabs/DebugTab/DesignConverterPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DesignConverterPanel.cs index 9553f72..0de63ed 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DesignConverterPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DesignConverterPanel.cs @@ -8,10 +8,11 @@ using ImGuiNET; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData.Gui.Debug; namespace Glamourer.Gui.Tabs.DebugTab; -public class DesignConverterPanel(DesignConverter _designConverter) : IDebugTabTree +public class DesignConverterPanel(DesignConverter _designConverter) : IGameDataDrawer { public string Label => "Design Converter"; diff --git a/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs index af4814a..8f40388 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs @@ -1,17 +1,16 @@ using System; using System.Linq; using Dalamud.Interface; -using Glamourer.Customization; using Glamourer.Designs; -using Glamourer.Structs; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; namespace Glamourer.Gui.Tabs.DebugTab; -public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _designFileSystem) : IDebugTabTree +public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _designFileSystem) : IGameDataDrawer { public string Label => $"Design Manager ({_designManager.Designs.Count} Designs)###Design Manager"; diff --git a/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs index 481006e..a4e17ed 100644 --- a/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/DesignTesterPanel.cs @@ -1,19 +1,18 @@ using System; using System.Linq; using Dalamud.Interface; -using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Services; -using Glamourer.Structs; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using Penumbra.GameData.Data; +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) : IDebugTabTree +public class DesignTesterPanel(ItemManager _items, HumanModelList _humans) : IGameDataDrawer { public string Label => "Base64 Design Tester"; @@ -85,7 +84,7 @@ public class DesignTesterPanel(ItemManager _items, HumanModelList _humans) : IDe 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 })) + using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 0 })) { foreach (var (c1, c2) in _restore.Zip(_base64)) { @@ -99,7 +98,7 @@ public class DesignTesterPanel(ItemManager _items, HumanModelList _humans) : IDe foreach (var ((b1, b2), idx) in _base64Bytes.Zip(_restoreBytes).WithIndex()) { - using (var group = ImRaii.Group()) + using (_ = ImRaii.Group()) { ImGui.TextUnformatted(idx.ToString("D2")); ImGui.TextUnformatted(b1.ToString("X2")); @@ -121,7 +120,7 @@ public class DesignTesterPanel(ItemManager _items, HumanModelList _humans) : IDe using var font = ImRaii.PushFont(UiBuilder.MonoFont); foreach (var (b, idx) in _base64Bytes.WithIndex()) { - using (var group = ImRaii.Group()) + using (_ = ImRaii.Group()) { ImGui.TextUnformatted(idx.ToString("D2")); ImGui.TextUnformatted(b.ToString("X2")); diff --git a/Glamourer/Gui/Tabs/DebugTab/FunPanel.cs b/Glamourer/Gui/Tabs/DebugTab/FunPanel.cs index 9afc125..c517070 100644 --- a/Glamourer/Gui/Tabs/DebugTab/FunPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/FunPanel.cs @@ -1,9 +1,10 @@ using Glamourer.State; using ImGuiNET; +using Penumbra.GameData.Gui.Debug; namespace Glamourer.Gui.Tabs.DebugTab; -public class FunPanel(FunModule _funModule, Configuration _config) : IDebugTabTree +public class FunPanel(FunModule _funModule, Configuration _config) : IGameDataDrawer { public string Label => "Fun Module"; diff --git a/Glamourer/Gui/Tabs/DebugTab/IdentifierPanel.cs b/Glamourer/Gui/Tabs/DebugTab/IdentifierPanel.cs deleted file mode 100644 index b17f35a..0000000 --- a/Glamourer/Gui/Tabs/DebugTab/IdentifierPanel.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Linq; -using Dalamud.Interface.Utility; -using Glamourer.Services; -using ImGuiNET; -using OtterGui; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Glamourer.Gui.Tabs.DebugTab; - -public class IdentifierPanel(ItemManager _items) : IDebugTabTree -{ - public string Label - => "Identifier Service"; - - public bool Disabled - => !_items.IdentifierService.Valid; - - private string _gamePath = string.Empty; - private int _setId; - private int _secondaryId; - private int _variant; - - public void Draw() - { - 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(); - DebugTab.DrawInputModelSet(true, ref _setId, ref _secondaryId, ref _variant); - - 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))); - } -} diff --git a/Glamourer/Gui/Tabs/DebugTab/InventoryPanel.cs b/Glamourer/Gui/Tabs/DebugTab/InventoryPanel.cs index 1334f2b..bd447e7 100644 --- a/Glamourer/Gui/Tabs/DebugTab/InventoryPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/InventoryPanel.cs @@ -2,10 +2,11 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData.Gui.Debug; namespace Glamourer.Gui.Tabs.DebugTab; -public unsafe class InventoryPanel : IDebugTabTree +public unsafe class InventoryPanel : IGameDataDrawer { public string Label => "Inventory"; diff --git a/Glamourer/Gui/Tabs/DebugTab/IpcTesterPanel.cs b/Glamourer/Gui/Tabs/DebugTab/IpcTesterPanel.cs index 02a2e21..24e839e 100644 --- a/Glamourer/Gui/Tabs/DebugTab/IpcTesterPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/IpcTesterPanel.cs @@ -1,16 +1,19 @@ -using Dalamud.Game.ClientState.Objects.Types; +using System; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin; using Glamourer.Api; using Glamourer.Interop; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using System; -using System.Linq; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui; +using Penumbra.GameData.Gui.Debug; +using Penumbra.GameData.Structs; namespace Glamourer.Gui.Tabs.DebugTab; -public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManager _objectManager) : IDebugTabTree +public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManager _objectManager) : IGameDataDrawer { public string Label => "IPC Tester"; @@ -18,17 +21,23 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag public bool Disabled => false; - private int _gameObjectIndex; - private string _gameObjectName = string.Empty; - private string _base64Apply = string.Empty; - private string _designIdentifier = string.Empty; + private int _gameObjectIndex; + private CustomItemId _customItemId; + private StainId _stainId; + private EquipSlot _slot = EquipSlot.Head; + private string _gameObjectName = string.Empty; + private string _base64Apply = string.Empty; + private string _designIdentifier = string.Empty; + private GlamourerIpc.GlamourerErrorCode _setItemEc; + private GlamourerIpc.GlamourerErrorCode _setItemByActorNameEc; - public void Draw() + public unsafe void Draw() { 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); - ImGui.InputTextWithHint("##identifier", "Design identifier...", ref _designIdentifier, 36); + ImGui.InputTextWithHint("##gameObject", "Character Name...", ref _gameObjectName, 64); + ImGui.InputTextWithHint("##base64", "Design Base64...", ref _base64Apply, 2047); + ImGui.InputTextWithHint("##identifier", "Design identifier...", ref _designIdentifier, 36); + DrawItemInput(); using var table = ImRaii.Table("##ipc", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) return; @@ -54,15 +63,6 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag else ImGui.TextUnformatted("Error"); - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelGetDesignList); - ImGui.TableNextColumn(); - var designList = GlamourerIpc.GetDesignListSubscriber(_pluginInterface) - .Invoke(); - if (designList != null) - ImGuiUtil.CopyOnClickSelectable(string.Join(", ", designList)); - else - ImGui.TextUnformatted("Error"); - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelRevert); ImGui.TableNextColumn(); if (ImGui.Button("Revert##Name")) @@ -106,6 +106,19 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag GlamourerIpc.ApplyOnlyCustomizationToCharacterSubscriber(_pluginInterface) .Invoke(_base64Apply, _objectManager.Objects[_gameObjectIndex] as Character); + + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyByGuid); + ImGui.TableNextColumn(); + if (ImGui.Button("Apply##ByGuidName") && Guid.TryParse(_designIdentifier, out var guid1)) + GlamourerIpc.ApplyByGuidSubscriber(_pluginInterface).Invoke(guid1, _gameObjectName); + + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyByGuidToCharacter); + ImGui.TableNextColumn(); + if (ImGui.Button("Apply##ByGuidCharacter") && Guid.TryParse(_designIdentifier, out var guid2)) + GlamourerIpc.ApplyByGuidToCharacterSubscriber(_pluginInterface) + .Invoke(guid2, _objectManager.Objects[_gameObjectIndex] as Character); + + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelUnlock); ImGui.TableNextColumn(); if (ImGui.Button("Unlock##CustomizeCharacter")) @@ -118,15 +131,52 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag GlamourerIpc.RevertToAutomationCharacterSubscriber(_pluginInterface) .Invoke(_objectManager.Objects[_gameObjectIndex] as Character, 1337); - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyByGuid); - ImGui.TableNextColumn(); - if (ImGui.Button("Apply##ByGuidName")) - GlamourerIpc.ApplyByGuidSubscriber(_pluginInterface).Invoke(Guid.Parse(_designIdentifier), _gameObjectName); - ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyByGuidToCharacter); + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelGetDesignList); ImGui.TableNextColumn(); - if (ImGui.Button("Apply##ByGuidCharacter")) - GlamourerIpc.ApplyByGuidToCharacterSubscriber(_pluginInterface) - .Invoke(Guid.Parse(_designIdentifier), _objectManager.Objects[_gameObjectIndex] as Character); + var designList = GlamourerIpc.GetDesignListSubscriber(_pluginInterface) + .Invoke(); + if (ImGui.Button($"Copy {designList.Length} Designs to Clipboard###CopyDesignList")) + ImGui.SetClipboardText(string.Join("\n", designList)); + + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelSetItem); + ImGui.TableNextColumn(); + if (ImGui.Button("Set##SetItem")) + _setItemEc = (GlamourerIpc.GlamourerErrorCode)GlamourerIpc.SetItemSubscriber(_pluginInterface) + .Invoke(_objectManager.Objects[_gameObjectIndex] as Character, (byte)_slot, _customItemId.Id, _stainId.Id, 1337); + if (_setItemEc != GlamourerIpc.GlamourerErrorCode.Success) + { + ImGui.SameLine(); + ImGui.TextUnformatted(_setItemEc.ToString()); + } + + ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelSetItemByActorName); + ImGui.TableNextColumn(); + if (ImGui.Button("Set##SetItemByActorName")) + _setItemByActorNameEc = (GlamourerIpc.GlamourerErrorCode)GlamourerIpc.SetItemByActorNameSubscriber(_pluginInterface) + .Invoke(_gameObjectName, (byte)_slot, _customItemId.Id, _stainId.Id, 1337); + if (_setItemByActorNameEc != GlamourerIpc.GlamourerErrorCode.Success) + { + ImGui.SameLine(); + ImGui.TextUnformatted(_setItemByActorNameEc.ToString()); + } + } + + private void DrawItemInput() + { + var tmp = _customItemId.Id; + if (ImGuiUtil.InputUlong("Custom Item ID", ref tmp)) + _customItemId = (CustomItemId)tmp; + var width = ImGui.GetContentRegionAvail().X; + EquipSlotCombo.Draw("Equip Slot", string.Empty, ref _slot); + var value = (int)_stainId.Id; + ImGui.SameLine(); + width -= ImGui.GetContentRegionAvail().X; + ImGui.SetNextItemWidth(width); + if (ImGui.InputInt("Stain ID", ref value, 1, 3)) + { + value = Math.Clamp(value, 0, byte.MaxValue); + _stainId = (StainId)value; + } } } diff --git a/Glamourer/Gui/Tabs/DebugTab/ItemManagerPanel.cs b/Glamourer/Gui/Tabs/DebugTab/ItemManagerPanel.cs deleted file mode 100644 index 7ef34b6..0000000 --- a/Glamourer/Gui/Tabs/DebugTab/ItemManagerPanel.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Linq; -using Glamourer.Services; -using ImGuiNET; -using OtterGui.Raii; -using Penumbra.GameData.Enums; - -namespace Glamourer.Gui.Tabs.DebugTab; - -public class ItemManagerPanel(ItemManager _items) : IDebugTabTree -{ - public string Label - => "Item Manager"; - - public bool Disabled - => !_items.ItemService.Valid; - - private string _itemFilter = string.Empty; - - public void Draw() - { - ImRaii.TreeNode($"Default Sword: {_items.DefaultSword.Name} ({_items.DefaultSword.ItemId}) ({_items.DefaultSword.Weapon()})", - ImGuiTreeNodeFlags.Leaf).Dispose(); - DebugTab.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)); - DebugTab.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)) - { - DebugTab.DrawNameTable(type.ToName(), ref _itemFilter, - _items.ItemService.AwaitedService[type] - .Select(p => (p.ItemId.Id, $"{p.Name} ({(p.WeaponType == 0 ? p.Armor().ToString() : p.Weapon().ToString())})"))); - } - } -} diff --git a/Glamourer/Gui/Tabs/DebugTab/ItemUnlockPanel.cs b/Glamourer/Gui/Tabs/DebugTab/ItemUnlockPanel.cs index a218d96..efcc664 100644 --- a/Glamourer/Gui/Tabs/DebugTab/ItemUnlockPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/ItemUnlockPanel.cs @@ -7,11 +7,12 @@ using ImGuiNET; 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) : IDebugTabTree +public class ItemUnlockPanel(ItemUnlockManager _itemUnlocks, ItemManager _items) : IGameDataDrawer { public string Label => "Unlocked Items"; @@ -39,7 +40,7 @@ public class ItemUnlockPanel(ItemUnlockManager _itemUnlocks, ItemManager _items) var remainder = ImGuiClip.ClippedDraw(_itemUnlocks, skips, t => { ImGuiUtil.DrawTableColumn(t.Key.ToString()); - if (_items.ItemService.AwaitedService.TryGetValue(t.Key, EquipSlot.MainHand, out var equip)) + if (_items.ItemData.TryGetValue(t.Key, EquipSlot.MainHand, out var equip)) { ImGuiUtil.DrawTableColumn(equip.Name); ImGuiUtil.DrawTableColumn(equip.Type.ToName()); diff --git a/Glamourer/Gui/Tabs/DebugTab/JobPanel.cs b/Glamourer/Gui/Tabs/DebugTab/JobPanel.cs deleted file mode 100644 index 0fa8765..0000000 --- a/Glamourer/Gui/Tabs/DebugTab/JobPanel.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Glamourer.Interop; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; - -namespace Glamourer.Gui.Tabs.DebugTab; - -public class JobPanel(JobService _jobs) : IDebugTabTree -{ - public string Label - => "Job Service"; - - public bool Disabled - => false; - - public void Draw() - { - DrawJobs(); - DrawJobGroups(); - DrawValidJobGroups(); - } - - private void DrawJobs() - { - using var t = ImRaii.TreeNode("Jobs"); - if (!t) - return; - - using var table = ImRaii.Table("##jobs", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (!table) - return; - - foreach (var (id, job) in _jobs.Jobs) - { - ImGuiUtil.DrawTableColumn(id.ToString("D3")); - ImGuiUtil.DrawTableColumn(job.Name); - ImGuiUtil.DrawTableColumn(job.Abbreviation); - } - } - - private void DrawJobGroups() - { - using var t = ImRaii.TreeNode("All Job Groups"); - if (!t) - return; - - using var table = ImRaii.Table("##groups", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (!table) - return; - - foreach (var (group, idx) in _jobs.AllJobGroups.WithIndex()) - { - ImGuiUtil.DrawTableColumn(idx.ToString("D3")); - ImGuiUtil.DrawTableColumn(group.Name); - ImGuiUtil.DrawTableColumn(group.Count.ToString()); - } - } - - private void DrawValidJobGroups() - { - using var t = ImRaii.TreeNode("Valid Job Groups"); - if (!t) - return; - - using var table = ImRaii.Table("##groups", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (!table) - return; - - foreach (var (id, group) in _jobs.JobGroups) - { - ImGuiUtil.DrawTableColumn(id.ToString("D3")); - ImGuiUtil.DrawTableColumn(group.Name); - ImGuiUtil.DrawTableColumn(group.Count.ToString()); - } - } -} diff --git a/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs b/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs index 0b0526b..c70396c 100644 --- a/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs @@ -1,14 +1,13 @@ using System; using System.Numerics; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Glamourer.Customization; using Glamourer.Interop; using Glamourer.Interop.Structs; -using Glamourer.Structs; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; using Penumbra.GameData.Structs; namespace Glamourer.Gui.Tabs.DebugTab; @@ -18,7 +17,7 @@ public unsafe class ModelEvaluationPanel( VisorService _visorService, UpdateSlotService _updateSlotService, ChangeCustomizeService _changeCustomizeService, - CrestService _crestService) : IDebugTabTree + CrestService _crestService) : IGameDataDrawer { public string Label => "Model Evaluation"; @@ -126,15 +125,14 @@ public unsafe class ModelEvaluationPanel( model.AsHuman->Head.Value == 0 ? actor.GetArmor(EquipSlot.Head) : CharacterArmor.Empty); } - private void DrawWeaponState(Actor actor, Model model) + 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"); - var text = string.Empty; - + string text; if (!model.IsHuman) { text = "No Model"; @@ -146,19 +144,14 @@ public unsafe class ModelEvaluationPanel( else { var weapon = (DrawObject*)model.AsDrawObject->Object.ChildObject; - if ((weapon->Flags & 0x09) == 0x09) - text = "Visible"; - else - text = "Hidden"; + text = (weapon->Flags & 0x09) == 0x09 ? "Visible" : "Hidden"; } ImGuiUtil.DrawTableColumn(text); ImGui.TableNextColumn(); - if (!model.IsHuman) - return; } - private void DrawWetness(Actor actor, Model model) + private static void DrawWetness(Actor actor, Model model) { using var id = ImRaii.PushId("Wetness"); ImGuiUtil.DrawTableColumn("Wetness"); @@ -199,7 +192,7 @@ public unsafe class ModelEvaluationPanel( if (ImGui.SmallButton("Change Piece")) _updateSlotService.UpdateArmor(model, slot, - new CharacterArmor((SetId)(slot == EquipSlot.Hands ? 6064 : slot == EquipSlot.Head ? 6072 : 1), 1, 0)); + new CharacterArmor((PrimaryId)(slot == EquipSlot.Hands ? 6064 : slot == EquipSlot.Head ? 6072 : 1), 1, 0)); ImGui.SameLine(); if (ImGui.SmallButton("Change Stain")) _updateSlotService.UpdateStain(model, slot, 5); @@ -212,12 +205,12 @@ public unsafe class ModelEvaluationPanel( 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()); + var actorCustomize = actor.IsCharacter + ? *(CustomizeArray*)&actor.AsCharacter->DrawData.CustomizeData + : new CustomizeArray(); + var modelCustomize = model.IsHuman + ? *(CustomizeArray*)model.AsHuman->Customize.Data + : new CustomizeArray(); foreach (var type in Enum.GetValues()) { using var id2 = ImRaii.PushId((int)type); @@ -235,7 +228,7 @@ public unsafe class ModelEvaluationPanel( var shift = BitOperations.TrailingZeroCount(mask); var newValue = value + (1 << shift); modelCustomize.Set(type, (CustomizeValue)newValue); - _changeCustomizeService.UpdateCustomize(model, modelCustomize.Data); + _changeCustomizeService.UpdateCustomize(model, modelCustomize); } ImGui.SameLine(); @@ -246,14 +239,14 @@ public unsafe class ModelEvaluationPanel( var shift = BitOperations.TrailingZeroCount(mask); var newValue = value - (1 << shift); modelCustomize.Set(type, (CustomizeValue)newValue); - _changeCustomizeService.UpdateCustomize(model, modelCustomize.Data); + _changeCustomizeService.UpdateCustomize(model, modelCustomize); } ImGui.SameLine(); if (ImGui.SmallButton("Reset")) { modelCustomize.Set(type, actorCustomize[type]); - _changeCustomizeService.UpdateCustomize(model, modelCustomize.Data); + _changeCustomizeService.UpdateCustomize(model, modelCustomize); } } } diff --git a/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs b/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs index 53af228..2ebf215 100644 --- a/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/NpcAppearancePanel.cs @@ -3,19 +3,22 @@ using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Events; +using Glamourer.GameData; using Glamourer.Interop; using Glamourer.State; using ImGuiNET; 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 NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectManager _objectManager, DesignConverter _designConverter) : IDebugTabTree +public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectManager _objectManager, DesignConverter _designConverter) + : IGameDataDrawer { public string Label => "NPC Appearance"; @@ -23,23 +26,27 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM public bool Disabled => false; - private string _npcFilter = string.Empty; - private bool _customizeOrGear = false; + private string _npcFilter = string.Empty; + private bool _customizeOrGear; public void Draw() { ImGui.Checkbox("Compare Customize (or Gear)", ref _customizeOrGear); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - ImGui.InputTextWithHint("##npcFilter", "Filter...", ref _npcFilter, 64); + var resetScroll = ImGui.InputTextWithHint("##npcFilter", "Filter...", ref _npcFilter, 64); - using var table = ImRaii.Table("npcs", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, + using var table = ImRaii.Table("npcs", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, new Vector2(-1, 400 * ImGuiHelpers.GlobalScale)); if (!table) return; + if (resetScroll) + ImGui.SetScrollY(0); + ImGui.TableSetupColumn("Button", ImGuiTableColumnFlags.WidthFixed); ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, ImGuiHelpers.GlobalScale * 300); ImGui.TableSetupColumn("Kind", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Id", ImGuiTableColumnFlags.WidthFixed); ImGui.TableSetupColumn("Visor", ImGuiTableColumnFlags.WidthFixed); ImGui.TableSetupColumn("Compare", ImGuiTableColumnFlags.WidthStretch); @@ -48,21 +55,19 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM ImGui.TableNextRow(); var idx = 0; var remainder = ImGuiClip.FilteredClippedDraw(_npcCombo.Items, skips, - d => d.Name.Contains(_npcFilter, StringComparison.OrdinalIgnoreCase), Draw); + d => d.Name.Contains(_npcFilter, StringComparison.OrdinalIgnoreCase), DrawData); ImGui.TableNextColumn(); ImGuiClip.DrawEndDummy(remainder, ImGui.GetFrameHeightWithSpacing()); - - return; - void Draw(CustomizationNpcOptions.NpcData data) + void DrawData(NpcData data) { using var id = ImRaii.PushId(idx++); var disabled = !_state.GetOrCreate(_objectManager.Player, out var state); ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton("Apply", Vector2.Zero, string.Empty, disabled, false)) + if (ImGuiUtil.DrawDisabledButton("Apply", Vector2.Zero, string.Empty, disabled)) { - foreach (var (slot, item, stain) in _designConverter.FromDrawData(data.Equip.ToArray(), data.Mainhand, data.Offhand)) + foreach (var (slot, item, stain) in _designConverter.FromDrawData(data.Equip.ToArray(), data.Mainhand, data.Offhand, true)) _state.ChangeEquip(state!, slot, item, stain, StateChanged.Source.Manual); _state.ChangeVisorState(state!, data.VisorToggled, StateChanged.Source.Manual); _state.ChangeCustomize(state!, data.Customize, CustomizeFlagExtensions.All, StateChanged.Source.Manual); @@ -76,7 +81,11 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(data.Kind is ObjectKind.BattleNpc ? "B" : "E"); - using (var icon = ImRaii.PushFont(UiBuilder.IconFont)) + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(data.Id.Id.ToString()); + + using (_ = ImRaii.PushFont(UiBuilder.IconFont)) { ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); @@ -86,7 +95,7 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM using var mono = ImRaii.PushFont(UiBuilder.MonoFont); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(_customizeOrGear ? data.Customize.Data.ToString() : data.WriteGear()); + ImGui.TextUnformatted(_customizeOrGear ? data.Customize.ToString() : data.WriteGear()); } } } diff --git a/Glamourer/Gui/Tabs/DebugTab/ObjectManagerPanel.cs b/Glamourer/Gui/Tabs/DebugTab/ObjectManagerPanel.cs index cf9e443..e90cbda 100644 --- a/Glamourer/Gui/Tabs/DebugTab/ObjectManagerPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/ObjectManagerPanel.cs @@ -3,14 +3,15 @@ using System.Globalization; using System.Linq; using System.Numerics; using Glamourer.Interop; -using Glamourer.Services; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Gui.Debug; namespace Glamourer.Gui.Tabs.DebugTab; -public class ObjectManagerPanel(ObjectManager _objectManager, ActorService _actors) : IDebugTabTree +public class ObjectManagerPanel(ObjectManager _objectManager, ActorManager _actors) : IGameDataDrawer { public string Label => "Object Manager"; @@ -33,7 +34,7 @@ public class ObjectManagerPanel(ObjectManager _objectManager, ActorService _acto ImGui.TableNextColumn(); ImGuiUtil.DrawTableColumn("World"); - ImGuiUtil.DrawTableColumn(_actors.Valid ? _actors.AwaitedService.Data.ToWorldName(_objectManager.World) : "Service Missing"); + ImGuiUtil.DrawTableColumn(_actors.Finished ? _actors.Data.ToWorldName(_objectManager.World) : "Service Missing"); ImGuiUtil.DrawTableColumn(_objectManager.World.ToString()); ImGuiUtil.DrawTableColumn("Player Character"); diff --git a/Glamourer/Gui/Tabs/DebugTab/PenumbraPanel.cs b/Glamourer/Gui/Tabs/DebugTab/PenumbraPanel.cs index 6352412..9c7ccc1 100644 --- a/Glamourer/Gui/Tabs/DebugTab/PenumbraPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/PenumbraPanel.cs @@ -1,8 +1,5 @@ using System; -using Dalamud.Interface; using Dalamud.Interface.Utility; -using Glamourer.Gui.Tabs.DebugTab; -using Glamourer.Gui; using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using ImGuiNET; @@ -10,11 +7,12 @@ using OtterGui; using OtterGui.Raii; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; +using Penumbra.GameData.Gui.Debug; using Penumbra.GameData.Structs; namespace Glamourer.Gui.Tabs.DebugTab; -public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItemTooltip _penumbraTooltip) : IDebugTabTree +public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItemTooltip _penumbraTooltip) : IGameDataDrawer { public string Label => "Penumbra Interop"; @@ -27,9 +25,6 @@ public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItem public void Draw() { - if (!ImGui.CollapsingHeader("Penumbra")) - return; - using var table = ImRaii.Table("##PenumbraTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) return; @@ -59,7 +54,7 @@ public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItem ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); ImGui.InputInt("##CutsceneIndex", ref _gameObjectIndex, 0, 0); ImGuiUtil.DrawTableColumn(_penumbra.Available - ? _penumbra.CutsceneParent(_gameObjectIndex).ToString() + ? _penumbra.CutsceneParent((ushort) _gameObjectIndex).ToString() : "Penumbra Unavailable"); ImGuiUtil.DrawTableColumn("Redraw Object"); @@ -67,7 +62,7 @@ public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItem ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); ImGui.InputInt("##redrawObject", ref _gameObjectIndex, 0, 0); ImGui.TableNextColumn(); - using (var disabled = ImRaii.Disabled(!_penumbra.Available)) + using (_ = ImRaii.Disabled(!_penumbra.Available)) { if (ImGui.SmallButton("Redraw")) _penumbra.RedrawObject((ObjectIndex)_gameObjectIndex, RedrawType.Redraw); diff --git a/Glamourer/Gui/Tabs/DebugTab/RestrictedGearPanel.cs b/Glamourer/Gui/Tabs/DebugTab/RestrictedGearPanel.cs deleted file mode 100644 index 7e1e600..0000000 --- a/Glamourer/Gui/Tabs/DebugTab/RestrictedGearPanel.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Linq; -using Glamourer.Services; -using ImGuiNET; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Glamourer.Gui.Tabs.DebugTab; - -public class RestrictedGearPanel(ItemManager _items) : IDebugTabTree -{ - public string Label - => "Restricted Gear Service"; - - public bool Disabled - => false; - - private int _setId; - private int _secondaryId; - private int _variant; - - public void Draw() - { - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Resolve Model"); - DebugTab.DrawInputModelSet(false, ref _setId, ref _secondaryId, ref _variant); - foreach (var race in Enum.GetValues().Skip(1)) - { - ReadOnlySpan genders = [Gender.Male, Gender.Female]; - foreach (var gender in genders) - { - 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}."); - } - } - } - } -} diff --git a/Glamourer/Gui/Tabs/DebugTab/RetainedStatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/RetainedStatePanel.cs index 68841bc..027c316 100644 --- a/Glamourer/Gui/Tabs/DebugTab/RetainedStatePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/RetainedStatePanel.cs @@ -3,10 +3,11 @@ using Glamourer.Interop; using Glamourer.Interop.Structs; using Glamourer.State; using OtterGui.Raii; +using Penumbra.GameData.Gui.Debug; namespace Glamourer.Gui.Tabs.DebugTab; -public class RetainedStatePanel(StateManager _stateManager, ObjectManager _objectManager) : IDebugTabTree +public class RetainedStatePanel(StateManager _stateManager, ObjectManager _objectManager) : IGameDataDrawer { public string Label => "Retained States (Inactive Actors)"; diff --git a/Glamourer/Gui/Tabs/DebugTab/StainPanel.cs b/Glamourer/Gui/Tabs/DebugTab/StainPanel.cs deleted file mode 100644 index 643b6dc..0000000 --- a/Glamourer/Gui/Tabs/DebugTab/StainPanel.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Numerics; -using Dalamud.Interface.Utility; -using Glamourer.Services; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using ImGuiClip = OtterGui.ImGuiClip; - -namespace Glamourer.Gui.Tabs.DebugTab; - -public class StainPanel(ItemManager _items) : IDebugTabTree -{ - public string Label - => "Stain Service"; - - public bool Disabled - => false; - - private string _stainFilter = string.Empty; - - public void Draw() - { - 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); - } -} diff --git a/Glamourer/Gui/Tabs/DebugTab/UnlockableItemsPanel.cs b/Glamourer/Gui/Tabs/DebugTab/UnlockableItemsPanel.cs index cfd00fa..ecd8d83 100644 --- a/Glamourer/Gui/Tabs/DebugTab/UnlockableItemsPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/UnlockableItemsPanel.cs @@ -7,11 +7,12 @@ using ImGuiNET; 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) : IDebugTabTree +public class UnlockableItemsPanel(ItemUnlockManager _itemUnlocks, ItemManager _items) : IGameDataDrawer { public string Label => "Unlockable Items"; @@ -40,7 +41,7 @@ public class UnlockableItemsPanel(ItemUnlockManager _itemUnlocks, ItemManager _i 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)) + if (_items.ItemData.TryGetValue(t.Key, EquipSlot.MainHand, out var equip)) { ImGuiUtil.DrawTableColumn(equip.Name); ImGuiUtil.DrawTableColumn(equip.Type.ToName()); diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index b6e5fa3..848bb58 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -7,14 +7,12 @@ using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Internal.Notifications; using FFXIVClientStructs.FFXIV.Client.System.Framework; using Glamourer.Automation; -using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Events; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; using Glamourer.Interop; using Glamourer.State; -using Glamourer.Structs; using ImGuiNET; using OtterGui; using OtterGui.Classes; @@ -156,9 +154,9 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer private void DrawCustomizeApplication() { - var set = _selector.Selected!.CustomizationSet; - var available = set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender; - var flags = _selector.Selected!.ApplyCustomize == 0 ? 0 : (_selector.Selected!.ApplyCustomize & available) == available ? 3 : 1; + 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; diff --git a/Glamourer/Gui/Tabs/NpcCombo.cs b/Glamourer/Gui/Tabs/NpcCombo.cs index ab8d08d..4b1274c 100644 --- a/Glamourer/Gui/Tabs/NpcCombo.cs +++ b/Glamourer/Gui/Tabs/NpcCombo.cs @@ -1,36 +1,11 @@ -using System.Collections; -using System.Collections.Generic; -using System.Threading.Tasks; -using Dalamud.Plugin.Services; -using Glamourer.Customization; -using Glamourer.Services; +using Glamourer.GameData; using OtterGui.Widgets; -using Penumbra.GameData; namespace Glamourer.Gui.Tabs; -public class NpcCombo(ActorService actorManager, IdentifierService identifier, IDataManager data) - : FilterComboBase(new LazyList(actorManager, identifier, data), false, Glamourer.Log) +public class NpcCombo(NpcCustomizeSet npcCustomizeSet) + : FilterComboCache(npcCustomizeSet, Glamourer.Log) { - private class LazyList(ActorService actorManager, IdentifierService identifier, IDataManager data) - : IReadOnlyList - { - private readonly Task> _task - = Task.Run(() => CustomizationNpcOptions.CreateNpcData(actorManager.AwaitedService.Data.ENpcs, actorManager.AwaitedService.Data.BNpcs, identifier.AwaitedService, data)); - - public IEnumerator GetEnumerator() - => _task.Result.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public int Count - => _task.Result.Count; - - public CustomizationNpcOptions.NpcData this[int index] - => _task.Result[index]; - } - - protected override string ToString(CustomizationNpcOptions.NpcData obj) + 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..bb22a85 --- /dev/null +++ b/Glamourer/Gui/Tabs/NpcTab/LocalNpcAppearanceData.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Glamourer.Designs; +using Glamourer.GameData; +using Glamourer.Services; +using ImGuiNET; +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..f57985b --- /dev/null +++ b/Glamourer/Gui/Tabs/NpcTab/NpcFilter.cs @@ -0,0 +1,45 @@ +using System; +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..cd88f97 --- /dev/null +++ b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs @@ -0,0 +1,294 @@ +using System; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Glamourer.Designs; +using Glamourer.Events; +using Glamourer.Gui.Customization; +using Glamourer.Gui.Equipment; +using Glamourer.Gui.Tabs.DesignTab; +using Glamourer.Interop; +using Glamourer.State; +using ImGuiNET; +using Lumina.Data.Parsing.Scd; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using Penumbra.GameData.Enums; + +namespace Glamourer.Gui.Tabs.NpcTab; + +public class NpcPanel( + NpcSelector _selector, + LocalNpcAppearanceData _favorites, + CustomizationDrawer _customizeDrawer, + EquipmentDrawer _equipDrawer, + DesignConverter _converter, + DesignManager _designManager, + StateManager _state, + ObjectManager _objects, + DesignColors _colors) +{ + private readonly DesignColorCombo _colorCombo = new(_colors, true); + private string _newName = string.Empty; + private DesignBase? _newDesign; + + 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), 2, ExportToClipboardButton(), SaveAsDesignButton(), FavoriteButton()); + SaveDesignDrawPopup(); + } + + private HeaderDrawer.Button FavoriteButton() + { + var (desc, color) = _favorites.IsFavorite(_selector.Selection) + ? ("Remove this NPC appearance from your favorites.", ColorId.FavoriteStarOn.Value()) + : ("Add this NPC Appearance to your favorites.", 0x80000000); + return new HeaderDrawer.Button + { + Icon = FontAwesomeIcon.Star, + OnClick = () => _favorites.ToggleFavorite(_selector.Selection), + Visible = _selector.HasSelection, + Description = desc, + TextColor = color, + }; + } + + private HeaderDrawer.Button ExportToClipboardButton() + => new() + { + 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.", + Icon = FontAwesomeIcon.Copy, + OnClick = ExportToClipboard, + Visible = _selector.HasSelection, + }; + + private HeaderDrawer.Button SaveAsDesignButton() + => new() + { + 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.", + Icon = FontAwesomeIcon.Save, + OnClick = SaveDesignOpen, + Visible = _selector.HasSelection, + }; + + private void ExportToClipboard() + { + try + { + var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags(); + var data = ToDesignData(); + var text = _converter.ShareBase64(data, applyGear, applyCustomize, applyCrest); + ImGui.SetClipboardText(text); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Could not copy {_selector.Selection.Name}'s data to clipboard.", + $"Could not copy data from NPC appearance {_selector.Selection.Kind} {_selector.Selection.Id.Id} to clipboard", + NotificationType.Error); + } + } + + private void SaveDesignOpen() + { + ImGui.OpenPopup("Save as Design"); + _newName = _selector.Selection.Name; + var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags(); + + var data = ToDesignData(); + _newDesign = _converter.Convert(data, applyGear, applyCustomize, applyCrest); + } + + 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 child = ImRaii.Child("##Panel", -Vector2.One, true); + if (!child || !_selector.HasSelection) + return; + + DrawButtonRow(); + DrawCustomization(); + DrawEquipment(); + DrawAppearanceInfo(); + } + + private void DrawButtonRow() + { + DrawApplyToSelf(); + ImGui.SameLine(); + DrawApplyToTarget(); + } + + private void DrawCustomization() + { + if (!ImGui.CollapsingHeader("Customization")) + return; + + _customizeDrawer.Draw(_selector.Selection.Customize, true, true); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + } + + private void DrawEquipment() + { + if (!ImGui.CollapsingHeader("Equipment")) + 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(ActorState.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 (!ImGuiUtil.DrawDisabledButton("Apply to Yourself", Vector2.Zero, + "Apply the current NPC appearance to your character.\nHold Control to only apply gear.\nHold Shift to only apply customizations.", + !data.Valid)) + return; + + if (_state.GetOrCreate(id, data.Objects[0], out var state)) + { + var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags(); + var design = _converter.Convert(ToDesignData(), applyGear, applyCustomize, applyCrest); + _state.ApplyDesign(design, state, StateChanged.Source.Manual); + } + } + + 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." + : "The current target can not be manipulated." + : "No valid target selected."; + 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, applyCrest) = UiHelpers.ConvertKeysToFlags(); + var design = _converter.Convert(ToDesignData(), applyGear, applyCustomize, applyCrest); + _state.ApplyDesign(design, state, StateChanged.Source.Manual); + } + } + + + private void DrawAppearanceInfo() + { + if (!ImGui.CollapsingHeader("Appearance Details")) + return; + + using var table = ImRaii.Table("Details", 2); + if (!table) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Last Update Datem").X); + ImGui.TableSetupColumn("Data", ImGuiTableColumnFlags.WidthStretch); + + var selection = _selector.Selection; + CopyButton("NPC Name", selection.Name); + CopyButton("NPC ID", 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)); + + ImGuiUtil.DrawFrameColumn("Color"); + 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. Right-Click to revert to automatic coloring.", + 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); + ImGuiUtil.HoverTooltip("The color associated with this design does not exist."); + } + + return; + + static void CopyButton(string label, string text) + { + ImGuiUtil.DrawFrameColumn(label); + ImGui.TableNextColumn(); + if (ImGui.Button(text, new Vector2(ImGui.GetContentRegionAvail().X, 0))) + ImGui.SetClipboardText(text); + ImGuiUtil.HoverTooltip("Click to copy to clipboard."); + } + } +} diff --git a/Glamourer/Gui/Tabs/NpcTab/NpcSelector.cs b/Glamourer/Gui/Tabs/NpcTab/NpcSelector.cs new file mode 100644 index 0000000..10d2264 --- /dev/null +++ b/Glamourer/Gui/Tabs/NpcTab/NpcSelector.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Glamourer.GameData; +using ImGuiNET; +using OtterGui; +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..fb64227 --- /dev/null +++ b/Glamourer/Gui/Tabs/NpcTab/NpcTab.cs @@ -0,0 +1,19 @@ +using System; +using Dalamud.Interface.Utility; +using ImGuiNET; +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 index f8395fe..340a9fd 100644 --- a/Glamourer/Gui/Tabs/SettingsTab.cs +++ b/Glamourer/Gui/Tabs/SettingsTab.cs @@ -60,7 +60,7 @@ public class SettingsTab : ITab if (!child) return; - Checkbox("Enable Auto Designs", "Enable the application of designs associated to characters to be applied automatically.", + Checkbox("Enable Auto Designs", "Enable the application of designs associated to characters in the Automation tab to be applied automatically.", _config.EnableAutoDesigns, v => _config.EnableAutoDesigns = v); ImGui.NewLine(); ImGui.NewLine(); @@ -247,22 +247,17 @@ public class SettingsTab : ITab if (ImGui.Checkbox(code, ref state)) { action(state); - _config.Codes[i] = (code, state); - _codeService.VerifyState(); - _config.Save(); + _codeService.SaveState(); } } - if (_codeService.EnabledCaptain) - { - if (ImGui.Button("Who am I?!?")) - _funModule.WhoAmI(); + if (ImGui.Button("Who am I?!?")) + _funModule.WhoAmI(); - ImGui.SameLine(); + ImGui.SameLine(); - if (ImGui.Button("Who is that!?!")) - _funModule.WhoIsThat(); - } + if (ImGui.Button("Who is that!?!")) + _funModule.WhoIsThat(); } private void DrawCodeHints() diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs index 2bd79d8..65203b9 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Numerics; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface.Utility; -using Glamourer.Customization; +using Glamourer.GameData; using Glamourer.Interop; using Glamourer.Services; using Glamourer.Unlocks; @@ -19,7 +19,7 @@ public class UnlockOverview { private readonly ItemManager _items; private readonly ItemUnlockManager _itemUnlocks; - private readonly CustomizationService _customizations; + private readonly CustomizeService _customizations; private readonly CustomizeUnlockManager _customizeUnlocks; private readonly PenumbraChangedItemTooltip _tooltip; private readonly TextureService _textures; @@ -41,7 +41,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() || !_items.ItemData.ByType.TryGetValue(type, out var items) || items.Count == 0) continue; if (ImGui.Selectable(type.ToName(), _selected1 == type)) @@ -52,25 +52,22 @@ public class UnlockOverview } } - foreach (var clan in _customizations.AwaitedService.Clans) + foreach (var (clan, gender) in CustomizeManager.AllSets()) { - foreach (var gender in _customizations.AwaitedService.Genders) - { - if (_customizations.AwaitedService.GetList(clan, gender).HairStyles.Count == 0) - continue; + 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; - } + if (ImGui.Selectable($"{(gender is Gender.Male ? '♂' : '♀')} {clan.ToShortName()} Hair & Paint", + _selected2 == clan && _selected3 == gender)) + { + _selected1 = FullEquipType.Unknown; + _selected2 = clan; + _selected3 = gender; } } } - public UnlockOverview(ItemManager items, CustomizationService customizations, ItemUnlockManager itemUnlocks, + public UnlockOverview(ItemManager items, CustomizeService customizations, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks, PenumbraChangedItemTooltip tooltip, TextureService textures, CodeService codes, JobService jobs, FavoriteManager favorites) { @@ -107,7 +104,7 @@ public class UnlockOverview 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); @@ -121,10 +118,10 @@ public class UnlockOverview continue; var unlocked = _customizeUnlocks.IsUnlocked(customize, out var time); - var icon = _customizations.AwaitedService.GetIcon(customize.IconId); + var icon = _customizations.Manager.GetIcon(customize.IconId); ImGui.Image(icon.ImGuiHandle, iconSize, Vector2.Zero, Vector2.One, - unlocked || _codes.EnabledShirts ? Vector4.One : UnavailableTint); + unlocked || _codes.Enabled(CodeService.CodeFlag.Shirts) ? Vector4.One : UnavailableTint); if (ImGui.IsItemHovered()) { using var tt = ImRaii.Tooltip(); @@ -150,7 +147,7 @@ public class UnlockOverview private void DrawItems() { - if (!_items.ItemService.AwaitedService.TryGetValue(_selected1, out var items)) + if (!_items.ItemData.ByType.TryGetValue(_selected1, out var items)) return; var spacing = IconSpacing; @@ -160,6 +157,30 @@ public class UnlockOverview var numRows = (items.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, items.Count); + var counter = 0; + for (var idx = skips * iconsPerRow; idx < end; ++idx) + { + DrawItem(items[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); @@ -168,7 +189,7 @@ public class UnlockOverview 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); + 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); @@ -189,7 +210,7 @@ public class UnlockOverview 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)}"); + $"{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( @@ -219,29 +240,6 @@ public class UnlockOverview _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 counter = 0; - for (var idx = skips * iconsPerRow; idx < end; ++idx) - { - DrawItem(items[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); } private static Vector2 IconSpacing diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs index 5ac087f..d7e1ce3 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockTable.cs @@ -8,7 +8,6 @@ using Dalamud.Interface.Utility; using Glamourer.Events; using Glamourer.Interop; using Glamourer.Services; -using Glamourer.Structs; using Glamourer.Unlocks; using ImGuiNET; using OtterGui; @@ -278,7 +277,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); @@ -297,7 +296,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); @@ -411,21 +410,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/ToggleDrawData.cs b/Glamourer/Gui/ToggleDrawData.cs index dda4584..3991893 100644 --- a/Glamourer/Gui/ToggleDrawData.cs +++ b/Glamourer/Gui/ToggleDrawData.cs @@ -2,7 +2,6 @@ using Glamourer.Designs; using Glamourer.Events; using Glamourer.State; -using Glamourer.Structs; using Penumbra.GameData.Enums; namespace Glamourer.Gui; @@ -101,4 +100,23 @@ public ref struct ToggleDrawData SetValue = setValue, }; } + + public static ToggleDrawData FromValue(ActorState.MetaIndex index, bool value) + { + var (label, tooltip) = index switch + { + ActorState.MetaIndex.HatState => ("Hat Visible", "Hide or show the characters head gear."), + ActorState.MetaIndex.VisorState => ("Visor Toggled", "Toggle the visor state of the characters head gear."), + ActorState.MetaIndex.WeaponState => ("Weapon Visible", "Hide or show the characters weapons when not drawn."), + ActorState.MetaIndex.Wetness => ("Force Wetness", "Force the character to be wet or not."), + _ => throw new Exception("Unsupported meta index."), + }; + return new ToggleDrawData + { + Label = label, + Tooltip = tooltip, + Locked = true, + CurrentValue = value, + }; + } } diff --git a/Glamourer/Gui/UiHelpers.cs b/Glamourer/Gui/UiHelpers.cs index 6e83838..916bdaa 100644 --- a/Glamourer/Gui/UiHelpers.cs +++ b/Glamourer/Gui/UiHelpers.cs @@ -1,9 +1,7 @@ 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 Lumina.Misc; @@ -27,7 +25,7 @@ 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) { @@ -49,9 +47,21 @@ 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; } @@ -59,9 +69,8 @@ public static class UiHelpers public static (bool, bool) DrawMetaToggle(string label, bool currentValue, bool currentApply, out bool newValue, out bool newApply, bool locked) { - var flags = (sbyte)(currentApply ? currentValue ? 1 : -1 : 0); - 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)) { if (new TristateCheckbox(ColorId.TriStateCross.Value(), ColorId.TriStateCheck.Value(), ColorId.TriStateNeutral.Value()).Draw( "##" + label, flags, out flags)) @@ -82,7 +91,8 @@ public static class UiHelpers 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); return (currentValue != newValue, currentApply != newApply); diff --git a/Glamourer/Interop/ChangeCustomizeService.cs b/Glamourer/Interop/ChangeCustomizeService.cs index 9e9a043..947ce43 100644 --- a/Glamourer/Interop/ChangeCustomizeService.cs +++ b/Glamourer/Interop/ChangeCustomizeService.cs @@ -1,13 +1,11 @@ using System; -using System.Threading; using Dalamud.Hooking; using Dalamud.Plugin.Services; 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.Structs; namespace Glamourer.Interop; @@ -16,7 +14,7 @@ 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 : EventWrapper>, ChangeCustomizeService.Priority> { private readonly PenumbraReloaded _penumbraReloaded; private readonly IGameInteropProvider _interop; @@ -64,7 +62,7 @@ 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; @@ -75,17 +73,18 @@ public unsafe class ChangeCustomizeService : EventWrapper UpdateCustomize(actor.Model, customize); private bool ChangeCustomizeDetour(Human* human, byte* data, byte skipEquipment) { if (!InUpdate.InMethod) { - var customize = new Ref(new Customize(*(CustomizeData*)data)); + var customize = new Ref(*(CustomizeArray*)data); Invoke(this, (Model)human, customize); - ((Customize*)data)->Load(customize.Value); + *(CustomizeArray*)data = customize.Value; } + return _changeCustomizeHook.Original(human, data, skipEquipment); } } diff --git a/Glamourer/Interop/CharaFile/CharaFile.cs b/Glamourer/Interop/CharaFile/CharaFile.cs index 6a58809..9ddcc03 100644 --- a/Glamourer/Interop/CharaFile/CharaFile.cs +++ b/Glamourer/Interop/CharaFile/CharaFile.cs @@ -1,8 +1,6 @@ using System; -using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Services; -using Glamourer.Structs; using Newtonsoft.Json.Linq; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -70,7 +68,7 @@ public sealed class CharaFile return; data.SetItem(slot, item); - data.SetStain(slot, (StainId)dye); + data.SetStain(slot, dye); flags |= slot.ToFlag(); flags |= slot.ToStainFlag(); } @@ -94,7 +92,7 @@ public sealed class CharaFile flags |= slot.ToStainFlag(); } - private static CustomizeFlag ParseCustomize(JObject jObj, ref Customize customize) + private static CustomizeFlag ParseCustomize(JObject jObj, ref CustomizeArray customize) { CustomizeFlag ret = 0; customize.Race = ParseRace(jObj, ref ret); @@ -149,7 +147,7 @@ public sealed class CharaFile return id; } - private static void ParseFacial(JObject jObj, ref Customize customize, ref CustomizeFlag application) + private static void ParseFacial(JObject jObj, ref CustomizeArray customize, ref CustomizeFlag application) { var jTok = jObj["FacialFeatures"]; if (jTok == null) @@ -186,7 +184,7 @@ public sealed class CharaFile customize[CustomizeIndex.LegacyTattoo] = CustomizeValue.Max; } - private static void ParseHighlights(JObject jObj, ref Customize customize, ref CustomizeFlag application) + private static void ParseHighlights(JObject jObj, ref CustomizeArray customize, ref CustomizeFlag application) { var jTok = jObj["EnableHighlights"]; if (jTok == null) @@ -242,15 +240,15 @@ public sealed class CharaFile throw new Exception($"Age {age} != Normal is not supported."); } - private static unsafe void ParseByte(JObject jObj, string property, CustomizeIndex idx, ref Customize customize, + 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.Data[idx.ToByteAndMask().ByteIdx] = jTok.ToObject(); - application |= idx.ToFlag(); + customize.Data[idx.ToByteAndMask().ByteIdx] = jTok.ToObject(); + application |= idx.ToFlag(); } private static SubRace ParseTribe(JObject jObj, ref CustomizeFlag application) diff --git a/Glamourer/Interop/CharaFile/CmaFile.cs b/Glamourer/Interop/CharaFile/CmaFile.cs index 15b8af1..e525e7e 100644 --- a/Glamourer/Interop/CharaFile/CmaFile.cs +++ b/Glamourer/Interop/CharaFile/CmaFile.cs @@ -42,7 +42,7 @@ public sealed class CmaFile var byteData = Convert.FromHexString(bytes); fixed (byte* ptr = byteData) { - data.Customize.Data.Read(ptr); + data.Customize.Read(ptr); } } @@ -64,7 +64,7 @@ public sealed class CmaFile data.SetStain(slot, armor.Stain); } - data.Customize.Data.Read(ptr); + data.Customize.Read(ptr); } } @@ -78,8 +78,8 @@ public sealed class CmaFile return; } - var set = mainhand["Item1"]?.ToObject() ?? items.DefaultSword.ModelId; - var type = mainhand["Item2"]?.ToObject() ?? items.DefaultSword.WeaponType; + 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); @@ -95,17 +95,17 @@ public sealed class CmaFile if (offhand == null) { data.SetItem(EquipSlot.MainHand, defaultOffhand); - data.SetStain(EquipSlot.MainHand, defaultOffhand.ModelId.Id == 0 ? 0 : data.Stain(EquipSlot.MainHand)); + data.SetStain(EquipSlot.MainHand, defaultOffhand.PrimaryId.Id == 0 ? 0 : data.Stain(EquipSlot.MainHand)); return; } - var set = offhand["Item1"]?.ToObject() ?? items.DefaultSword.ModelId; - var type = offhand["Item2"]?.ToObject() ?? items.DefaultSword.WeaponType; + 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.ModelId.Id == 0 ? 0 : (StainId)stain); + data.SetStain(EquipSlot.OffHand, defaultOffhand.PrimaryId.Id == 0 ? 0 : (StainId)stain); } } diff --git a/Glamourer/Interop/ContextMenuService.cs b/Glamourer/Interop/ContextMenuService.cs index c210f1f..96dc107 100644 --- a/Glamourer/Interop/ContextMenuService.cs +++ b/Glamourer/Interop/ContextMenuService.cs @@ -68,7 +68,7 @@ public class ContextMenuService : IDisposable if (itemId > 500000) itemId -= 500000; - if (!_items.ItemService.AwaitedService.TryGetValue(itemId, EquipSlot.MainHand, out var item)) + if (!_items.ItemData.TryGetValue(itemId, EquipSlot.MainHand, out var item)) return null; return new InventoryContextMenuItem(TryOnString, GetInventoryAction(item)); @@ -80,7 +80,7 @@ public class ContextMenuService : IDisposable if (itemId > 500000) itemId -= 500000; - if (!_items.ItemService.AwaitedService.TryGetValue(itemId, EquipSlot.MainHand, out var item)) + if (!_items.ItemData.TryGetValue(itemId, EquipSlot.MainHand, out var item)) return null; return new GameObjectContextMenuItem(TryOnString, GetGameObjectAction(item)); @@ -122,10 +122,10 @@ public class ContextMenuService : IDisposable _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)) + if (item.PrimaryId.Id is > 1600 and < 1651 + && _items.ItemData.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)) + if (_items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) _state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, StateChanged.Source.Manual); } }; @@ -146,10 +146,10 @@ public class ContextMenuService : IDisposable _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)) + if (item.PrimaryId.Id is > 1600 and < 1651 + && _items.ItemData.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)) + if (_items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand)) _state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, StateChanged.Source.Manual); } }; diff --git a/Glamourer/Interop/CrestService.cs b/Glamourer/Interop/CrestService.cs index 9285ec6..7db9a59 100644 --- a/Glamourer/Interop/CrestService.cs +++ b/Glamourer/Interop/CrestService.cs @@ -1,12 +1,10 @@ using System; -using System.Linq; using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Interop.Structs; -using Glamourer.Structs; using OtterGui.Classes; using Penumbra.GameData.Enums; @@ -79,7 +77,7 @@ public sealed unsafe class CrestService : EventWrapper _dragDropManager.CreateImGuiSource("DatDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".dat"), m => @@ -139,7 +140,7 @@ public class ImportService(CustomizationService _customizations, IDragDropManage return true; } - public bool SaveDesignAsDat(string path, in Customize input, string description) + public bool SaveDesignAsDat(string path, in CustomizeArray input, string description) { if (!Verify(input, out var voice)) return false; @@ -168,7 +169,7 @@ public class ImportService(CustomizationService _customizations, IDragDropManage } } - public bool Verify(in Customize input, out byte voice, byte? inputVoice = null) + 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) @@ -178,14 +179,14 @@ public class ImportService(CustomizationService _customizations, IDragDropManage if (input.BodyType.Value != 1) return false; - var set = _customizations.AwaitedService.GetList(input.Clan, input.Gender); + 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 (!CustomizationService.IsCustomizationValid(set, input.Face, index, input[index])) + if (!CustomizeService.IsCustomizationValid(set, input.Face, index, input[index])) return false; } diff --git a/Glamourer/Interop/JobService.cs b/Glamourer/Interop/JobService.cs index 2fe322f..5ee9cf6 100644 --- a/Glamourer/Interop/JobService.cs +++ b/Glamourer/Interop/JobService.cs @@ -6,7 +6,9 @@ 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.Structs; namespace Glamourer.Interop; @@ -14,19 +16,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, IDataManager gameData, 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/ObjectManager.cs b/Glamourer/Interop/ObjectManager.cs index 52dd397..5db7400 100644 --- a/Glamourer/Interop/ObjectManager.cs +++ b/Glamourer/Interop/ObjectManager.cs @@ -5,8 +5,8 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Control; using Glamourer.Interop.Structs; -using Glamourer.Services; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; namespace Glamourer.Interop; @@ -15,13 +15,13 @@ public class ObjectManager : IReadOnlyDictionary private readonly IFramework _framework; private readonly IClientState _clientState; private readonly IObjectTable _objects; - private readonly ActorService _actors; + private readonly ActorManager _actors; private readonly ITargetManager _targets; public IObjectTable Objects => _objects; - public ObjectManager(IFramework framework, IClientState clientState, IObjectTable objects, ActorService actors, ITargetManager targets) + public ObjectManager(IFramework framework, IClientState clientState, IObjectTable objects, ActorManager actors, ITargetManager targets) { _framework = framework; _clientState = clientState; @@ -57,7 +57,7 @@ public class ObjectManager : IReadOnlyDictionary for (var i = 0; i < (int)ScreenActor.CutsceneStart; ++i) { Actor character = _objects.GetObjectAddress(i); - if (character.Identifier(_actors.AwaitedService, out var identifier)) + if (character.Identifier(_actors, out var identifier)) HandleIdentifier(identifier, character); } @@ -70,13 +70,13 @@ public class ObjectManager : IReadOnlyDictionary if (!character.Valid && i == (int)ScreenActor.CutsceneStart) break; - HandleIdentifier(character.GetIdentifier(_actors.AwaitedService), character); + HandleIdentifier(character.GetIdentifier(_actors), character); } void AddSpecial(ScreenActor idx, string label) { Actor actor = _objects.GetObjectAddress((int)idx); - if (actor.Identifier(_actors.AwaitedService, out var ident)) + if (actor.Identifier(_actors, out var ident)) { var data = new ActorData(actor, label); _identifiers.Add(ident, data); @@ -95,7 +95,7 @@ public class ObjectManager : IReadOnlyDictionary for (var i = (int)ScreenActor.ScreenEnd; i < _objects.Length; ++i) { Actor character = _objects.GetObjectAddress(i); - if (character.Identifier(_actors.AwaitedService, out var identifier)) + if (character.Identifier(_actors, out var identifier)) HandleIdentifier(identifier, character); } @@ -120,7 +120,7 @@ public class ObjectManager : IReadOnlyDictionary if (identifier.Type is IdentifierType.Player or IdentifierType.Owned) { - var allWorld = _actors.AwaitedService.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue, + var allWorld = _actors.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId); @@ -137,7 +137,7 @@ public class ObjectManager : IReadOnlyDictionary if (identifier.Type is IdentifierType.Owned) { - var nonOwned = _actors.AwaitedService.CreateNpc(identifier.Kind, identifier.DataId); + var nonOwned = _actors.CreateNpc(identifier.Kind, identifier.DataId); if (!_nonOwnedIdentifiers.TryGetValue(nonOwned, out var nonOwnedData)) { nonOwnedData = new ActorData(character, nonOwned.ToString()); @@ -170,7 +170,7 @@ public class ObjectManager : IReadOnlyDictionary get { Update(); - return Player.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data) + return Player.Identifier(_actors, out var ident) && _identifiers.TryGetValue(ident, out var data) ? (ident, data) : (ident, ActorData.Invalid); } @@ -181,7 +181,7 @@ public class ObjectManager : IReadOnlyDictionary get { Update(); - return Target.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data) + return Target.Identifier(_actors, out var ident) && _identifiers.TryGetValue(ident, out var data) ? (ident, data) : (ident, ActorData.Invalid); } diff --git a/Glamourer/Interop/Penumbra/PenumbraService.cs b/Glamourer/Interop/Penumbra/PenumbraService.cs index 4515b7c..2629ad7 100644 --- a/Glamourer/Interop/Penumbra/PenumbraService.cs +++ b/Glamourer/Interop/Penumbra/PenumbraService.cs @@ -221,8 +221,8 @@ public unsafe class PenumbraService : IDisposable => Available ? _drawObjectInfo.Invoke(drawObject.Address).Item1 : 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) (Available ? _cutsceneParent.Invoke(idx) : -1); /// Try to redraw the given actor. public void RedrawObject(Actor actor, RedrawType settings) diff --git a/Glamourer/Interop/Structs/Actor.cs b/Glamourer/Interop/Structs/Actor.cs index 750527b..78dd9fa 100644 --- a/Glamourer/Interop/Structs/Actor.cs +++ b/Glamourer/Interop/Structs/Actor.cs @@ -2,8 +2,6 @@ using System; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Glamourer.Customization; -using Glamourer.Structs; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.String; @@ -116,8 +114,8 @@ public readonly unsafe struct Actor : IEquatable public CharacterWeapon GetOffhand() => new(AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).ModelId.Value); - public Customize GetCustomize() - => *(Customize*)&AsCharacter->DrawData.CustomizeData; + public CustomizeArray GetCustomize() + => *(CustomizeArray*)&AsCharacter->DrawData.CustomizeData; // TODO remove this when available in ClientStructs internal ref CrestFlag CrestBitfield @@ -125,4 +123,21 @@ public readonly unsafe struct Actor : IEquatable public override string ToString() => $"0x{Address:X}"; + + public OnlineStatus OnlineStatus + => (OnlineStatus)AsCharacter->CharacterData.OnlineStatus; +} + +public enum OnlineStatus : byte +{ + Normal = 0x00, + Mentor = 0x1B, + PvEMentor = 0x1C, + TradeMentor = 0x1D, + PvPMentor = 0x1E, + Busy = 0x0C, + Away = 0x11, + MeldMateria = 0x15, + RolePlaying = 0x16, + LookingForGroup = 0x17, } diff --git a/Glamourer/Interop/Structs/Model.cs b/Glamourer/Interop/Structs/Model.cs index 77bf24e..9705248 100644 --- a/Glamourer/Interop/Structs/Model.cs +++ b/Glamourer/Interop/Structs/Model.cs @@ -1,7 +1,6 @@ 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; @@ -91,8 +90,8 @@ public readonly unsafe struct Model : IEquatable public CharacterArmor GetArmor(EquipSlot slot) => ((CharacterArmor*)&AsHuman->Head)[slot.ToIndex()]; - public Customize GetCustomize() - => *(Customize*)&AsHuman->Customize; + public CustomizeArray GetCustomize() + => *(CustomizeArray*)&AsHuman->Customize; public (Model Address, CharacterWeapon Data) GetMainhand() { diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index f5a0ec0..31472ff 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -4,6 +4,7 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using Glamourer.Events; using Glamourer.Interop.Structs; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; diff --git a/Glamourer/Interop/VisorService.cs b/Glamourer/Interop/VisorService.cs index 2b49eba..75c5e36 100644 --- a/Glamourer/Interop/VisorService.cs +++ b/Glamourer/Interop/VisorService.cs @@ -18,7 +18,6 @@ public class VisorService : IDisposable { Event = visorStateChanged; _setupVisorHook = interop.HookFromAddress((nint)Human.MemberFunctionPointers.SetupVisor, SetupVisorDetour); - interop.InitializeFromAttributes(this); _setupVisorHook.Enable(); } diff --git a/Glamourer/Interop/WeaponService.cs b/Glamourer/Interop/WeaponService.cs index 389a05f..4c87430 100644 --- a/Glamourer/Interop/WeaponService.cs +++ b/Glamourer/Interop/WeaponService.cs @@ -5,7 +5,6 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using Glamourer.Events; using Glamourer.Interop.Structs; -using Glamourer.Structs; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -14,18 +13,15 @@ namespace Glamourer.Interop; public unsafe class WeaponService : IDisposable { private readonly WeaponLoading _event; - private readonly CrestService _crestService; private readonly ThreadLocal _inUpdate = new(() => false); private readonly delegate* unmanaged[Stdcall] _original; - - public WeaponService(WeaponLoading @event, IGameInteropProvider interop, CrestService crestService) + public WeaponService(WeaponLoading @event, IGameInteropProvider interop) { - _event = @event; - _crestService = crestService; + _event = @event; _loadWeaponHook = interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour); _original = @@ -73,10 +69,10 @@ public unsafe class WeaponService : IDisposable _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) + if (tmpWeapon.Skeleton.Id == 0) tmpWeapon.Stain = 0; _loadWeaponHook.Original(drawData, slot, tmpWeapon.Value, 1, unk2, 1, unk4); } @@ -119,7 +115,7 @@ public unsafe class WeaponService : IDisposable 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 ? 0 : stain); LoadWeapon(character, slot, weapon); } } diff --git a/Glamourer/Services/CodeService.cs b/Glamourer/Services/CodeService.cs index f065e50..7bc5f04 100644 --- a/Glamourer/Services/CodeService.cs +++ b/Glamourer/Services/CodeService.cs @@ -12,23 +12,67 @@ 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, } - 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 const CodeFlag DyeCodes = CodeFlag.Clown | CodeFlag.World | CodeFlag.Elephants; + public const CodeFlag GearCodes = CodeFlag.Emperor | CodeFlag.World | CodeFlag.Elephants; + + public const CodeFlag RaceCodes = CodeFlag.OopsHyur + | CodeFlag.OopsElezen + | CodeFlag.OopsLalafell + | CodeFlag.OopsMiqote + | CodeFlag.OopsRoegadyn + | CodeFlag.OopsAuRa + | CodeFlag.OopsHrothgar; + + public const CodeFlag SizeCodes = CodeFlag.Dwarf | CodeFlag.Giant; + + private CodeFlag _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) { @@ -69,92 +113,96 @@ public class CodeService } public Action? CheckCode(string name) + { + var flag = GetCode(name); + if (flag == 0) + return null; + + var badFlags = ~GetMutuallyExclusive(flag); + return v => _enabled = v ? (_enabled | flag) & badFlags : _enabled & ~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 - { - _ 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, - }; - // @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 + private static CodeFlag GetMutuallyExclusive(CodeFlag flag) + => flag switch + { + CodeFlag.Clown => DyeCodes & ~CodeFlag.Clown, + CodeFlag.Emperor => GearCodes & ~CodeFlag.Emperor, + CodeFlag.Individual => 0, + CodeFlag.Dwarf => SizeCodes & ~CodeFlag.Dwarf, + CodeFlag.Giant => SizeCodes & ~CodeFlag.Giant, + CodeFlag.OopsHyur => RaceCodes & ~CodeFlag.OopsHyur, + CodeFlag.OopsElezen => RaceCodes & ~CodeFlag.OopsElezen, + CodeFlag.OopsLalafell => RaceCodes & ~CodeFlag.OopsLalafell, + CodeFlag.OopsMiqote => RaceCodes & ~CodeFlag.OopsMiqote, + CodeFlag.OopsRoegadyn => RaceCodes & ~CodeFlag.OopsRoegadyn, + CodeFlag.OopsAuRa => RaceCodes & ~CodeFlag.OopsAuRa, + CodeFlag.OopsHrothgar => RaceCodes & ~CodeFlag.OopsHrothgar, + CodeFlag.OopsViera => RaceCodes & ~CodeFlag.OopsViera, + CodeFlag.Artisan => 0, + CodeFlag.SixtyThree => 0, + CodeFlag.Shirts => 0, + CodeFlag.World => (DyeCodes | GearCodes) & ~CodeFlag.World, + CodeFlag.Elephants => (DyeCodes | GearCodes) & ~CodeFlag.Elephants, + CodeFlag.Crown => 0, + _ => 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.Artisan => [ 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 ], + 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 ], + _ => [], + }; } diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index 253442f..ed60243 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -5,17 +5,16 @@ 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.Gui; using Glamourer.Interop; using Glamourer.State; -using Glamourer.Structs; using ImGuiNET; using OtterGui; using OtterGui.Classes; using Penumbra.GameData.Actors; +using Penumbra.GameData.Enums; namespace Glamourer.Services; @@ -27,7 +26,7 @@ public class CommandService : IDisposable private readonly ICommandManager _commands; private readonly MainWindow _mainWindow; private readonly IChatGui _chat; - private readonly ActorService _actors; + private readonly ActorManager _actors; private readonly ObjectManager _objects; private readonly StateManager _stateManager; private readonly AutoDesignApplier _autoDesignApplier; @@ -37,7 +36,7 @@ public class CommandService : IDisposable private readonly DesignFileSystem _designFileSystem; private readonly Configuration _config; - public CommandService(ICommandManager commands, MainWindow mainWindow, IChatGui chat, ActorService actors, ObjectManager objects, + public CommandService(ICommandManager commands, MainWindow mainWindow, IChatGui chat, ActorManager actors, ObjectManager objects, AutoDesignApplier autoDesignApplier, StateManager stateManager, DesignManager designManager, DesignConverter converter, DesignFileSystem designFileSystem, AutoDesignManager autoDesignManager, Configuration config) { @@ -86,7 +85,8 @@ public class CommandService : IDisposable 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().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; @@ -114,6 +114,7 @@ public class CommandService : IDisposable "automation" => SetAutomation(argument), "copy" => CopyState(argument), "save" => SaveState(argument), + "delete" => Delete(argument), _ => PrintHelp(argumentList[0]), }; } @@ -402,7 +403,7 @@ public class CommandService : IDisposable { foreach (var actor in actors.Objects) { - if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors.AwaitedService), actor, out var state)) + if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors), actor, out var state)) _stateManager.ApplyDesign(design, state, StateChanged.Source.Manual); } } @@ -411,6 +412,33 @@ public class CommandService : IDisposable return true; } + 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 (!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) @@ -521,14 +549,13 @@ public class CommandService : IDisposable 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; - } + if (design != null) + return true; + + _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) @@ -537,7 +564,7 @@ public class CommandService : IDisposable { 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) @@ -547,15 +574,15 @@ public class CommandService : IDisposable } 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) @@ -568,7 +595,7 @@ 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}") diff --git a/Glamourer/Services/CustomizationService.cs b/Glamourer/Services/CustomizeService.cs similarity index 59% rename from Glamourer/Services/CustomizationService.cs rename to Glamourer/Services/CustomizeService.cs index 1b3f6e2..0b094f3 100644 --- a/Glamourer/Services/CustomizationService.cs +++ b/Glamourer/Services/CustomizeService.cs @@ -1,27 +1,36 @@ using System.Linq; using System.Runtime.CompilerServices; -using Dalamud.Plugin.Services; -using Glamourer.Customization; -using Penumbra.GameData.Data; +using System.Threading.Tasks; +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); @@ -35,8 +44,18 @@ 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); + => race is Race.Hrothgar ? gender == Gender.Male : 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 +129,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,7 +158,7 @@ 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()}."; @@ -215,7 +199,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 +216,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; @@ -251,12 +235,12 @@ 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; @@ -269,11 +253,11 @@ public sealed class CustomizationService : AsyncServiceWrapper(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/FilenameService.cs b/Glamourer/Services/FilenameService.cs index 2ca573f..7f54f19 100644 --- a/Glamourer/Services/FilenameService.cs +++ b/Glamourer/Services/FilenameService.cs @@ -18,6 +18,7 @@ public class FilenameService public readonly string FavoriteFile; public readonly string DesignColorFile; public readonly string EphemeralConfigFile; + public readonly string NpcAppearanceFile; public FilenameService(DalamudPluginInterface pi) { @@ -32,9 +33,9 @@ public class FilenameService 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"); } - public IEnumerable Designs() { if (!Directory.Exists(DesignDirectory)) diff --git a/Glamourer/Services/IGamePathParser.cs b/Glamourer/Services/IGamePathParser.cs new file mode 100644 index 0000000..28364d1 --- /dev/null +++ b/Glamourer/Services/IGamePathParser.cs @@ -0,0 +1,6 @@ +namespace Glamourer.Services +{ + internal interface IGamePathParser + { + } +} \ No newline at end of file diff --git a/Glamourer/Services/ItemManager.cs b/Glamourer/Services/ItemManager.cs index 745469e..6e73945 100644 --- a/Glamourer/Services/ItemManager.cs +++ b/Glamourer/Services/ItemManager.cs @@ -1,17 +1,17 @@ using System; using System.Linq; using System.Runtime.CompilerServices; -using Dalamud.Plugin; using Dalamud.Plugin.Services; using Lumina.Excel; 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 SmallClothesNpc = "Smallclothes (NPC)"; @@ -19,30 +19,24 @@ public class ItemManager : IDisposable private readonly Configuration _config; - public readonly IdentifierService IdentifierService; + public readonly ObjectIdentification ObjectIdentification; public readonly ExcelSheet ItemSheet; - public readonly StainData Stains; - public readonly ItemService ItemService; + public readonly DictStain Stains; + public readonly ItemData ItemData; 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) { - _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; + DefaultSword = EquipItem.FromMainhand(ItemSheet.GetRow(1601)!); // Weathered Shortsword } public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender) @@ -74,11 +68,12 @@ public class ItemManager : IDisposable if (itemId == SmallclothesId(slot)) return SmallClothesItem(slot); - if (!itemId.IsItem || !ItemService.AwaitedService.TryGetValue(itemId.Item, slot, out var item)) + if (!itemId.IsItem || !ItemData.TryGetValue(itemId.Item, slot, out var item)) return EquipItem.FromId(itemId); if (item.Type.ToSlot() != slot) - return new EquipItem(string.Intern($"Invalid #{itemId}"), itemId, item.IconId, item.ModelId, item.WeaponType, item.Variant, 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; @@ -89,12 +84,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, + return new EquipItem(string.Intern($"Invalid #{itemId}"), itemId, item.IconId, item.PrimaryId, item.SecondaryId, item.Variant, 0, 0, + 0, 0); return item; @@ -103,7 +99,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,7 +110,7 @@ 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()); @@ -131,7 +127,8 @@ public class ItemManager : IDisposable return NothingItem(offhandType); } - public EquipItem Identify(EquipSlot slot, SetId id, WeaponType type, Variant variant, FullEquipType mainhandType = FullEquipType.Unknown) + public EquipItem Identify(EquipSlot slot, PrimaryId id, SecondaryId type, Variant variant, + FullEquipType mainhandType = FullEquipType.Unknown) { if (slot is EquipSlot.OffHand) { @@ -143,7 +140,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()); diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 8dc4c8d..db3922c 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; -using System.Reflection; -using Dalamud.Plugin; +using Dalamud.Plugin; using Glamourer.Api; using Glamourer.Automation; using Glamourer.Designs; @@ -14,6 +11,7 @@ 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.UnlocksTab; using Glamourer.Interop; using Glamourer.Interop.Penumbra; @@ -22,18 +20,22 @@ using Glamourer.Unlocks; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; using OtterGui.Log; +using OtterGui.Services; +using Penumbra.GameData.Actors; using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Glamourer.Services; -public static class ServiceManager +public static class ServiceManagerA { - public static ServiceProvider CreateProvider(DalamudPluginInterface pi, Logger log) + public static ServiceManager CreateProvider(DalamudPluginInterface pi, Logger log) { EventWrapper.ChangeLogger(log); - var services = new ServiceCollection() - .AddSingleton(log) - .AddDalamud(pi) + var services = new ServiceManager(log) + .AddExistingService(log) .AddMeta() .AddInterop() .AddEvents() @@ -41,19 +43,16 @@ public static class ServiceManager .AddDesigns() .AddState() .AddUi() - .AddApi() - .AddDebug(); - - return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); - } - - private static IServiceCollection AddDalamud(this IServiceCollection services, DalamudPluginInterface pi) - { - new DalamudServices(pi).AddServices(services); + .AddApi(); + DalamudServices.AddServices(services, pi); + services.AddIServices(typeof(EquipItem).Assembly); + services.AddIServices(typeof(Glamourer).Assembly); + services.AddIServices(typeof(EquipFlag).Assembly); + services.CreateProvider(); return services; } - private static IServiceCollection AddMeta(this IServiceCollection services) + private static ServiceManager AddMeta(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton() @@ -66,7 +65,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton(); - private static IServiceCollection AddEvents(this IServiceCollection services) + private static ServiceManager AddEvents(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton() @@ -82,21 +81,23 @@ public static class ServiceManager .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(p => new CutsceneResolver(p.GetRequiredService().CutsceneParent)) .AddSingleton() .AddSingleton() .AddSingleton() @@ -108,7 +109,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton(); - private static IServiceCollection AddDesigns(this IServiceCollection services) + private static ServiceManager AddDesigns(this ServiceManager services) => services.AddSingleton() .AddSingleton() .AddSingleton() @@ -117,20 +118,24 @@ public static class ServiceManager .AddSingleton() .AddSingleton(); - private static IServiceCollection AddState(this IServiceCollection services) + private static ServiceManager AddState(this ServiceManager services) => services.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() @@ -157,16 +162,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton(); - private static IServiceCollection AddDebug(this IServiceCollection services) - { - services.AddSingleton(p => new DebugTab(p)); - var iType = typeof(IDebugTabTree); - foreach (var type in Assembly.GetAssembly(iType)!.GetTypes().Where(t => !t.IsInterface && iType.IsAssignableFrom(t))) - services.AddSingleton(type); - return services; - } - - private static IServiceCollection AddApi(this IServiceCollection services) + private static ServiceManager AddApi(this ServiceManager services) => services.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/State/ActorState.cs b/Glamourer/State/ActorState.cs index 3cd7cba..484d0a9 100644 --- a/Glamourer/State/ActorState.cs +++ b/Glamourer/State/ActorState.cs @@ -1,13 +1,11 @@ -using Glamourer.Customization; -using Glamourer.Designs; +using Glamourer.Designs; using Glamourer.Events; -using Glamourer.Structs; 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 Penumbra.GameData.Structs; namespace Glamourer.State; @@ -34,7 +32,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; diff --git a/Glamourer/State/FunModule.cs b/Glamourer/State/FunModule.cs index b570d45..43f8f07 100644 --- a/Glamourer/State/FunModule.cs +++ b/Glamourer/State/FunModule.cs @@ -2,17 +2,17 @@ using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Internal.Notifications; -using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Gui; using Glamourer.Interop; using Glamourer.Interop.Structs; using Glamourer.Services; using ImGuiNET; +using OtterGui; using OtterGui.Classes; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using CustomizeIndex = Glamourer.Customization.CustomizeIndex; +using CustomizeIndex = Penumbra.GameData.Enums.CustomizeIndex; namespace Glamourer.State; @@ -26,18 +26,18 @@ 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 ObjectManager _objects; + private readonly StainId[] _stains; public FestivalType CurrentFestival { get; private set; } = FestivalType.None; private FunEquipSet? _festivalSet; @@ -68,7 +68,7 @@ public unsafe class FunModule : IDisposable internal void ResetFestival() => OnDayChange(DateTime.Now.Day, DateTime.Now.Month, DateTime.Now.Year); - public FunModule(CodeService codes, CustomizationService customizations, ItemManager items, Configuration config, + public FunModule(CodeService codes, CustomizeService customizations, ItemManager items, Configuration config, GenericPopupWindow popupWindow, StateManager stateManager, ObjectManager objects, DesignConverter designConverter, DesignManager designManager) { @@ -90,54 +90,107 @@ 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 (!ValidFunTarget(actor)) return; - if (_config.DisableFestivals == 0 && _festivalSet != null - || _codes.EnabledWorld && actor.Index != 0) + if (IsInFestival) { - armor = actor.Model.Valid ? actor.Model.GetArmor(slot) : armor; + KeepOldArmor(actor, slot, ref armor); + return; } - else + + 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, 0); + return; + } + + switch (_codes.Masked(CodeService.GearCodes)) + { + case CodeService.CodeFlag.Emperor: + SetRandomItem(slot, ref armor); + break; + case CodeService.CodeFlag.Elephants: + SetElephant(slot, ref armor); + break; + 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) + public void ApplyFunOnLoad(Actor actor, Span armor, ref CustomizeArray customize) { if (!ValidFunTarget(actor)) return; - if (_config.DisableFestivals == 0 && _festivalSet != null) + // 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); - } - else if (_codes.EnabledWorld && actor.Index != 0) - { - _worldSets.Apply(actor, _rng, armor); - } - else - { - ApplyEmperor(armor); - ApplyClown(armor); + _festivalSet!.Apply(_stains, _rng, armor); + return; } - ApplyOops(ref customize); - Apply63(ref customize); - ApplyIndividual(ref customize); - ApplySizing(actor, ref customize); + 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: + SetElephant(EquipSlot.Body, ref armor[1]); + SetElephant(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 ApplyFun(Actor actor, ref CharacterWeapon weapon, EquipSlot slot) + public void ApplyFunToWeapon(Actor actor, ref CharacterWeapon weapon, EquipSlot slot) { if (!ValidFunTarget(actor)) return; - if (_codes.EnabledWorld) + if (_codes.Enabled(CodeService.CodeFlag.World) && actor.Index != 0) _worldSets.Apply(actor, _rng, ref weapon, slot); } @@ -147,57 +200,71 @@ public unsafe class FunModule : IDisposable && !actor.IsTransformed && actor.AsCharacter->CharacterData.ModelCharaId == 0; - public void ApplyClown(Span armors) - { - if (!_codes.EnabledClown) - return; + private static void KeepOldArmor(Actor actor, EquipSlot slot, ref CharacterArmor armor) + => armor = actor.Model.Valid ? actor.Model.GetArmor(slot) : armor; - foreach (ref var armor in armors) - { - var stainIdx = _rng.Next(0, _stains.Length - 1); - armor.Stain = _stains[stainIdx]; - } + private void SetRandomDye(ref CharacterArmor armor) + { + var stainIdx = _rng.Next(0, _stains.Length - 1); + armor.Stain = _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 - 1); + var item = list[rng]; + armor.Set = item.PrimaryId; + armor.Variant = item.Variant; } - public void ApplyOops(ref Customize customize) + private static void SetElephant(EquipSlot slot, ref CharacterArmor armor) { - if (_codes.EnabledOops == Race.Unknown) + armor = slot switch + { + EquipSlot.Body => new CharacterArmor(6133, 1, 87), + EquipSlot.Head => new CharacterArmor(6133, 1, 87), + _ => armor, + }; + } + + private static void SetCrown(Span armor) + { + var clown = new CharacterArmor(6117, 1, 0); + 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); + var targetClan = (SubRace)((int)race * 2 - (int)customize.Clan % 2); // TODO Female Hrothgar - if (_codes.EnabledOops is Race.Hrothgar && customize.Gender is Gender.Female) + if (race is Race.Hrothgar && customize.Gender is Gender.Female) targetClan = targetClan is SubRace.Lost ? SubRace.Seawolf : SubRace.Hellsguard; _customizations.ChangeClan(ref customize, targetClan); } - public void ApplyIndividual(ref Customize customize) + private void SetGender(ref CustomizeArray customize) { - if (!_codes.EnabledIndividual) + if (!_codes.Enabled(CodeService.CodeFlag.SixtyThree) || customize.Race is Race.Hrothgar) // TODO Female Hrothgar return; - var set = _customizations.AwaitedService.GetList(customize.Clan, customize.Gender); + _customizations.ChangeGender(ref customize, customize.Gender is Gender.Male ? Gender.Female : Gender.Male); + } + + private void RandomizeCustomize(ref CustomizeArray customize) + { + if (!_codes.Enabled(CodeService.CodeFlag.Individual)) + return; + + var set = _customizations.Manager.GetSet(customize.Clan, customize.Gender); foreach (var index in Enum.GetValues()) { if (index is CustomizeIndex.Face || !set.IsAvailable(index)) @@ -208,27 +275,18 @@ public unsafe class FunModule : IDisposable } } - public void Apply63(ref Customize customize) + private void SetSize(Actor actor, ref CustomizeArray customize) { - if (!_codes.Enabled63 || customize.Race is Race.Hrothgar) // TODO Female Hrothgar - return; - - _customizations.ChangeGender(ref customize, customize.Gender is Gender.Male ? Gender.Female : Gender.Male); - } - - public void ApplySizing(Actor actor, ref Customize customize) - { - if (_codes.EnabledSizing == CodeService.Sizing.None) - return; - - var size = _codes.EnabledSizing switch + var size = _codes.Masked(CodeService.SizeCodes) switch { - 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, + 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; diff --git a/Glamourer/State/StateApplier.cs b/Glamourer/State/StateApplier.cs index 5eaf672..43c9097 100644 --- a/Glamourer/State/StateApplier.cs +++ b/Glamourer/State/StateApplier.cs @@ -1,11 +1,9 @@ using System.Linq; -using Glamourer.Customization; using Glamourer.Events; using Glamourer.Interop; using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; -using Glamourer.Structs; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -40,7 +38,7 @@ public class StateApplier(UpdateSlotService _updateSlot, VisorService _visor, We /// 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) { @@ -48,15 +46,15 @@ public class StateApplier(UpdateSlotService _updateSlot, VisorService _visor, We 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 @@ -66,7 +64,7 @@ public class StateApplier(UpdateSlotService _updateSlot, VisorService _visor, We } } - /// + /// public ActorData ChangeCustomize(ActorState state, bool apply) { var data = GetData(state); @@ -188,7 +186,7 @@ public class StateApplier(UpdateSlotService _updateSlot, VisorService _visor, We /// Apply a weapon to the offhand. public void ChangeOffhand(ActorData data, EquipItem weapon, StainId stain) { - stain = weapon.ModelId.Id == 0 ? 0 : stain; + stain = weapon.PrimaryId.Id == 0 ? 0 : stain; foreach (var actor in data.Objects.Where(a => a.Model.IsHuman)) _weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon().With(stain)); } diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index 3b9d0cb..0095fe9 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -1,11 +1,9 @@ using System; using System.Linq; using Dalamud.Plugin.Services; -using Glamourer.Customization; using Glamourer.Events; using Glamourer.Services; -using Glamourer.Structs; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -14,12 +12,12 @@ namespace Glamourer.State; public class StateEditor { private readonly ItemManager _items; - private readonly CustomizationService _customizations; + private readonly CustomizeService _customizations; private readonly HumanModelList _humans; private readonly GPoseService _gPose; private readonly ICondition _condition; - public StateEditor(CustomizationService customizations, HumanModelList humans, ItemManager items, GPoseService gPose, ICondition condition) + public StateEditor(CustomizeService customizations, HumanModelList humans, ItemManager items, GPoseService gPose, ICondition condition) { _customizations = customizations; _humans = humans; @@ -30,7 +28,7 @@ public class StateEditor /// 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, + public bool ChangeModelId(ActorState state, uint modelId, in CustomizeArray customize, nint equipData, StateChanged.Source source, out uint oldModelId, uint key = 0) { oldModelId = state.ModelData.ModelId; @@ -57,7 +55,7 @@ public class StateEditor return false; // Fix up everything else to make sure the result is a valid human. - state.ModelData.Customize = Customize.Default; + state.ModelData.Customize = CustomizeArray.Default; state.ModelData.SetDefaultEquipment(_items); state.ModelData.SetHatVisible(true); state.ModelData.SetWeaponVisible(true); @@ -74,7 +72,7 @@ public class StateEditor state[CustomizeIndex.Clan] = source; state[CustomizeIndex.Gender] = source; - var set = _customizations.AwaitedService.GetList(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender); + var set = _customizations.Manager.GetSet(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender); foreach (var index in Enum.GetValues().Where(set.IsAvailable)) state[index] = source; } @@ -104,8 +102,8 @@ public class StateEditor } /// 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) + public bool ChangeHumanCustomize(ActorState state, in CustomizeArray customizeInput, CustomizeFlag applyWhich, StateChanged.Source source, + out CustomizeArray old, out CustomizeFlag changed, uint key = 0) { old = state.ModelData.Customize; changed = 0; diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index ae6de2f..bbc519f 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -1,5 +1,4 @@ using Glamourer.Automation; -using Glamourer.Customization; using Glamourer.Events; using Glamourer.Interop; using Glamourer.Interop.Penumbra; @@ -7,13 +6,12 @@ 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 Glamourer.Structs; +using Penumbra.GameData.DataContainers; namespace Glamourer.State; @@ -25,12 +23,12 @@ namespace Glamourer.State; public class StateListener : IDisposable { private readonly Configuration _config; - private readonly ActorService _actors; + private readonly ActorManager _actors; private readonly ObjectManager _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 WeaponLoading _weaponLoading; @@ -50,11 +48,11 @@ public class StateListener : IDisposable private ActorState? _creatingState; private CharacterWeapon _lastFistOffhand = CharacterWeapon.Empty; - public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorService actors, Configuration config, + public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorManager 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, CrestService crestService) + ChangeCustomizeService changeCustomizeService, CustomizeService customizations, ICondition condition, CrestService crestService) { _manager = manager; _items = items; @@ -111,10 +109,10 @@ public class StateListener : IDisposable 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)) { switch (UpdateBaseData(actor, _creatingState, modelId, customizePtr, equipDataPtr)) @@ -135,12 +133,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 unsafe void OnCustomizeChange(Model model, Ref customize) { if (!model.IsHuman) return; @@ -149,14 +147,14 @@ public class StateListener : IDisposable if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) return; - if (!actor.Identifier(_actors.AwaitedService, out var identifier) + if (!actor.Identifier(_actors, out var identifier) || !_manager.TryGetValue(identifier, out var state)) return; UpdateCustomize(actor, state, ref customize.Value, 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)) { @@ -169,7 +167,7 @@ public class StateListener : IDisposable 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) @@ -211,14 +209,14 @@ 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; } - _funModule.ApplyFun(actor, ref armor.Value, slot); + _funModule.ApplyFunToSlot(actor, ref armor.Value, slot); if (!_config.UseRestrictedGearProtection || locked) return; @@ -238,7 +236,7 @@ public class StateListener : IDisposable 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)) + if (model.Value == current.Value || !_items.ItemData.TryGetValue(item, EquipSlot.MainHand, out var changedItem)) continue; var changed = changedItem.Weapon(stain); @@ -272,10 +270,10 @@ public class StateListener : IDisposable return; // Fist weapon gauntlet hack. - if (slot is EquipSlot.OffHand && weapon.Value.Variant == 0 && weapon.Value.Set.Id != 0 && _lastFistOffhand.Set.Id != 0) + if (slot is EquipSlot.OffHand && weapon.Value.Variant == 0 && weapon.Value.Weapon.Id != 0 && _lastFistOffhand.Weapon.Id != 0) weapon.Value = _lastFistOffhand; - if (!actor.Identifier(_actors.AwaitedService, out var identifier) + if (!actor.Identifier(_actors, out var identifier) || !_manager.TryGetValue(identifier, out var state)) return; @@ -308,16 +306,16 @@ public class StateListener : IDisposable 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) + else if (actorWeapon.Skeleton.Id != 0) actorWeapon = actorWeapon.With(newWeapon.Stain); } // 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, + if (slot is EquipSlot.MainHand && weapon.Value.Skeleton.Id is > 1600 and < 1651) + _lastFistOffhand = new CharacterWeapon((PrimaryId)(weapon.Value.Skeleton.Id + 50), weapon.Value.Weapon, weapon.Value.Variant, weapon.Value.Stain); - _funModule.ApplyFun(actor, ref weapon.Value, slot); + _funModule.ApplyFunToWeapon(actor, ref weapon.Value, slot); } /// Update base data for a single changed equipment slot. @@ -329,7 +327,7 @@ public class StateListener : IDisposable return false; var offhand = actor.GetOffhand(); - return offhand.Variant == 0 && offhand.Set.Id != 0 && armor.Set.Id == offhand.Set.Id; + return offhand.Variant == 0 && offhand.Weapon.Id != 0 && armor.Set.Id == offhand.Weapon.Id; } var actorArmor = actor.GetArmor(slot); @@ -413,7 +411,7 @@ public class StateListener : IDisposable if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) return; - if (!actor.Identifier(_actors.AwaitedService, out var identifier) + if (!actor.Identifier(_actors, out var identifier) || !_manager.TryGetValue(identifier, out var state)) return; @@ -439,7 +437,7 @@ public class StateListener : IDisposable if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) return; - if (!actor.Identifier(_actors.AwaitedService, out var identifier) + if (!actor.Identifier(_actors, out var identifier) || !_manager.TryGetValue(identifier, out var state)) return; @@ -474,7 +472,7 @@ public class StateListener : IDisposable 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) @@ -483,9 +481,9 @@ public class StateListener : IDisposable 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, + var item = _items.Identify(slot, weapon.Skeleton, weapon.Weapon, weapon.Variant, slot is EquipSlot.OffHand ? state.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown); state.BaseData.SetItem(slot, item); change = UpdateState.Change; @@ -515,7 +513,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; } @@ -526,7 +524,7 @@ 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 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)) @@ -537,7 +535,7 @@ 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; } @@ -555,7 +553,7 @@ public class StateListener : IDisposable 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)) @@ -588,7 +586,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)) @@ -621,7 +619,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)) diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 68c9cfc..dc83305 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -4,25 +4,30 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Plugin.Services; -using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Events; using Glamourer.Interop; using Glamourer.Interop.Structs; using Glamourer.Services; -using Glamourer.Structs; using Penumbra.GameData.Actors; -using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; namespace Glamourer.State; -public class StateManager(ActorService _actors, ItemManager _items, StateChanged _event, StateApplier _applier, StateEditor _editor, - HumanModelList _humans, ICondition _condition, IClientState _clientState) +public class StateManager( + ActorManager _actors, + ItemManager _items, + StateChanged _event, + StateApplier _applier, + StateEditor _editor, + HumanModelList _humans, + ICondition _condition, + IClientState _clientState) : IReadOnlyDictionary { - private readonly Dictionary _states = new(); + private readonly Dictionary _states = []; public IEnumerator> GetEnumerator() => _states.GetEnumerator(); @@ -50,7 +55,7 @@ public class StateManager(ActorService _actors, ItemManager _items, StateChanged /// 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) @@ -102,7 +107,7 @@ public class StateManager(ActorService _actors, ItemManager _items, StateChanged // TODO reverse search model data to get model id from model. if (!_humans.IsHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId)) { - ret.LoadNonHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData, + ret.LoadNonHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId, *(CustomizeArray*)&actor.AsCharacter->DrawData.CustomizeData, (nint)(&actor.AsCharacter->DrawData.Head)); return ret; } @@ -170,8 +175,8 @@ public class StateManager(ActorService _actors, ItemManager _items, StateChanged } // 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.SetItem(EquipSlot.OffHand, offItem); @@ -190,13 +195,13 @@ public class StateManager(ActorService _actors, ItemManager _items, StateChanged /// 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); } @@ -205,10 +210,10 @@ public class StateManager(ActorService _actors, ItemManager _items, StateChanged /// Turn an actor human. public void TurnHuman(ActorState state, StateChanged.Source source, uint key = 0) - => ChangeModelId(state, 0, Customize.Default, nint.Zero, source, key); + => 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, + public void ChangeModelId(ActorState state, uint modelId, CustomizeArray customize, nint equipData, StateChanged.Source source, uint key = 0) { if (!_editor.ChangeModelId(state, modelId, customize, equipData, source, out var old, key)) @@ -233,7 +238,8 @@ public class StateManager(ActorService _actors, ItemManager _items, StateChanged } /// Change an entire customization array according to flags. - public void ChangeCustomize(ActorState state, in Customize customizeInput, CustomizeFlag apply, StateChanged.Source source, uint key = 0) + public void ChangeCustomize(ActorState state, in CustomizeArray customizeInput, CustomizeFlag apply, StateChanged.Source source, + uint key = 0) { if (!_editor.ChangeHumanCustomize(state, customizeInput, apply, source, out var old, out var applied, key)) return; @@ -447,7 +453,7 @@ public class StateManager(ActorService _actors, ItemManager _items, StateChanged 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()) @@ -458,7 +464,7 @@ public class StateManager(ActorService _actors, ItemManager _items, StateChanged state[slot, true] = StateChanged.Source.Game; state[slot, false] = StateChanged.Source.Game; } - + foreach (var type in Enum.GetValues()) state[type] = StateChanged.Source.Game; @@ -470,7 +476,7 @@ public class StateManager(ActorService _actors, ItemManager _items, StateChanged 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); + _event.Invoke(StateChanged.Type.Reset, StateChanged.Source.Manual, state, actors); } public void ResetStateFixed(ActorState state, uint key = 0) @@ -538,7 +544,7 @@ public class StateManager(ActorService _actors, ItemManager _items, StateChanged if (!GetOrCreate(actor, out var state)) return; - ApplyAll(state, !actor.Model.IsHuman || Customize.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), + ApplyAll(state, !actor.Model.IsHuman || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); } diff --git a/Glamourer/State/WorldSets.cs b/Glamourer/State/WorldSets.cs index 3f694ee..4590eec 100644 --- a/Glamourer/State/WorldSets.cs +++ b/Glamourer/State/WorldSets.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Glamourer.Interop.Structs; -using Glamourer.Structs; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -303,12 +302,12 @@ public class WorldSets 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; if (job >= StarterWeapons.Length) return null; diff --git a/Glamourer/Unlocks/CustomizeUnlockManager.cs b/Glamourer/Unlocks/CustomizeUnlockManager.cs index 19a3f6c..2bdbb78 100644 --- a/Glamourer/Unlocks/CustomizeUnlockManager.cs +++ b/Glamourer/Unlocks/CustomizeUnlockManager.cs @@ -8,10 +8,11 @@ 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 Penumbra.GameData.Enums; namespace Glamourer.Unlocks; @@ -28,7 +29,7 @@ public class CustomizeUnlockManager : IDisposable, ISavable public IReadOnlyDictionary Unlocked => _unlocked; - public CustomizeUnlockManager(SaveService saveService, CustomizationService customizations, IDataManager gameData, + public CustomizeUnlockManager(SaveService saveService, CustomizeService customizations, IDataManager gameData, IClientState clientState, ObjectUnlocked @event, IGameInteropProvider interop) { interop.InitializeFromAttributes(this); @@ -172,39 +173,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) + 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.FirstOrDefault(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.FeatureID == 61 + ? "Eternal Bond" + : x.HintItem.Value?.Name.ToDalamudString().ToString().Replace("Modern Aesthetics - ", string.Empty) + ?? string.Empty; + ret.TryAdd(hair, (x.Data, name)); } + } - foreach (var paint in list.FacePaints) + foreach (var paint in list.FacePaints) + { + var x = sheet.FirstOrDefault(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.HintItem.Value?.Name.ToDalamudString().ToString().Replace("Modern Cosmetics - ", string.Empty) + ?? string.Empty; + ret.TryAdd(paint, (x.Data, name)); } } } diff --git a/Glamourer/Unlocks/ItemUnlockManager.cs b/Glamourer/Unlocks/ItemUnlockManager.cs index 972a393..fa7d74a 100644 --- a/Glamourer/Unlocks/ItemUnlockManager.cs +++ b/Glamourer/Unlocks/ItemUnlockManager.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.UI; using Glamourer.Events; using Glamourer.Services; using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Cabinet = Lumina.Excel.GeneratedSheets.Cabinet; @@ -22,7 +23,7 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionary _unlocked = new(); @@ -45,7 +46,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 +101,12 @@ 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); } @@ -282,7 +283,7 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionary()!; foreach (var row in cabinet) { - if (items.ItemService.AwaitedService.TryGetValue(row.Item.Row, EquipSlot.MainHand, out var item)) + if (items.ItemData.TryGetValue(row.Item.Row, EquipSlot.MainHand, out var item)) ret.TryAdd(item.ItemId, new UnlockRequirements(row.RowId, 0, 0, 0, UnlockType.Cabinet)); } @@ -290,7 +291,7 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionary()!; foreach (var row in gilShopItem) { - if (!items.ItemService.AwaitedService.TryGetValue(row.Item.Row, EquipSlot.MainHand, out var item)) + if (!items.ItemData.TryGetValue(row.Item.Row, EquipSlot.MainHand, out var item)) continue; var quest1 = row.QuestRequired[0].Row; @@ -323,10 +324,10 @@ public class ItemUnlockManager : ISavable, IDisposable, IReadOnlyDictionary