diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index 9f3e286..83ce3c4 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -27,7 +27,7 @@ public class AutoDesignApplier : IDisposable private readonly JobService _jobs; private readonly EquippedGearset _equippedGearset; private readonly ActorManager _actors; - private readonly CustomizationService _customizations; + private readonly CustomizeService _customizations; private readonly CustomizeUnlockManager _customizeUnlocks; private readonly ItemUnlockManager _itemUnlocks; private readonly AutomationChanged _event; @@ -48,7 +48,7 @@ public class AutoDesignApplier : IDisposable } public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, - CustomizationService customizations, ActorManager 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) { @@ -468,7 +468,7 @@ public class AutoDesignApplier : IDisposable totalCustomizeFlags |= CustomizeFlag.Face; } - var set = _customizations.Service.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()) { @@ -477,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/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 8e11ad8..7602f80 100644 --- a/Glamourer/Designs/DesignBase.cs +++ b/Glamourer/Designs/DesignBase.cs @@ -25,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); + CustomizeSet = SetCustomizationSet(customize); } internal DesignBase(DesignBase clone) { _designData = clone._designData; - CustomizationSet = clone.CustomizationSet; + 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); + CustomizeSet = SetCustomizationSet(customize); } #region Application Data @@ -69,11 +69,11 @@ public class DesignBase } private CustomizeFlag _applyCustomize = CustomizeFlagExtensions.AllRelevant; - public CustomizationSet CustomizationSet { get; private set; } + public CustomizeSet CustomizeSet { get; private set; } internal CustomizeFlag ApplyCustomize { - get => _applyCustomize.FixApplication(CustomizationSet); + get => _applyCustomize.FixApplication(CustomizeSet); set => _applyCustomize = value & CustomizeFlagExtensions.AllRelevant; } @@ -84,13 +84,13 @@ public class DesignBase internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant; private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible; - public bool SetCustomize(CustomizationService customizationService, CustomizeArray customize) + public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize) { if (customize.Equals(_designData.Customize)) return false; _designData.Customize = customize; - CustomizationSet = customizationService.Service.GetList(customize.Clan, customize.Gender); + CustomizeSet = customizeService.Manager.GetSet(customize.Clan, customize.Gender); return true; } @@ -240,10 +240,10 @@ public class DesignBase } } - private CustomizationSet SetCustomizationSet(CustomizationService customize) + private CustomizeSet SetCustomizationSet(CustomizeService customize) => !_designData.IsHuman - ? customize.Service.GetList(SubRace.Midlander, Gender.Male) - : customize.Service.GetList(_designData.Customize.Clan, _designData.Customize.Gender); + ? customize.Manager.GetSet(SubRace.Midlander, Gender.Male) + : customize.Manager.GetSet(_designData.Customize.Clan, _designData.Customize.Gender); #endregion @@ -330,7 +330,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 @@ -340,7 +340,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); @@ -435,7 +435,7 @@ 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) @@ -473,7 +473,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; } @@ -485,18 +485,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, + PrintWarning(CustomizeService.ValidateCustomizeValue(set, design._designData.Customize.Face, idx, data, out data, allowUnknown)); var apply = tok?["Apply"]?.ToObject() ?? false; design._designData.Customize[idx] = data; @@ -504,7 +504,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 { @@ -518,7 +518,7 @@ public class DesignBase SetApplyVisorToggle(applyVisor); SetApplyWeaponVisible(applyWeapon); SetApplyWetness(true); - CustomizationSet = SetCustomizationSet(customize); + CustomizeSet = SetCustomizationSet(customize); } catch (Exception ex) { diff --git a/Glamourer/Designs/DesignConverter.cs b/Glamourer/Designs/DesignConverter.cs index 282ab0a..f7867c4 100644 --- a/Glamourer/Designs/DesignConverter.cs +++ b/Glamourer/Designs/DesignConverter.cs @@ -13,7 +13,7 @@ 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; diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index 1320d6a..80cd0e0 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -18,7 +18,7 @@ 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; @@ -29,7 +29,7 @@ public class DesignManager 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; diff --git a/Glamourer/GameData/CharaMakeParams.cs b/Glamourer/GameData/CharaMakeParams.cs index 12dedf9..4db5825 100644 --- a/Glamourer/GameData/CharaMakeParams.cs +++ b/Glamourer/GameData/CharaMakeParams.cs @@ -4,9 +4,7 @@ using Lumina.Excel.GeneratedSheets; 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 { diff --git a/Glamourer/GameData/ColorParameters.cs b/Glamourer/GameData/ColorParameters.cs index 630a5a7..975e003 100644 --- a/Glamourer/GameData/ColorParameters.cs +++ b/Glamourer/GameData/ColorParameters.cs @@ -6,10 +6,12 @@ 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); @@ -18,6 +20,7 @@ public class ColorParameters : IReadOnlyList 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) { @@ -32,19 +35,23 @@ public class ColorParameters : IReadOnlyList 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 = Array.Empty(); + _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/CustomName.cs b/Glamourer/GameData/CustomName.cs deleted file mode 100644 index c7d74a1..0000000 --- a/Glamourer/GameData/CustomName.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Glamourer.GameData; - -/// For 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/CustomizationManager.cs b/Glamourer/GameData/CustomizationManager.cs deleted file mode 100644 index f249bf6..0000000 --- a/Glamourer/GameData/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.GameData; - -public class CustomizationManager : ICustomizationManager -{ - private static CustomizationOptions? _options; - - private CustomizationManager() - { } - - public static ICustomizationManager Create(ITextureProvider textures, IDataManager gameData, IPluginLog log, NpcCustomizeSet npcCustomizeSet) - { - _options ??= new CustomizationOptions(textures, gameData, log, npcCustomizeSet); - 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/CustomizationNpcOptions.cs b/Glamourer/GameData/CustomizationNpcOptions.cs deleted file mode 100644 index 84509be..0000000 --- a/Glamourer/GameData/CustomizationNpcOptions.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Penumbra.GameData.Enums; -using System.Collections.Generic; -using System.Linq; -using Penumbra.GameData.Structs; - -namespace Glamourer.GameData; - -public static class CustomizationNpcOptions -{ - public static Dictionary<(SubRace, Gender), IReadOnlyList<(CustomizeIndex, CustomizeValue)>> CreateNpcData(CustomizationSet[] sets, - NpcCustomizeSet npcCustomizeSet) - { - 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 npcCustomizeSet.Select(s => s.Customize)) - { - 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 = [(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()); - } -} diff --git a/Glamourer/GameData/CustomizationOptions.cs b/Glamourer/GameData/CustomizationOptions.cs deleted file mode 100644 index 6fc3b03..0000000 --- a/Glamourer/GameData/CustomizationOptions.cs +++ /dev/null @@ -1,530 +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 Penumbra.GameData.Structs; -using Race = Penumbra.GameData.Enums.Race; - -namespace Glamourer.GameData; - -// 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, NpcCustomizeSet npcCustomizeSet) - { - var tmp = new TemporaryData(gameData, this, log); - _icons = new IconStorage(textures, gameData); - SetNames(gameData); - foreach (var race in Clans) - { - foreach (var gender in Genders) - _customizationSets[ToIndex(race, gender)] = tmp.GetSet(race, gender); - } - - tmp.SetNpcData(_customizationSets, npcCustomizeSet); - } - - // 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) - { - 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 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, NpcCustomizeSet npcCustomizeSet) - { - var data = CustomizationNpcOptions.CreateNpcData(sets, npcCustomizeSet); - 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 ColorParameters(gameData, log); - _customizeSheet = 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; - public readonly ExcelSheet Lobby; - private readonly ColorParameters _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) - { - var ret = new CustomizeData[num]; - var idx = 0; - foreach (var value in _cmpFile.GetSlice(offset, num)) - { - ret[idx] = new CustomizeData(index, (CustomizeValue)(light ? 128 + idx : idx), value, (ushort)(offset + idx)); - ++idx; - } - - return ret; - } - - - 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 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); - } - } - - // 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); - - 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)); - } - - // 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/CustomizeData.cs b/Glamourer/GameData/CustomizeData.cs index 3a3e89c..4d4e2fd 100644 --- a/Glamourer/GameData/CustomizeData.cs +++ b/Glamourer/GameData/CustomizeData.cs @@ -5,26 +5,34 @@ using Penumbra.GameData.Structs; 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; @@ -34,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..fabc483 --- /dev/null +++ b/Glamourer/GameData/CustomizeManager.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +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 : IAsyncService +{ + /// 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 (!Awaiter.IsCompletedSuccessfully) + 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 tmpTask = Task.Run(() => 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); + } + + /// + public Task Awaiter { get; } + + 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; + } +} \ No newline at end of file diff --git a/Glamourer/GameData/CustomizationSet.cs b/Glamourer/GameData/CustomizeSet.cs similarity index 94% rename from Glamourer/GameData/CustomizationSet.cs rename to Glamourer/GameData/CustomizeSet.cs index 2a79c66..d2dc8e9 100644 --- a/Glamourer/GameData/CustomizationSet.cs +++ b/Glamourer/GameData/CustomizeSet.cs @@ -8,11 +8,13 @@ using Penumbra.GameData.Structs; 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; @@ -24,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) @@ -33,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]; @@ -95,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), @@ -194,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)] @@ -244,7 +248,7 @@ public class CustomizationSet public CharaMakeParams.MenuType Type(CustomizeIndex index) => Types[(int)index]; - internal static IReadOnlyDictionary ComputeOrder(CustomizationSet set) + internal static IReadOnlyDictionary ComputeOrder(CustomizeSet set) { var ret = Enum.GetValues().ToArray(); ret[(int)CustomizeIndex.TattooColor] = CustomizeIndex.EyeColorLeft; @@ -305,6 +309,6 @@ public class CustomizationSet public static class CustomizationSetExtensions { /// Return only the available customizations in this set and Clan or Gender. - public static CustomizeFlag FixApplication(this CustomizeFlag flag, CustomizationSet set) + public static CustomizeFlag FixApplication(this CustomizeFlag flag, CustomizeSet set) => flag & (set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender); } diff --git a/Glamourer/GameData/CustomizeSetFactory.cs b/Glamourer/GameData/CustomizeSetFactory.cs new file mode 100644 index 0000000..5eaaa58 --- /dev/null +++ b/Glamourer/GameData/CustomizeSetFactory.cs @@ -0,0 +1,459 @@ +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); + } + + /// 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)) + { + 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 _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 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 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); + } + } + + /// 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/ICustomizationManager.cs b/Glamourer/GameData/ICustomizationManager.cs deleted file mode 100644 index 2d884cd..0000000 --- a/Glamourer/GameData/ICustomizationManager.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using Dalamud.Interface.Internal; -using Penumbra.GameData.Enums; - -namespace Glamourer.GameData; - -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/NpcCustomizeSet.cs b/Glamourer/GameData/NpcCustomizeSet.cs index fd261c6..a99448e 100644 --- a/Glamourer/GameData/NpcCustomizeSet.cs +++ b/Glamourer/GameData/NpcCustomizeSet.cs @@ -13,20 +13,30 @@ 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); - private readonly List _data = []; + /// + public long Time { get; private set; } - public long Time { get; private set; } + /// public long Memory { get; private set; } + + /// public int TotalCount => _data.Count; + /// public Task Awaiter { get; } + /// 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); @@ -40,17 +50,21 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList }); } + /// 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; @@ -63,6 +77,8 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList 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); @@ -90,19 +106,25 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList 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; @@ -115,6 +137,7 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList 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()) @@ -125,13 +148,18 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList return list; } - private void FilterAndOrderNpcData(List eNpcEquip, List bNpcEquip) + /// 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 (name, 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]; @@ -145,6 +173,7 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList } } + // If there is only a single entry, add that. This does not take additional string memory through interning. if (duplicates.Count == 1) { _data.Add(duplicates[0]); @@ -152,24 +181,29 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList } else { + // Add all distinct duplicates with their ID specified in the name. _data.AddRange(duplicates .Select(duplicate => duplicate with { - Name = $"{name} ({(duplicate.Kind is ObjectKind.BattleNpc ? 'B' : 'E')}{duplicate.Id})" + Name = $"{name} ({(duplicate.Kind is ObjectKind.BattleNpc ? 'B' : 'E')}{duplicate.Id})", })); Memory += 96 * duplicates.Count + duplicates.Sum(d => d.Name.Length * 2); } } + // 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)); @@ -187,96 +221,102 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList 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); + 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 (customize.BodyType.Value != 1 - || !CustomizationOptions.Races.Contains(customize.Race) - || !CustomizationOptions.Clans.Contains(customize.Clan) - || !CustomizationOptions.Genders.Contains(customize.Gender)) + || !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); + 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 (customize.BodyType.Value != 1 - || !CustomizationOptions.Races.Contains(customize.Race) - || !CustomizationOptions.Clans.Contains(customize.Clan) - || !CustomizationOptions.Genders.Contains(customize.Gender)) + || !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 index 70bfe58..2db8fb5 100644 --- a/Glamourer/GameData/NpcData.cs +++ b/Glamourer/GameData/NpcData.cs @@ -5,17 +5,34 @@ using Penumbra.GameData.Structs; namespace Glamourer.GameData; +/// A struct containing everything to replicate the appearance of a human NPC. public unsafe struct NpcData { - public string Name; - public CustomizeArray Customize; - private fixed byte _equip[40]; - public CharacterWeapon Mainhand; - public CharacterWeapon Offhand; - public NpcId Id; - public bool VisorToggled; - public ObjectKind Kind; + /// 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 @@ -27,38 +44,40 @@ public unsafe struct NpcData } } + /// 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")); - sb.Append('-'); - sb.Append(span[i].Variant.Id.ToString("D3")); - sb.Append('-'); - sb.Append(span[i].Stain.Id.ToString("D3")); - sb.Append(", "); + 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")); - sb.Append('-'); - sb.Append(Mainhand.Weapon.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.Skeleton.Id.ToString("D4")); - sb.Append('-'); - sb.Append(Offhand.Weapon.Id.ToString("D4")); - sb.Append('-'); - sb.Append(Offhand.Variant.Id.ToString("D3")); - sb.Append('-'); - sb.Append(Offhand.Stain.Id.ToString("D3")); + sb.Append(Mainhand.Skeleton.Id.ToString("D4")) + .Append('-') + .Append(Mainhand.Weapon.Id.ToString("D4")) + .Append('-') + .Append(Mainhand.Variant.Id.ToString("D3")) + .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) @@ -67,6 +86,7 @@ public unsafe struct NpcData } } + /// Check if the appearance data, excluding ID and Name, of two NpcData is equal. public bool DataEquals(in NpcData other) { if (VisorToggled != other.VisorToggled) diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs index 939b0f6..9b35b92 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs @@ -29,7 +29,7 @@ public partial class CustomizationDrawer npc = true; } - var icon = _service.Service.GetIcon(custom!.Value.IconId); + var icon = _service.Manager.GetIcon(custom!.Value.IconId); using (_ = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw)) { if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) @@ -69,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.Service.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); @@ -180,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.Service.GetIcon(feature.IconId) - : _service.Service.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.cs b/Glamourer/Gui/Customization/CustomizationDrawer.cs index c131cf5..90cf5c2 100644 --- a/Glamourer/Gui/Customization/CustomizationDrawer.cs +++ b/Glamourer/Gui/Customization/CustomizationDrawer.cs @@ -14,7 +14,7 @@ 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); @@ -23,7 +23,7 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio private Exception? _terminate; private CustomizeArray _customize = CustomizeArray.Default; - private CustomizationSet _set = null!; + private CustomizeSet _set = null!; public CustomizeArray Customize => _customize; @@ -117,7 +117,7 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio return DrawArtisan(); DrawRaceGenderSelector(); - _set = _service.Service.GetList(_customize.Clan, _customize.Gender); + _set = _service.Manager.GetSet(_customize.Clan, _customize.Gender); foreach (var id in _set.Order[CharaMakeParams.MenuType.Percentage]) PercentageSelector(id); diff --git a/Glamourer/Gui/DesignCombo.cs b/Glamourer/Gui/DesignCombo.cs index e82ab3c..d52be90 100644 --- a/Glamourer/Gui/DesignCombo.cs +++ b/Glamourer/Gui/DesignCombo.cs @@ -180,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) @@ -210,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/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/Tabs/AutomationTab/SetPanel.cs b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs index 2a0453c..846823a 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs @@ -27,7 +27,7 @@ public class SetPanel( ItemUnlockManager _itemUnlocks, RevertDesignCombo _designCombo, CustomizeUnlockManager _customizeUnlocks, - CustomizationService _customizations, + CustomizeService _customizations, IdentifierDrawer _identifierDrawer, Configuration _config) { @@ -295,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.Service.GetList(customize.Clan, customize.Gender); + var set = _customizations.Manager.GetSet(customize.Clan, customize.Gender); foreach (var type in CustomizationExtensions.All) { var flag = type.ToFlag(); diff --git a/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs b/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs index c57c1b5..d2b9212 100644 --- a/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/CustomizationServicePanel.cs @@ -8,30 +8,27 @@ using Penumbra.GameData.Enums; namespace Glamourer.Gui.Tabs.DebugTab; -public class CustomizationServicePanel(CustomizationService _customization) : IDebugTabTree +public class CustomizationServicePanel(CustomizeService customize) : IDebugTabTree { public string Label => "Customization Service"; public bool Disabled - => !_customization.Awaiter.IsCompletedSuccessfully; + => !customize.Awaiter.IsCompletedSuccessfully; public void Draw() { - foreach (var clan in _customization.Service.Clans) + foreach (var (clan, gender) in CustomizeManager.AllSets()) { - foreach (var gender in _customization.Service.Genders) - { - var set = _customization.Service.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; @@ -49,9 +46,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/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index cd47606..5333ba5 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -154,7 +154,7 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer private void DrawCustomizeApplication() { - var set = _selector.Selected!.CustomizationSet; + var set = _selector.Selected!.CustomizeSet; var available = set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender; var flags = _selector.Selected!.ApplyCustomize == 0 ? 0 : (_selector.Selected!.ApplyCustomize & available) == available ? 3 : 1; if (ImGui.CheckboxFlags("Apply All Customizations", ref flags, 3)) diff --git a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs index 8953501..10d972f 100644 --- a/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs +++ b/Glamourer/Gui/Tabs/UnlocksTab/UnlockOverview.cs @@ -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; @@ -52,25 +52,22 @@ public class UnlockOverview } } - foreach (var clan in _customizations.Service.Clans) + foreach (var (clan, gender) in CustomizeManager.AllSets()) { - foreach (var gender in _customizations.Service.Genders) - { - if (_customizations.Service.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.Service.GetList(_selected2, _selected3); + var set = _customizations.Manager.GetSet(_selected2, _selected3); var spacing = IconSpacing; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); @@ -121,7 +118,7 @@ public class UnlockOverview continue; var unlocked = _customizeUnlocks.IsUnlocked(customize, out var time); - var icon = _customizations.Service.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); diff --git a/Glamourer/Interop/ImportService.cs b/Glamourer/Interop/ImportService.cs index 4cac25e..4ec59e9 100644 --- a/Glamourer/Interop/ImportService.cs +++ b/Glamourer/Interop/ImportService.cs @@ -15,7 +15,7 @@ using Penumbra.GameData.Structs; namespace Glamourer.Interop; -public class ImportService(CustomizationService _customizations, IDragDropManager _dragDropManager, ItemManager _items) +public class ImportService(CustomizeService _customizations, IDragDropManager _dragDropManager, ItemManager _items) { public void CreateDatSource() => _dragDropManager.CreateImGuiSource("DatDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".dat"), m => @@ -179,14 +179,14 @@ public class ImportService(CustomizationService _customizations, IDragDropManage if (input.BodyType.Value != 1) return false; - var set = _customizations.Service.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/Services/CustomizationService.cs b/Glamourer/Services/CustomizeService.cs similarity index 62% rename from Glamourer/Services/CustomizationService.cs rename to Glamourer/Services/CustomizeService.cs index 9e28e42..1ed6f07 100644 --- a/Glamourer/Services/CustomizationService.cs +++ b/Glamourer/Services/CustomizeService.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; -using Dalamud.Plugin.Services; using Glamourer.GameData; using OtterGui.Services; using Penumbra.GameData.DataContainers; @@ -10,26 +9,18 @@ using Penumbra.GameData.Structs; namespace Glamourer.Services; -public sealed class CustomizationService( - ITextureProvider textures, - IDataManager gameData, +public sealed class CustomizeService( HumanModelList humanModels, - IPluginLog log, - NpcCustomizeSet npcCustomizeSet) + NpcCustomizeSet npcCustomizeSet, + CustomizeManager manager) : IAsyncService { - public readonly HumanModelList HumanModels = humanModels; + public readonly HumanModelList HumanModels = humanModels; + public readonly CustomizeManager Manager = manager; + public readonly NpcCustomizeSet NpcCustomizeSet = npcCustomizeSet; - private ICustomizationManager? _service; - - private readonly Task _task = Task.WhenAll(humanModels.Awaiter, npcCustomizeSet.Awaiter) - .ContinueWith(_ => CustomizationManager.Create(textures, gameData, log, npcCustomizeSet)); - - public ICustomizationManager Service - => _service ??= _task.Result; - - public Task Awaiter - => _task; + public Task Awaiter { get; } + = Task.WhenAll(humanModels.Awaiter, manager.Awaiter, npcCustomizeSet.Awaiter); public (CustomizeArray NewValue, CustomizeFlag Applied, CustomizeFlag Changed) Combine(CustomizeArray oldValues, CustomizeArray newValues, CustomizeFlag applyWhich, bool allowUnknown) @@ -51,7 +42,7 @@ public sealed class CustomizationService( } - var set = Service.GetList(ret.Clan, ret.Gender); + var set = Manager.GetSet(ret.Clan, ret.Gender); applyWhich = applyWhich.FixApplication(set); foreach (var index in CustomizationExtensions.AllBasic) { @@ -79,69 +70,34 @@ public sealed class CustomizationService( gender = Gender.Female; if (gender == Gender.MaleNpc) gender = Gender.Male; - return (gender, race) switch - { - (Gender.Male, SubRace.Midlander) => Service.GetName(CustomName.MidlanderM), - (Gender.Male, SubRace.Highlander) => Service.GetName(CustomName.HighlanderM), - (Gender.Male, SubRace.Wildwood) => Service.GetName(CustomName.WildwoodM), - (Gender.Male, SubRace.Duskwight) => Service.GetName(CustomName.DuskwightM), - (Gender.Male, SubRace.Plainsfolk) => Service.GetName(CustomName.PlainsfolkM), - (Gender.Male, SubRace.Dunesfolk) => Service.GetName(CustomName.DunesfolkM), - (Gender.Male, SubRace.SeekerOfTheSun) => Service.GetName(CustomName.SeekerOfTheSunM), - (Gender.Male, SubRace.KeeperOfTheMoon) => Service.GetName(CustomName.KeeperOfTheMoonM), - (Gender.Male, SubRace.Seawolf) => Service.GetName(CustomName.SeawolfM), - (Gender.Male, SubRace.Hellsguard) => Service.GetName(CustomName.HellsguardM), - (Gender.Male, SubRace.Raen) => Service.GetName(CustomName.RaenM), - (Gender.Male, SubRace.Xaela) => Service.GetName(CustomName.XaelaM), - (Gender.Male, SubRace.Helion) => Service.GetName(CustomName.HelionM), - (Gender.Male, SubRace.Lost) => Service.GetName(CustomName.LostM), - (Gender.Male, SubRace.Rava) => Service.GetName(CustomName.RavaM), - (Gender.Male, SubRace.Veena) => Service.GetName(CustomName.VeenaM), - (Gender.Female, SubRace.Midlander) => Service.GetName(CustomName.MidlanderF), - (Gender.Female, SubRace.Highlander) => Service.GetName(CustomName.HighlanderF), - (Gender.Female, SubRace.Wildwood) => Service.GetName(CustomName.WildwoodF), - (Gender.Female, SubRace.Duskwight) => Service.GetName(CustomName.DuskwightF), - (Gender.Female, SubRace.Plainsfolk) => Service.GetName(CustomName.PlainsfolkF), - (Gender.Female, SubRace.Dunesfolk) => Service.GetName(CustomName.DunesfolkF), - (Gender.Female, SubRace.SeekerOfTheSun) => Service.GetName(CustomName.SeekerOfTheSunF), - (Gender.Female, SubRace.KeeperOfTheMoon) => Service.GetName(CustomName.KeeperOfTheMoonF), - (Gender.Female, SubRace.Seawolf) => Service.GetName(CustomName.SeawolfF), - (Gender.Female, SubRace.Hellsguard) => Service.GetName(CustomName.HellsguardF), - (Gender.Female, SubRace.Raen) => Service.GetName(CustomName.RaenF), - (Gender.Female, SubRace.Xaela) => Service.GetName(CustomName.XaelaF), - (Gender.Female, SubRace.Helion) => Service.GetName(CustomName.HelionM), - (Gender.Female, SubRace.Lost) => Service.GetName(CustomName.LostM), - (Gender.Female, SubRace.Rava) => Service.GetName(CustomName.RavaF), - (Gender.Female, SubRace.Veena) => Service.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) - => Service.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 : Service.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(Service.GetList(race, gender), face, type, value); + => IsCustomizationValid(Manager.GetSet(race, gender), face, type, value); /// /// Check that the given race and clan are valid. @@ -160,10 +116,10 @@ public sealed class CustomizationService( return string.Empty; } - if (Service.Races.Contains(race)) + if (CustomizeManager.Races.Contains(race)) { actualRace = race; - actualClan = Service.Clans.FirstOrDefault(c => c.ToRace() == race, SubRace.Unknown); + actualClan = CustomizeManager.Clans.FirstOrDefault(c => c.ToRace() == race, SubRace.Unknown); // This should not happen. if (actualClan == SubRace.Unknown) { @@ -189,7 +145,7 @@ public sealed class CustomizationService( /// public string ValidateGender(Race race, Gender gender, out Gender actualGender) { - if (!Service.Genders.Contains(gender)) + if (!CustomizeManager.Genders.Contains(gender)) { actualGender = Gender.Male; return $"The gender {gender.ToName()} is unknown, reset to {Gender.Male.ToName()}."; @@ -230,7 +186,7 @@ public sealed class CustomizationService( /// The returned actualValue is either the correct value or the one with index 0. /// The return value is an empty string or a warning message. /// - 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)) @@ -266,7 +222,7 @@ public sealed class CustomizationService( flags |= CustomizeFlag.Gender; } - var set = Service.GetList(customize.Clan, customize.Gender); + var set = Manager.GetSet(customize.Clan, customize.Gender); return FixValues(set, ref customize) | flags; } @@ -284,11 +240,11 @@ public sealed class CustomizationService( return 0; customize.Gender = newGender; - var set = Service.GetList(customize.Clan, customize.Gender); + var set = Manager.GetSet(customize.Clan, customize.Gender); return FixValues(set, ref customize) | CustomizeFlag.Gender; } - private static CustomizeFlag FixValues(CustomizationSet set, ref CustomizeArray customize) + private static CustomizeFlag FixValues(CustomizeSet set, ref CustomizeArray customize) { CustomizeFlag flags = 0; foreach (var idx in CustomizationExtensions.AllBasic) diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 3afe390..9cbcd01 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -84,7 +84,7 @@ public static class ServiceManagerA => services.AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(); diff --git a/Glamourer/State/FunModule.cs b/Glamourer/State/FunModule.cs index 67c9e08..deb7771 100644 --- a/Glamourer/State/FunModule.cs +++ b/Glamourer/State/FunModule.cs @@ -27,7 +27,7 @@ public unsafe class FunModule : IDisposable private readonly WorldSets _worldSets = new(); private readonly ItemManager _items; - private readonly CustomizationService _customizations; + private readonly CustomizeService _customizations; private readonly Configuration _config; private readonly CodeService _codes; private readonly Random _rng; @@ -67,7 +67,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) { @@ -197,7 +197,7 @@ public unsafe class FunModule : IDisposable if (!_codes.EnabledIndividual) return; - var set = _customizations.Service.GetList(customize.Clan, customize.Gender); + var set = _customizations.Manager.GetSet(customize.Clan, customize.Gender); foreach (var index in Enum.GetValues()) { if (index is CustomizeIndex.Face || !set.IsAvailable(index)) diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index 50b2605..0095fe9 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -12,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; @@ -72,7 +72,7 @@ public class StateEditor state[CustomizeIndex.Clan] = source; state[CustomizeIndex.Gender] = source; - var set = _customizations.Service.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; } diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index d7f947f..1f3c36b 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -28,7 +28,7 @@ public class StateListener : IDisposable 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; @@ -52,7 +52,7 @@ public class StateListener : IDisposable 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; @@ -167,7 +167,7 @@ public class StateListener : IDisposable return; } - var set = _customizations.Service.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) diff --git a/Glamourer/Unlocks/CustomizeUnlockManager.cs b/Glamourer/Unlocks/CustomizeUnlockManager.cs index a1e95ef..2bdbb78 100644 --- a/Glamourer/Unlocks/CustomizeUnlockManager.cs +++ b/Glamourer/Unlocks/CustomizeUnlockManager.cs @@ -29,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); @@ -174,38 +174,35 @@ public class CustomizeUnlockManager : IDisposable, ISavable "customization"); /// Create a list of all unlockable hairstyles and face paints. - private static Dictionary CreateUnlockableCustomizations(CustomizationService customizations, + private static Dictionary CreateUnlockableCustomizations(CustomizeService customizations, IDataManager gameData) { var ret = new Dictionary(); var sheet = gameData.GetExcelSheet(ClientLanguage.English)!; - foreach (var clan in customizations.Service.Clans) + foreach (var (clan, gender) in CustomizeManager.AllSets()) { - foreach (var gender in customizations.Service.Genders) + var list = customizations.Manager.GetSet(clan, gender); + foreach (var hair in list.HairStyles) { - var list = customizations.Service.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/OtterGui b/OtterGui index 197d23e..4404d62 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 197d23eee167c232000f22ef40a7a2bded913b6c +Subproject commit 4404d62b7442daa7e3436dc417364905e3d5cd2f