using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Dalamud; using Dalamud.Data; using Dalamud.Plugin; using Dalamud.Utility; using Lumina.Excel; using Lumina.Excel.GeneratedSheets; using OtterGui.Classes; using Penumbra.GameData.Enums; using Race = Penumbra.GameData.Enums.Race; namespace Glamourer.Customization; // Generate everything about customization per tribe and gender. public partial class CustomizationOptions { // All races except for Unknown internal static readonly Race[] Races = ((Race[])Enum.GetValues(typeof(Race))).Skip(1).ToArray(); // All tribes except for Unknown internal static readonly SubRace[] Clans = ((SubRace[])Enum.GetValues(typeof(SubRace))).Skip(1).ToArray(); // Two genders. internal static readonly Gender[] Genders = { Gender.Male, Gender.Female, }; // Every tribe and gender has a separate set of available customizations. internal CustomizationSet GetList(SubRace race, Gender gender) => _customizationSets[ToIndex(race, gender)]; // Get specific icons. internal ImGuiScene.TextureWrap 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. private 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 { private readonly bool _valid; public string GetName(CustomName name) => _names[(int)name]; internal CustomizationOptions(DalamudPluginInterface pi, DataManager gameData) { 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); } } // 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(DataManager gameData, TemporaryData tmp) { var subRace = gameData.GetExcelSheet()!; void Set(CustomName id, Lumina.Text.SeString? s, string def) => _names[(int)id] = s?.ToDalamudString().TextValue ?? def; Set(CustomName.Clan, tmp.Lobby.GetRow(102)?.Text, "Clan"); Set(CustomName.Gender, tmp.Lobby.GetRow(103)?.Text, "Gender"); Set(CustomName.Reverse, tmp.Lobby.GetRow(2135)?.Text, "Reverse"); Set(CustomName.OddEyes, tmp.Lobby.GetRow(2125)?.Text, "Odd Eyes"); Set(CustomName.IrisSmall, tmp.Lobby.GetRow(1076)?.Text, "Small"); Set(CustomName.IrisLarge, tmp.Lobby.GetRow(1075)?.Text, "Large"); Set(CustomName.IrisSize, tmp.Lobby.GetRow(244)?.Text, "Iris Size"); Set(CustomName.MidlanderM, subRace.GetRow((int)SubRace.Midlander)?.Masculine, SubRace.Midlander.ToName()); Set(CustomName.MidlanderF, subRace.GetRow((int)SubRace.Midlander)?.Feminine, SubRace.Midlander.ToName()); Set(CustomName.HighlanderM, subRace.GetRow((int)SubRace.Highlander)?.Masculine, SubRace.Highlander.ToName()); Set(CustomName.HighlanderF, subRace.GetRow((int)SubRace.Highlander)?.Feminine, SubRace.Highlander.ToName()); Set(CustomName.WildwoodM, subRace.GetRow((int)SubRace.Wildwood)?.Masculine, SubRace.Wildwood.ToName()); Set(CustomName.WildwoodF, subRace.GetRow((int)SubRace.Wildwood)?.Feminine, SubRace.Wildwood.ToName()); Set(CustomName.DuskwightM, subRace.GetRow((int)SubRace.Duskwight)?.Masculine, SubRace.Duskwight.ToName()); Set(CustomName.DuskwightF, subRace.GetRow((int)SubRace.Duskwight)?.Feminine, SubRace.Duskwight.ToName()); Set(CustomName.PlainsfolkM, subRace.GetRow((int)SubRace.Plainsfolk)?.Masculine, SubRace.Plainsfolk.ToName()); Set(CustomName.PlainsfolkF, subRace.GetRow((int)SubRace.Plainsfolk)?.Feminine, SubRace.Plainsfolk.ToName()); Set(CustomName.DunesfolkM, subRace.GetRow((int)SubRace.Dunesfolk)?.Masculine, SubRace.Dunesfolk.ToName()); Set(CustomName.DunesfolkF, subRace.GetRow((int)SubRace.Dunesfolk)?.Feminine, SubRace.Dunesfolk.ToName()); Set(CustomName.SeekerOfTheSunM, subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Masculine, SubRace.SeekerOfTheSun.ToName()); Set(CustomName.SeekerOfTheSunF, subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Feminine, SubRace.SeekerOfTheSun.ToName()); Set(CustomName.KeeperOfTheMoonM, subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Masculine, SubRace.KeeperOfTheMoon.ToName()); Set(CustomName.KeeperOfTheMoonF, subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Feminine, SubRace.KeeperOfTheMoon.ToName()); Set(CustomName.SeawolfM, subRace.GetRow((int)SubRace.Seawolf)?.Masculine, SubRace.Seawolf.ToName()); Set(CustomName.SeawolfF, subRace.GetRow((int)SubRace.Seawolf)?.Feminine, SubRace.Seawolf.ToName()); Set(CustomName.HellsguardM, subRace.GetRow((int)SubRace.Hellsguard)?.Masculine, SubRace.Hellsguard.ToName()); Set(CustomName.HellsguardF, subRace.GetRow((int)SubRace.Hellsguard)?.Feminine, SubRace.Hellsguard.ToName()); Set(CustomName.RaenM, subRace.GetRow((int)SubRace.Raen)?.Masculine, SubRace.Raen.ToName()); Set(CustomName.RaenF, subRace.GetRow((int)SubRace.Raen)?.Feminine, SubRace.Raen.ToName()); Set(CustomName.XaelaM, subRace.GetRow((int)SubRace.Xaela)?.Masculine, SubRace.Xaela.ToName()); Set(CustomName.XaelaF, subRace.GetRow((int)SubRace.Xaela)?.Feminine, SubRace.Xaela.ToName()); Set(CustomName.HelionM, subRace.GetRow((int)SubRace.Helion)?.Masculine, SubRace.Helion.ToName()); Set(CustomName.HelionF, subRace.GetRow((int)SubRace.Helion)?.Feminine, SubRace.Helion.ToName()); Set(CustomName.LostM, subRace.GetRow((int)SubRace.Lost)?.Masculine, SubRace.Lost.ToName()); Set(CustomName.LostF, subRace.GetRow((int)SubRace.Lost)?.Feminine, SubRace.Lost.ToName()); Set(CustomName.RavaM, subRace.GetRow((int)SubRace.Rava)?.Masculine, SubRace.Rava.ToName()); Set(CustomName.RavaF, subRace.GetRow((int)SubRace.Rava)?.Feminine, SubRace.Rava.ToName()); Set(CustomName.VeenaM, subRace.GetRow((int)SubRace.Veena)?.Masculine, SubRace.Veena.ToName()); Set(CustomName.VeenaF, subRace.GetRow((int)SubRace.Veena)?.Feminine, SubRace.Veena.ToName()); } private class TemporaryData { public bool Valid => _cmpFile.Valid; public CustomizationSet GetSet(SubRace race, Gender gender) { var (skin, hair) = GetColors(race, gender); var row = _listSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; var hrothgar = race.ToRace() == Race.Hrothgar; // Create the initial set with all the easily accessible parameters available for anyone. var set = new CustomizationSet(race, gender) { 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 TemporaryData(DataManager gameData, CustomizationOptions options) { _options = options; _cmpFile = new CmpFile(gameData); _customizeSheet = gameData.GetExcelSheet()!; Lobby = gameData.GetExcelSheet()!; var tmp = gameData.Excel.GetType().GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)? .MakeGenericMethod(typeof(CharaMakeParams)).Invoke(gameData.Excel, new object?[] { "charamaketype", 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 CmpFile _cmpFile; // Those values are shared between all races. private readonly CustomizeData[] _highlightPicker; private readonly CustomizeData[] _eyeColorPicker; private readonly CustomizeData[] _facePaintColorPickerDark; private readonly CustomizeData[] _facePaintColorPickerLight; private readonly CustomizeData[] _lipColorPickerDark; private readonly CustomizeData[] _lipColorPickerLight; private readonly CustomizeData[] _tattooColorPicker; private readonly CustomizationOptions _options; private CustomizeData[] CreateColorPicker(CustomizeIndex index, int offset, int num, bool light = false) => _cmpFile.GetSlice(offset, num) .Select((c, i) => new CustomizeData(index, (CustomizeValue)(light ? 128 + i : 0 + i), c, (ushort)(offset + i))) .ToArray(); private void SetHairByFace(CustomizationSet set) { if (set.Race != Race.Hrothgar) { set.HairByFace = Enumerable.Repeat(set.HairStyles, set.Faces.Count + 1).ToArray(); return; } var tmp = new IReadOnlyList[set.Faces.Count + 1]; tmp[0] = set.HairStyles; for (var i = 1; i <= set.Faces.Count; ++i) { bool Valid(CustomizeData c) { var data = _customizeSheet.GetRow(c.CustomizeId)?.Unknown6 ?? 0; return data == 0 || data == i + set.Faces.Count; } tmp[i] = set.HairStyles.Where(Valid).ToArray(); } set.HairByFace = tmp; } private static void SetMenuTypes(CustomizationSet set, CharaMakeParams row) { // Set up the menu types for all customizations. set.Types = Enum.GetValues().Select(c => { // Those types are not correctly given in the menu, so special case them to color pickers. switch (c) { case CustomizeIndex.HighlightsColor: case CustomizeIndex.EyeColorLeft: case CustomizeIndex.EyeColorRight: 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); return menu?.Type ?? CharaMakeParams.MenuType.ListSelector; }).ToArray(); set.Order = CustomizationSet.ComputeOrder(set); } // Set customizations available if they have any options. private static void SetAvailability(CustomizationSet set, CharaMakeParams row) { if (set.Race == Race.Hrothgar && set.Gender == Gender.Female) return; void Set(bool available, CustomizeIndex flag) { if (available) set.SetAvailable(flag); } Set(true, CustomizeIndex.Height); Set(set.Faces.Count > 0, CustomizeIndex.Face); Set(true, CustomizeIndex.Hairstyle); Set(true, CustomizeIndex.Highlights); Set(true, CustomizeIndex.SkinColor); Set(true, CustomizeIndex.EyeColorRight); Set(true, CustomizeIndex.HairColor); Set(true, CustomizeIndex.HighlightsColor); Set(true, CustomizeIndex.TattooColor); Set(set.NumEyebrows > 0, CustomizeIndex.Eyebrows); Set(true, CustomizeIndex.EyeColorLeft); Set(set.NumEyeShapes > 0, CustomizeIndex.EyeShape); Set(set.NumNoseShapes > 0, CustomizeIndex.Nose); Set(set.NumJawShapes > 0, CustomizeIndex.Jaw); Set(set.NumMouthShapes > 0, CustomizeIndex.Mouth); Set(set.LipColorsDark.Count > 0, CustomizeIndex.LipColor); Set(GetListSize(row, CustomizeIndex.MuscleMass) > 0, CustomizeIndex.MuscleMass); Set(set.TailEarShapes.Count > 0, CustomizeIndex.TailShape); Set(GetListSize(row, CustomizeIndex.BustSize) > 0, CustomizeIndex.BustSize); Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaint); Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaintColor); Set(true, CustomizeIndex.FacialFeature1); Set(true, CustomizeIndex.FacialFeature2); Set(true, CustomizeIndex.FacialFeature3); Set(true, CustomizeIndex.FacialFeature4); Set(true, CustomizeIndex.FacialFeature5); Set(true, CustomizeIndex.FacialFeature6); Set(true, CustomizeIndex.FacialFeature7); Set(true, CustomizeIndex.LegacyTattoo); Set(true, CustomizeIndex.SmallIris); Set(set.Race != Race.Hrothgar, CustomizeIndex.Lipstick); Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaintReversed); } // Create a list of lists of facial features and the legacy tattoo. private static void SetFacialFeatures(CustomizationSet set, CharaMakeParams row) { var count = set.Faces.Count; set.FacialFeature1 = new List<(CustomizeData, CustomizeData)>(count); static (CustomizeData, CustomizeData) Create(CustomizeIndex i, uint data) => (new CustomizeData(i, CustomizeValue.Zero, data, 0), new CustomizeData(i, CustomizeValue.Max, data, 1)); set.LegacyTattoo = Create(CustomizeIndex.LegacyTattoo, 137905); var tmp = Enumerable.Repeat(0, 7).Select(_ => new (CustomizeData, CustomizeData)[count + 1]).ToArray(); for (var i = 0; i < count; ++i) { var data = row.FacialFeatureByFace[i].Icons; tmp[0][i + 1] = Create(CustomizeIndex.FacialFeature1, data[0]); tmp[1][i + 1] = Create(CustomizeIndex.FacialFeature2, data[1]); tmp[2][i + 1] = Create(CustomizeIndex.FacialFeature3, data[2]); tmp[3][i + 1] = Create(CustomizeIndex.FacialFeature4, data[3]); tmp[4][i + 1] = Create(CustomizeIndex.FacialFeature5, data[4]); tmp[5][i + 1] = Create(CustomizeIndex.FacialFeature6, data[5]); tmp[6][i + 1] = Create(CustomizeIndex.FacialFeature7, data[6]); } set.FacialFeature1 = tmp[0]; set.FacialFeature2 = tmp[1]; set.FacialFeature3 = tmp[2]; set.FacialFeature4 = tmp[3]; set.FacialFeature5 = tmp[4]; set.FacialFeature6 = tmp[5]; set.FacialFeature7 = tmp[6]; } // Set the names for the given set of parameters. private void SetNames(CustomizationSet set, CharaMakeParams row) { var nameArray = Enum.GetValues().Select(c => { // Find the first menu that corresponds to the Id. var byteId = c.ToByteAndMask().ByteIdx; var menu = row.Menus .Cast() .FirstOrDefault(m => m!.Value.Customize == byteId); if (menu == null) { // If none exists and the id corresponds to highlights, set the Highlights name. if (c == CustomizeIndex.Highlights) return Lobby.GetRow(237)?.Text.ToDalamudString().ToString() ?? "Highlights"; // Otherwise there is an error and we use the default name. return c.ToDefaultName(); } // Facial Features and Tattoos is created by combining two strings. if (c is >= CustomizeIndex.FacialFeature1 and <= CustomizeIndex.LegacyTattoo) return $"{Lobby.GetRow(1741)?.Text.ToDalamudString().ToString() ?? "Facial Features"} & {Lobby.GetRow(1742)?.Text.ToDalamudString().ToString() ?? "Tattoos"}"; // Otherwise all is normal, get the menu name or if it does not work the default name. var textRow = Lobby.GetRow(menu.Value.Id); return textRow?.Text.ToDalamudString().ToString() ?? c.ToDefaultName(); }).ToArray(); // Add names for both eye colors. nameArray[(int)CustomizeIndex.EyeColorLeft] = nameArray[(int)CustomizeIndex.EyeColorRight]; nameArray[(int)CustomizeIndex.EyeColorRight] = _options.GetName(CustomName.OddEyes); set.OptionName = nameArray; } // Obtain available skin and hair colors for the given subrace and gender. private (CustomizeData[], CustomizeData[]) GetColors(SubRace race, Gender gender) { if (race is > SubRace.Veena or SubRace.Unknown) throw new ArgumentOutOfRangeException(nameof(race), race, null); var gv = gender == Gender.Male ? 0 : 1; var idx = ((int)race * 2 + gv) * 5 + 3; return (CreateColorPicker(CustomizeIndex.SkinColor, idx << 8, 192), CreateColorPicker(CustomizeIndex.HairColor, (idx + 1) << 8, 192)); } // Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender. private CustomizeData[] GetHairStyles(SubRace race, Gender gender) { var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; // Unknown30 is the number of available hairstyles. var hairList = new List(row.Unknown30); // Hairstyles can be found starting at Unknown66. for (var i = 0; i < row.Unknown30; ++i) { var name = $"Unknown{66 + i * 9}"; var customizeIdx = (uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row) ?? uint.MaxValue; if (customizeIdx == uint.MaxValue) continue; // Hair Row from CustomizeSheet might not be set in case of unlockable hair. var hairRow = _customizeSheet.GetRow(customizeIdx); if (hairRow == null) hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)i, customizeIdx)); else if (_options._icons.IconExists(hairRow.Icon)) hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon, (ushort)hairRow.RowId)); } return hairList.OrderBy(h => h.Value.Value).ToArray(); } // Get Features. private CustomizeData FromValueAndIndex(CustomizeIndex id, uint value, int index) { var row = _customizeSheet.GetRow(value); return row == null ? new CustomizeData(id, (CustomizeValue)(index + 1), value) : new CustomizeData(id, (CustomizeValue)row.FeatureID, row.Icon, (ushort)row.RowId); } // Get List sizes. private static int GetListSize(CharaMakeParams row, CustomizeIndex index) { var gameId = index.ToByteAndMask().ByteIdx; var menu = row.Menus.Cast().FirstOrDefault(m => m!.Value.Customize == gameId); return menu?.Size ?? 0; } // Get face paints from the hair sheet via reflection. private CustomizeData[] GetFacePaints(SubRace race, Gender gender) { var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; var paintList = new List(row.Unknown37); // Number of available face paints is at Unknown37. for (var i = 0; i < row.Unknown37; ++i) { // Face paints start at Unknown73. var name = $"Unknown{73 + i * 9}"; var customizeIdx = (uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row) ?? uint.MaxValue; if (customizeIdx == uint.MaxValue) continue; var paintRow = _customizeSheet.GetRow(customizeIdx); // Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints. if (paintRow != null) paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon, (ushort)paintRow.RowId)); else paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)i, customizeIdx)); } return paintList.OrderBy(p => p.Value.Value).ToArray(); } // Specific icons for tails or ears. private CustomizeData[] GetTailEarShapes(CharaMakeParams row) => row.Menus.Cast() .FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.TailShape.ToByteAndMask().ByteIdx)?.Values .Select((v, i) => FromValueAndIndex(CustomizeIndex.TailShape, v, i)).ToArray() ?? Array.Empty(); // Specific icons for faces. private CustomizeData[] GetFaces(CharaMakeParams row) => row.Menus.Cast().FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.Face.ToByteAndMask().ByteIdx) ?.Values .Select((v, i) => FromValueAndIndex(CustomizeIndex.Face, v, i)).ToArray() ?? Array.Empty(); // Specific icons for Hrothgar patterns. private CustomizeData[] HrothgarFurPattern(CharaMakeParams row) => row.Menus.Cast() .FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.LipColor.ToByteAndMask().ByteIdx)?.Values .Select((v, i) => FromValueAndIndex(CustomizeIndex.LipColor, v, i)).ToArray() ?? Array.Empty(); } }