using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using OtterGui; using Penumbra.GameData.Enums; namespace Glamourer.Customization; // Each Subrace and Gender combo has a customization set. // This describes the available customizations, their types and their names. public class CustomizationSet { internal CustomizationSet(SubRace clan, Gender gender) { Gender = gender; Clan = clan; Race = clan.ToRace(); SettingAvailable = 0; } public Gender Gender { get; } public SubRace Clan { get; } public Race Race { get; } public CustomizeFlag SettingAvailable { get; internal set; } internal void SetAvailable(CustomizeIndex index) => SettingAvailable |= index.ToFlag(); public bool IsAvailable(CustomizeIndex index) => SettingAvailable.HasFlag(index.ToFlag()); // Meta public IReadOnlyList OptionName { get; internal set; } = null!; public string Option(CustomizeIndex index) => OptionName[(int)index]; public IReadOnlyList Voices { get; internal init; } = null!; public IReadOnlyList Types { get; internal set; } = null!; public IReadOnlyDictionary Order { get; internal set; } = null!; // Always list selector. public int NumEyebrows { get; internal init; } public int NumEyeShapes { get; internal init; } public int NumNoseShapes { get; internal init; } public int NumJawShapes { get; internal init; } public int NumMouthShapes { get; internal init; } // Always Icon Selector public IReadOnlyList Faces { get; internal init; } = null!; public IReadOnlyList HairStyles { get; internal init; } = null!; public IReadOnlyList> HairByFace { get; internal set; } = null!; public IReadOnlyList TailEarShapes { get; internal init; } = null!; public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature1 { get; internal set; } = null!; public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature2 { get; internal set; } = null!; public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature3 { get; internal set; } = null!; public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature4 { get; internal set; } = null!; public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature5 { get; internal set; } = null!; public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature6 { get; internal set; } = null!; public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature7 { get; internal set; } = null!; 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!; public IReadOnlyList HairColors { get; internal init; } = null!; public IReadOnlyList HighlightColors { get; internal init; } = null!; public IReadOnlyList EyeColors { get; internal init; } = null!; public IReadOnlyList TattooColors { get; internal init; } = null!; public IReadOnlyList FacePaintColorsLight { get; internal init; } = null!; public IReadOnlyList FacePaintColorsDark { get; internal init; } = null!; 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) { 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), CharaMakeParams.MenuType.List1Selector => GetInteger1(out custom), CharaMakeParams.MenuType.IconSelector => index switch { CustomizeIndex.Face => Get(Faces, HrothgarFaceHack(value), out custom), CustomizeIndex.Hairstyle => Get((face = HrothgarFaceHack(face)).Value < HairByFace.Count ? HairByFace[face.Value] : HairStyles, value, out custom), CustomizeIndex.TailShape => Get(TailEarShapes, value, out custom), CustomizeIndex.FacePaint => Get(FacePaints, value, out custom), CustomizeIndex.LipColor => Get(LipColorsDark, value, out custom), _ => Invalid(out custom), }, CharaMakeParams.MenuType.ColorPicker => index switch { CustomizeIndex.SkinColor => Get(SkinColors, value, out custom), CustomizeIndex.EyeColorLeft => Get(EyeColors, value, out custom), CustomizeIndex.EyeColorRight => Get(EyeColors, value, out custom), CustomizeIndex.HairColor => Get(HairColors, value, out custom), CustomizeIndex.HighlightsColor => Get(HighlightColors, value, out custom), CustomizeIndex.TattooColor => Get(TattooColors, value, out custom), CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom), CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom), _ => Invalid(out custom), }, CharaMakeParams.MenuType.DoubleColorPicker => index switch { CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom), CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom), _ => Invalid(out custom), }, CharaMakeParams.MenuType.IconCheckmark => GetBool(index, value, out custom), CharaMakeParams.MenuType.Percentage => GetInteger0(out custom), CharaMakeParams.MenuType.Checkmark => GetBool(index, value, out custom), _ => Invalid(out custom), }; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public CustomizeData Data(CustomizeIndex index, int idx) => Data(index, idx, CustomizeValue.Zero); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public CustomizeData Data(CustomizeIndex index, int idx, CustomizeValue face) { if (idx >= Count(index, face = HrothgarFaceHack(face))) throw new IndexOutOfRangeException(); switch (Types[(int)index]) { case CharaMakeParams.MenuType.Percentage: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx); case CharaMakeParams.MenuType.ListSelector: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx); case CharaMakeParams.MenuType.List1Selector: return new CustomizeData(index, (CustomizeValue)(idx + 1), 0, (ushort)idx); case CharaMakeParams.MenuType.Checkmark: return new CustomizeData(index, CustomizeValue.Bool(idx != 0), 0, (ushort)idx); } return index switch { CustomizeIndex.Face => Faces[idx], CustomizeIndex.Hairstyle => face < HairByFace.Count ? HairByFace[face.Value][idx] : HairStyles[idx], CustomizeIndex.TailShape => TailEarShapes[idx], CustomizeIndex.FacePaint => FacePaints[idx], CustomizeIndex.FacialFeature1 => idx == 0 ? FacialFeature1[face.Value].Item1 : FacialFeature1[face.Value].Item2, CustomizeIndex.FacialFeature2 => idx == 0 ? FacialFeature2[face.Value].Item1 : FacialFeature2[face.Value].Item2, CustomizeIndex.FacialFeature3 => idx == 0 ? FacialFeature3[face.Value].Item1 : FacialFeature3[face.Value].Item2, CustomizeIndex.FacialFeature4 => idx == 0 ? FacialFeature4[face.Value].Item1 : FacialFeature4[face.Value].Item2, CustomizeIndex.FacialFeature5 => idx == 0 ? FacialFeature5[face.Value].Item1 : FacialFeature5[face.Value].Item2, CustomizeIndex.FacialFeature6 => idx == 0 ? FacialFeature6[face.Value].Item1 : FacialFeature6[face.Value].Item2, CustomizeIndex.FacialFeature7 => idx == 0 ? FacialFeature7[face.Value].Item1 : FacialFeature7[face.Value].Item2, CustomizeIndex.LegacyTattoo => idx == 0 ? LegacyTattoo.Item1 : LegacyTattoo.Item2, CustomizeIndex.SkinColor => SkinColors[idx], CustomizeIndex.EyeColorLeft => EyeColors[idx], CustomizeIndex.EyeColorRight => EyeColors[idx], CustomizeIndex.HairColor => HairColors[idx], CustomizeIndex.HighlightsColor => HighlightColors[idx], CustomizeIndex.TattooColor => TattooColors[idx], CustomizeIndex.LipColor => idx < 96 ? LipColorsDark[idx] : LipColorsLight[idx - 96], CustomizeIndex.FacePaintColor => idx < 96 ? FacePaintColorsDark[idx] : FacePaintColorsLight[idx - 96], _ => new CustomizeData(0, CustomizeValue.Zero), }; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public CharaMakeParams.MenuType Type(CustomizeIndex index) => Types[(int)index]; internal static IReadOnlyDictionary ComputeOrder(CustomizationSet set) { var ret = Enum.GetValues().ToArray(); ret[(int)CustomizeIndex.TattooColor] = CustomizeIndex.EyeColorLeft; ret[(int)CustomizeIndex.EyeColorLeft] = CustomizeIndex.EyeColorRight; ret[(int)CustomizeIndex.EyeColorRight] = CustomizeIndex.TattooColor; var dict = ret.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray()); foreach (var type in Enum.GetValues()) dict.TryAdd(type, Array.Empty()); return dict; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public int Count(CustomizeIndex index) => Count(index, CustomizeValue.Zero); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public int Count(CustomizeIndex index, CustomizeValue face) { if (!IsAvailable(index)) return 0; return Type(index) switch { CharaMakeParams.MenuType.Percentage => 101, CharaMakeParams.MenuType.IconCheckmark => 2, CharaMakeParams.MenuType.Checkmark => 2, _ => index switch { CustomizeIndex.Face => Faces.Count, CustomizeIndex.Hairstyle => (face = HrothgarFaceHack(face)) < HairByFace.Count ? HairByFace[face.Value].Count : HairStyles.Count, CustomizeIndex.SkinColor => SkinColors.Count, CustomizeIndex.EyeColorRight => EyeColors.Count, CustomizeIndex.HairColor => HairColors.Count, CustomizeIndex.HighlightsColor => HighlightColors.Count, CustomizeIndex.TattooColor => TattooColors.Count, CustomizeIndex.Eyebrows => NumEyebrows, CustomizeIndex.EyeColorLeft => EyeColors.Count, CustomizeIndex.EyeShape => NumEyeShapes, CustomizeIndex.Nose => NumNoseShapes, CustomizeIndex.Jaw => NumJawShapes, CustomizeIndex.Mouth => NumMouthShapes, CustomizeIndex.LipColor => LipColorsLight.Count + LipColorsDark.Count, CustomizeIndex.TailShape => TailEarShapes.Count, CustomizeIndex.FacePaint => FacePaints.Count, CustomizeIndex.FacePaintColor => FacePaintColorsLight.Count + FacePaintColorsDark.Count, _ => throw new ArgumentOutOfRangeException(nameof(index), index, null), }, }; } private CustomizeValue HrothgarFaceHack(CustomizeValue value) => Race == Race.Hrothgar && value.Value is > 4 and < 9 ? value - 4 : value; }