diff --git a/Glamourer.GameData/Customization/CustomizationNpcOptions.cs b/Glamourer.GameData/Customization/CustomizationNpcOptions.cs new file mode 100644 index 0000000..d92f1da --- /dev/null +++ b/Glamourer.GameData/Customization/CustomizationNpcOptions.cs @@ -0,0 +1,141 @@ +using System.Collections.Generic; +using System.Linq; +using Lumina.Excel; +using Lumina.Excel.GeneratedSheets; +using OtterGui; +using Penumbra.GameData.Enums; + +namespace Glamourer.Customization; + +public static class CustomizationNpcOptions +{ + public static Dictionary<(SubRace, Gender), HashSet<(CustomizeIndex, CustomizeValue)>> CreateNpcData(CustomizationSet[] sets, + ExcelSheet bNpc, ExcelSheet eNpc) + { + var customizes = bNpc.SelectWhere(FromBnpcCustomize) + .Concat(eNpc.SelectWhere(FromEnpcBase)).ToList(); + + var dict = new Dictionary<(SubRace, Gender), HashSet<(CustomizeIndex, CustomizeValue)>>(); + var customizeIndices = new[] + { + CustomizeIndex.Face, + CustomizeIndex.Hairstyle, + CustomizeIndex.LipColor, + CustomizeIndex.SkinColor, + CustomizeIndex.FacePaintColor, + CustomizeIndex.HighlightsColor, + CustomizeIndex.HairColor, + CustomizeIndex.FacePaint, + CustomizeIndex.TattooColor, + CustomizeIndex.EyeColorLeft, + CustomizeIndex.EyeColorRight, + }; + + foreach (var customize in customizes) + { + var set = sets[CustomizationOptions.ToIndex(customize.Clan, customize.Gender)]; + foreach (var customizeIndex in customizeIndices) + { + var value = customize[customizeIndex]; + if (value == CustomizeValue.Zero) + continue; + + if (set.DataByValue(customizeIndex, value, out _, customize.Face) >= 0) + continue; + + if (!dict.TryGetValue((set.Clan, set.Gender), out var npcSet)) + { + npcSet = new HashSet<(CustomizeIndex, CustomizeValue)> { (customizeIndex, value) }; + dict.Add((set.Clan, set.Gender), npcSet); + } + else + { + npcSet.Add((customizeIndex, value)); + } + } + } + + return dict; + } + + private static (bool, Customize) FromBnpcCustomize(BNpcCustomize bnpcCustomize) + { + var customize = new Customize(); + customize.Data.Set(0, (byte)bnpcCustomize.Race.Row); + customize.Data.Set(1, bnpcCustomize.Gender); + customize.Data.Set(2, bnpcCustomize.BodyType); + customize.Data.Set(3, bnpcCustomize.Height); + customize.Data.Set(4, (byte)bnpcCustomize.Tribe.Row); + customize.Data.Set(5, bnpcCustomize.Face); + customize.Data.Set(6, bnpcCustomize.HairStyle); + customize.Data.Set(7, bnpcCustomize.HairHighlight); + customize.Data.Set(8, bnpcCustomize.SkinColor); + customize.Data.Set(9, bnpcCustomize.EyeHeterochromia); + customize.Data.Set(10, bnpcCustomize.HairColor); + customize.Data.Set(11, bnpcCustomize.HairHighlightColor); + customize.Data.Set(12, bnpcCustomize.FacialFeature); + customize.Data.Set(13, bnpcCustomize.FacialFeatureColor); + customize.Data.Set(14, bnpcCustomize.Eyebrows); + customize.Data.Set(15, bnpcCustomize.EyeColor); + customize.Data.Set(16, bnpcCustomize.EyeShape); + customize.Data.Set(17, bnpcCustomize.Nose); + customize.Data.Set(18, bnpcCustomize.Jaw); + customize.Data.Set(19, bnpcCustomize.Mouth); + customize.Data.Set(20, bnpcCustomize.LipColor); + customize.Data.Set(21, bnpcCustomize.BustOrTone1); + customize.Data.Set(22, bnpcCustomize.ExtraFeature1); + customize.Data.Set(23, bnpcCustomize.ExtraFeature2OrBust); + customize.Data.Set(24, bnpcCustomize.FacePaint); + customize.Data.Set(25, bnpcCustomize.FacePaintColor); + + if (customize.BodyType.Value != 1 + || !CustomizationOptions.Races.Contains(customize.Race) + || !CustomizationOptions.Clans.Contains(customize.Clan) + || !CustomizationOptions.Genders.Contains(customize.Gender)) + return (false, Customize.Default); + + return (true, customize); + } + + private static (bool, Customize) FromEnpcBase(ENpcBase enpcBase) + { + if (enpcBase.ModelChara.Row != 0) + return (false, Customize.Default); + + var customize = new Customize(); + customize.Data.Set(0, (byte)enpcBase.Race.Row); + customize.Data.Set(1, enpcBase.Gender); + customize.Data.Set(2, enpcBase.BodyType); + customize.Data.Set(3, enpcBase.Height); + customize.Data.Set(4, (byte)enpcBase.Tribe.Row); + customize.Data.Set(5, enpcBase.Face); + customize.Data.Set(6, enpcBase.HairStyle); + customize.Data.Set(7, enpcBase.HairHighlight); + customize.Data.Set(8, enpcBase.SkinColor); + customize.Data.Set(9, enpcBase.EyeHeterochromia); + customize.Data.Set(10, enpcBase.HairColor); + customize.Data.Set(11, enpcBase.HairHighlightColor); + customize.Data.Set(12, enpcBase.FacialFeature); + customize.Data.Set(13, enpcBase.FacialFeatureColor); + customize.Data.Set(14, enpcBase.Eyebrows); + customize.Data.Set(15, enpcBase.EyeColor); + customize.Data.Set(16, enpcBase.EyeShape); + customize.Data.Set(17, enpcBase.Nose); + customize.Data.Set(18, enpcBase.Jaw); + customize.Data.Set(19, enpcBase.Mouth); + customize.Data.Set(20, enpcBase.LipColor); + customize.Data.Set(21, enpcBase.BustOrTone1); + customize.Data.Set(22, enpcBase.ExtraFeature1); + customize.Data.Set(23, enpcBase.ExtraFeature2OrBust); + customize.Data.Set(24, enpcBase.FacePaint); + customize.Data.Set(25, enpcBase.FacePaintColor); + + if (customize.BodyType.Value != 1 + || !CustomizationOptions.Races.Contains(customize.Race) + || !CustomizationOptions.Clans.Contains(customize.Clan) + || !CustomizationOptions.Genders.Contains(customize.Gender)) + return (false, Customize.Default); + + return (true, customize); + } +} diff --git a/Glamourer.GameData/Customization/CustomizationOptions.cs b/Glamourer.GameData/Customization/CustomizationOptions.cs index ade35d7..f265d9a 100644 --- a/Glamourer.GameData/Customization/CustomizationOptions.cs +++ b/Glamourer.GameData/Customization/CustomizationOptions.cs @@ -45,7 +45,7 @@ public partial class CustomizationOptions // Get the index for the given pair of tribe and gender. - private static int ToIndex(SubRace race, Gender 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) @@ -59,8 +59,6 @@ public partial class CustomizationOptions public partial class CustomizationOptions { - private readonly bool _valid; - public string GetName(CustomName name) => _names[(int)name]; @@ -68,13 +66,13 @@ public partial class CustomizationOptions { var tmp = new TemporaryData(gameData, this); _icons = new IconStorage(pi, gameData, _customizationSets.Length * 50); - _valid = tmp.Valid; SetNames(gameData, tmp); foreach (var race in Clans) { foreach (var gender in Genders) _customizationSets[ToIndex(race, gender)] = tmp.GetSet(race, gender); } + tmp.SetNpcData(_customizationSets); } // Obtain localized names of customization options and race names from the game data. @@ -171,11 +169,24 @@ public partial class CustomizationOptions return set; } + public void SetNpcData(CustomizationSet[] sets) + { + var data = CustomizationNpcOptions.CreateNpcData(sets, _bnpcCustomize, _enpcBase); + foreach (var set in sets) + { + if (data.TryGetValue((set.Clan, set.Gender), out var npcData)) + set.NpcOptions = npcData.ToArray(); + } + } + + public TemporaryData(DataManager gameData, CustomizationOptions options) { _options = options; _cmpFile = new CmpFile(gameData); _customizeSheet = gameData.GetExcelSheet()!; + _bnpcCustomize = gameData.GetExcelSheet()!; + _enpcBase = gameData.GetExcelSheet()!; Lobby = gameData.GetExcelSheet()!; var tmp = gameData.Excel.GetType().GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)? .MakeGenericMethod(typeof(CharaMakeParams)).Invoke(gameData.Excel, new object?[] @@ -199,6 +210,8 @@ public partial class CustomizationOptions private readonly ExcelSheet _customizeSheet; private readonly ExcelSheet _listSheet; private readonly ExcelSheet _hairSheet; + private readonly ExcelSheet _bnpcCustomize; + private readonly ExcelSheet _enpcBase; public readonly ExcelSheet Lobby; private readonly CmpFile _cmpFile; @@ -213,6 +226,7 @@ public partial class CustomizationOptions private readonly CustomizationOptions _options; + private CustomizeData[] CreateColorPicker(CustomizeIndex index, int offset, int num, bool light = false) => _cmpFile.GetSlice(offset, num) .Select((c, i) => new CustomizeData(index, (CustomizeValue)(light ? 128 + i : 0 + i), c, (ushort)(offset + i))) diff --git a/Glamourer.GameData/Customization/CustomizationSet.cs b/Glamourer.GameData/Customization/CustomizationSet.cs index 3207ab1..3d80351 100644 --- a/Glamourer.GameData/Customization/CustomizationSet.cs +++ b/Glamourer.GameData/Customization/CustomizationSet.cs @@ -65,6 +65,8 @@ public class CustomizationSet public (CustomizeData, CustomizeData) LegacyTattoo { get; internal set; } public IReadOnlyList FacePaints { get; internal init; } = null!; + public IReadOnlyList<(CustomizeIndex Type, CustomizeValue Value)> NpcOptions { get; internal set; } = + Array.Empty<(CustomizeIndex Type, CustomizeValue Value)>(); // Always Color Selector public IReadOnlyList SkinColors { get; internal init; } = null!; @@ -77,6 +79,17 @@ public class CustomizationSet public IReadOnlyList LipColorsLight { get; internal init; } = null!; public IReadOnlyList LipColorsDark { get; internal init; } = null!; + public bool Validate(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom, CustomizeValue face) + { + if (IsAvailable(index)) + return DataByValue(index, value, out custom, face) >= 0 + || NpcOptions.Any(t => t.Type == index && t.Value == value); + + custom = null; + return value == CustomizeValue.Zero; + + } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public int DataByValue(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom, CustomizeValue face) { diff --git a/Glamourer/Glamourer.cs b/Glamourer/Glamourer.cs index a57a627..65eaa15 100644 --- a/Glamourer/Glamourer.cs +++ b/Glamourer/Glamourer.cs @@ -23,7 +23,6 @@ public class Glamourer : IDalamudPlugin public static readonly Logger Log = new(); public static ChatService Chat { get; private set; } = null!; - private readonly ServiceProvider _services; public Glamourer(DalamudPluginInterface pluginInterface) @@ -45,7 +44,5 @@ public class Glamourer : IDalamudPlugin public void Dispose() - { - _services?.Dispose(); - } + => _services?.Dispose(); } diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs index 7924c4c..327e135 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -822,7 +822,11 @@ public unsafe class DebugTab : ITab foreach (var clan in _customization.AwaitedService.Clans) { foreach (var gender in _customization.AwaitedService.Genders) - DrawCustomizationInfo(_customization.AwaitedService.GetList(clan, gender)); + { + var set = _customization.AwaitedService.GetList(clan, gender); + DrawCustomizationInfo(set); + DrawNpcCustomizationInfo(set); + } } } @@ -846,6 +850,23 @@ public unsafe class DebugTab : ITab } } + private void DrawNpcCustomizationInfo(CustomizationSet set) + { + using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender} (NPC Options)"); + if (!tree) + return; + + using var table = ImRaii.Table("npc", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + foreach(var (index, value) in set.NpcOptions) + { + ImGuiUtil.DrawTableColumn(index.ToString()); + ImGuiUtil.DrawTableColumn(value.Value.ToString()); + } + } + #endregion #region Designs diff --git a/Glamourer/Services/CustomizationService.cs b/Glamourer/Services/CustomizationService.cs index ef2c138..fcfc2f8 100644 --- a/Glamourer/Services/CustomizationService.cs +++ b/Glamourer/Services/CustomizationService.cs @@ -122,12 +122,12 @@ public sealed class CustomizationService : AsyncServiceWrapper set.DataByValue(type, value, out data, face) >= 0 || !set.IsAvailable(type) && value.Value == 0; + => 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) - => AwaitedService.GetList(race, gender).DataByValue(type, value, out _, face) >= 0; + => IsCustomizationValid(AwaitedService.GetList(race, gender), face, type, value); /// /// Check that the given race and clan are valid.