diff --git a/Glamourer.GameData/Customization/Customization.cs b/Glamourer.GameData/Customization/Customization.cs deleted file mode 100644 index 8527f3a..0000000 --- a/Glamourer.GameData/Customization/Customization.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Glamourer.Customization; - -// 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 Customization -{ - [FieldOffset(0)] - public readonly CustomizationId Id; - - [FieldOffset(1)] - public readonly byte Value; - - [FieldOffset(2)] - public readonly ushort CustomizeId; - - [FieldOffset(4)] - public readonly uint IconId; - - [FieldOffset(4)] - public readonly uint Color; - - public Customization(CustomizationId id, byte value, uint data = 0, ushort customizeId = 0) - { - Id = id; - Value = value; - IconId = data; - Color = data; - CustomizeId = customizeId; - } -} diff --git a/Glamourer.GameData/Customization/CustomizationData.cs b/Glamourer.GameData/Customization/CustomizationData.cs index 5420e5a..3a943bc 100644 --- a/Glamourer.GameData/Customization/CustomizationData.cs +++ b/Glamourer.GameData/Customization/CustomizationData.cs @@ -1,314 +1,33 @@ -using System; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; +using System.Runtime.InteropServices; namespace Glamourer.Customization; -public unsafe struct Customize +// 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 CustomizationData { - public readonly CustomizeData* Data; + [FieldOffset(0)] + public readonly CustomizationId Id; - public Customize(CustomizeData* data) - => Data = data; + [FieldOffset(1)] + public readonly CustomizationByteValue Value; - public Race Race + [FieldOffset(2)] + public readonly ushort CustomizeId; + + [FieldOffset(4)] + public readonly uint IconId; + + [FieldOffset(4)] + public readonly uint Color; + + public CustomizationData(CustomizationId id, CustomizationByteValue value, uint data = 0, ushort customizeId = 0) { - get => (Race)Data->Data[0]; - set => Data->Data[0] = (byte)value; + Id = id; + Value = value; + IconId = data; + Color = data; + CustomizeId = customizeId; } - - // Skip Unknown Gender - public Gender Gender - { - get => (Gender)(Data->Data[1] + 1); - set => Data->Data[1] = (byte)(value - 1); - } - - public byte BodyType - { - get => Data->Data[2]; - set => Data->Data[2] = value; - } - - public byte Height - { - get => Data->Data[3]; - set => Data->Data[3] = value; - } - - public SubRace Clan - { - get => (SubRace)Data->Data[4]; - set => Data->Data[4] = (byte)value; - } - - public byte Face - { - get => Data->Data[5]; - set => Data->Data[5] = value; - } - - public byte Hairstyle - { - get => Data->Data[6]; - set => Data->Data[6] = value; - } - - public bool HighlightsOn - { - get => Data->Data[7] >> 7 == 1; - set => Data->Data[7] = (byte)(value ? Data->Data[7] | 0x80 : Data->Data[7] & 0x7F); - } - - public byte SkinColor - { - get => Data->Data[8]; - set => Data->Data[8] = value; - } - - public byte EyeColorRight - { - get => Data->Data[9]; - set => Data->Data[9] = value; - } - - public byte HairColor - { - get => Data->Data[10]; - set => Data->Data[10] = value; - } - - public byte HighlightsColor - { - get => Data->Data[11]; - set => Data->Data[11] = value; - } - - public readonly ref struct FacialFeatureStruct - { - private readonly byte* _bitfield; - - public FacialFeatureStruct(byte* data) - => _bitfield = data; - - public bool this[int idx] - { - get => (*_bitfield & (1 << idx)) != 0; - set => Set(idx, value); - } - - public void Clear() - => *_bitfield = 0; - - public void All() - => *_bitfield = 0xFF; - - public void Set(int idx, bool value) - => *_bitfield = (byte)(value ? *_bitfield | (1 << idx) : *_bitfield & ~(1 << idx)); - } - - public FacialFeatureStruct FacialFeatures - => new(Data->Data + 12); - - public byte TattooColor - { - get => Data->Data[13]; - set => Data->Data[13] = value; - } - - public byte Eyebrows - { - get => Data->Data[14]; - set => Data->Data[14] = value; - } - - public byte EyeColorLeft - { - get => Data->Data[15]; - set => Data->Data[15] = value; - } - - public byte EyeShape - { - get => (byte)(Data->Data[16] & 0x7F); - set => Data->Data[16] = (byte)((value & 0x7F) | (Data->Data[16] & 0x80)); - } - - public bool SmallIris - { - get => Data->Data[16] >> 7 == 1; - set => Data->Data[16] = (byte)(value ? Data->Data[16] | 0x80 : Data->Data[16] & 0x7F); - } - - public byte Nose - { - get => Data->Data[17]; - set => Data->Data[17] = value; - } - - public byte Jaw - { - get => Data->Data[18]; - set => Data->Data[18] = value; - } - - public byte Mouth - { - get => (byte)(Data->Data[19] & 0x7F); - set => Data->Data[19] = (byte)((value & 0x7F) | (Data->Data[19] & 0x80)); - } - - public bool Lipstick - { - get => Data->Data[19] >> 7 == 1; - set => Data->Data[19] = (byte)(value ? Data->Data[19] | 0x80 : Data->Data[19] & 0x7F); - } - - public ref byte LipColor - => ref Data->Data[20]; - - public ref byte MuscleMass - => ref Data->Data[21]; - - public ref byte TailShape - => ref Data->Data[22]; - - public ref byte BustSize - => ref Data->Data[23]; - - public byte FacePaint - { - get => (byte)(Data->Data[24] & 0x7F); - set => Data->Data[24] = (byte)((value & 0x7F) | (Data->Data[24] & 0x80)); - } - - public bool FacePaintReversed - { - get => Data->Data[24] >> 7 == 1; - set => Data->Data[24] = (byte)(value ? Data->Data[24] | 0x80 : Data->Data[24] & 0x7F); - } - - public ref byte FacePaintColor - => ref Data->Data[25]; - - public static readonly CustomizeData Default = GenerateDefault(); - public static readonly CustomizeData Empty = new(); - - public byte Get(CustomizationId id) - => id switch - { - CustomizationId.Race => (byte)Race, - CustomizationId.Gender => (byte)Gender, - CustomizationId.BodyType => BodyType, - CustomizationId.Height => Height, - CustomizationId.Clan => (byte)Clan, - CustomizationId.Face => Face, - CustomizationId.Hairstyle => Hairstyle, - CustomizationId.HighlightsOnFlag => Data->Data[7], - CustomizationId.SkinColor => SkinColor, - CustomizationId.EyeColorR => EyeColorRight, - CustomizationId.HairColor => HairColor, - CustomizationId.HighlightColor => HighlightsColor, - CustomizationId.FacialFeaturesTattoos => Data->Data[12], - CustomizationId.TattooColor => TattooColor, - CustomizationId.Eyebrows => Eyebrows, - CustomizationId.EyeColorL => EyeColorLeft, - CustomizationId.EyeShape => EyeShape, - CustomizationId.Nose => Nose, - CustomizationId.Jaw => Jaw, - CustomizationId.Mouth => Mouth, - CustomizationId.LipColor => LipColor, - CustomizationId.MuscleToneOrTailEarLength => MuscleMass, - CustomizationId.TailEarShape => TailShape, - CustomizationId.BustSize => BustSize, - CustomizationId.FacePaint => FacePaint, - CustomizationId.FacePaintColor => FacePaintColor, - _ => throw new ArgumentOutOfRangeException(nameof(id), id, null), - }; - - public void Set(CustomizationId id, byte value) - { - switch (id) - { - // @formatter:off - case CustomizationId.Race: Race = (Race)value; break; - case CustomizationId.Gender: Gender = (Gender)value; break; - case CustomizationId.BodyType: BodyType = value; break; - case CustomizationId.Height: Height = value; break; - case CustomizationId.Clan: Clan = (SubRace)value; break; - case CustomizationId.Face: Face = value; break; - case CustomizationId.Hairstyle: Hairstyle = value; break; - case CustomizationId.HighlightsOnFlag: HighlightsOn = (value & 128) == 128; break; - case CustomizationId.SkinColor: SkinColor = value; break; - case CustomizationId.EyeColorR: EyeColorRight = value; break; - case CustomizationId.HairColor: HairColor = value; break; - case CustomizationId.HighlightColor: HighlightsColor = value; break; - case CustomizationId.FacialFeaturesTattoos: Data->Data[12] = value; break; - case CustomizationId.TattooColor: TattooColor = value; break; - case CustomizationId.Eyebrows: Eyebrows = value; break; - case CustomizationId.EyeColorL: EyeColorLeft = value; break; - case CustomizationId.EyeShape: EyeShape = value; break; - case CustomizationId.Nose: Nose = value; break; - case CustomizationId.Jaw: Jaw = value; break; - case CustomizationId.Mouth: Mouth = value; break; - case CustomizationId.LipColor: LipColor = value; break; - case CustomizationId.MuscleToneOrTailEarLength: MuscleMass = value; break; - case CustomizationId.TailEarShape: TailShape = value; break; - case CustomizationId.BustSize: BustSize = value; break; - case CustomizationId.FacePaint: FacePaint = value; break; - case CustomizationId.FacePaintColor: FacePaintColor = value; break; - default: throw new ArgumentOutOfRangeException(nameof(id), id, null); - // @formatter:on - } - } - - public bool Equals(Customize other) - => CustomizeData.Equals(Data, other.Data); - - public byte this[CustomizationId id] - { - get => Get(id); - set => Set(id, value); - } - - private static CustomizeData GenerateDefault() - { - var ret = new CustomizeData(); - var customize = new Customize(&ret) - { - Race = Race.Hyur, - Gender = Gender.Male, - BodyType = 1, - Height = 50, - Clan = SubRace.Midlander, - Face = 1, - Hairstyle = 1, - HighlightsOn = false, - SkinColor = 1, - EyeColorRight = 1, - HighlightsColor = 1, - TattooColor = 1, - Eyebrows = 1, - EyeColorLeft = 1, - EyeShape = 1, - Nose = 1, - Jaw = 1, - Mouth = 1, - LipColor = 1, - MuscleMass = 50, - TailShape = 1, - BustSize = 50, - FacePaint = 1, - FacePaintColor = 1, - }; - customize.FacialFeatures.Clear(); - - return ret; - } - - public void Load(Customize other) - => Data->Read(other.Data); - - public void Write(IntPtr target) - => Data->Write((void*)target); } diff --git a/Glamourer.GameData/Customization/CustomizationOptions.cs b/Glamourer.GameData/Customization/CustomizationOptions.cs index 49980e7..3e70e83 100644 --- a/Glamourer.GameData/Customization/CustomizationOptions.cs +++ b/Glamourer.GameData/Customization/CustomizationOptions.cs @@ -149,7 +149,7 @@ public partial class CustomizationOptions HighlightColors = _highlightPicker, TattooColors = _tattooColorPicker, LipColorsDark = hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark, - LipColorsLight = hrothgar ? Array.Empty() : _lipColorPickerLight, + LipColorsLight = hrothgar ? Array.Empty() : _lipColorPickerLight, FacePaintColorsDark = _facePaintColorPickerDark, FacePaintColorsLight = _facePaintColorPickerLight, Faces = GetFaces(row), @@ -203,19 +203,19 @@ public partial class CustomizationOptions private readonly CmpFile _cmpFile; // Those values are shared between all races. - private readonly Customization[] _highlightPicker; - private readonly Customization[] _eyeColorPicker; - private readonly Customization[] _facePaintColorPickerDark; - private readonly Customization[] _facePaintColorPickerLight; - private readonly Customization[] _lipColorPickerDark; - private readonly Customization[] _lipColorPickerLight; - private readonly Customization[] _tattooColorPicker; + private readonly CustomizationData[] _highlightPicker; + private readonly CustomizationData[] _eyeColorPicker; + private readonly CustomizationData[] _facePaintColorPickerDark; + private readonly CustomizationData[] _facePaintColorPickerLight; + private readonly CustomizationData[] _lipColorPickerDark; + private readonly CustomizationData[] _lipColorPickerLight; + private readonly CustomizationData[] _tattooColorPicker; private readonly CustomizationOptions _options; - private Customization[] CreateColorPicker(CustomizationId id, int offset, int num, bool light = false) + private CustomizationData[] CreateColorPicker(CustomizationId id, int offset, int num, bool light = false) => _cmpFile.GetSlice(offset, num) - .Select((c, i) => new Customization(id, (byte)(light ? 128 + i : 0 + i), c, (ushort)(offset + i))) + .Select((c, i) => new CustomizationData(id, (CustomizationByteValue)(light ? 128 + i : 0 + i), c, (ushort)(offset + i))) .ToArray(); @@ -227,12 +227,12 @@ public partial class CustomizationOptions return; } - var tmp = new IReadOnlyList[set.Faces.Count + 1]; + var tmp = new IReadOnlyList[set.Faces.Count + 1]; tmp[0] = set.HairStyles; for (var i = 1; i <= set.Faces.Count; ++i) { - bool Valid(Customization c) + bool Valid(CustomizationData c) { var data = _customizeSheet.GetRow(c.CustomizeId)?.Unknown6 ?? 0; return data == 0 || data == i + set.Faces.Count; @@ -295,13 +295,15 @@ public partial class CustomizationOptions private static void SetFacialFeatures(CustomizationSet set, CharaMakeParams row) { var count = set.Faces.Count; - var featureDict = new List>(count); + var featureDict = new List>(count); for (var i = 0; i < count; ++i) { - var legacyTattoo = new Customization(CustomizationId.FacialFeaturesTattoos, 1 << 7, 137905, (ushort)((i + 1) * 8)); + var legacyTattoo = new CustomizationData(CustomizationId.FacialFeaturesTattoos, (CustomizationByteValue)(1 << 7), 137905, + (ushort)((i + 1) * 8)); featureDict.Add(row.FacialFeatureByFace[i].Icons.Select((val, idx) - => new Customization(CustomizationId.FacialFeaturesTattoos, (byte)(1 << idx), val, (ushort)(i * 8 + idx))) + => new CustomizationData(CustomizationId.FacialFeaturesTattoos, (CustomizationByteValue)(1 << idx), val, + (ushort)(i * 8 + idx))) .Append(legacyTattoo) .ToArray()); } @@ -346,7 +348,7 @@ public partial class CustomizationOptions } // Obtain available skin and hair colors for the given subrace and gender. - private (Customization[], Customization[]) GetColors(SubRace race, Gender gender) + private (CustomizationData[], CustomizationData[]) GetColors(SubRace race, Gender gender) { if (race is > SubRace.Veena or SubRace.Unknown) throw new ArgumentOutOfRangeException(nameof(race), race, null); @@ -359,11 +361,11 @@ public partial class CustomizationOptions } // Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender. - private Customization[] GetHairStyles(SubRace race, Gender gender) + private CustomizationData[] 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); + var hairList = new List(row.Unknown30); // Hairstyles can be found starting at Unknown66. for (var i = 0; i < row.Unknown30; ++i) { @@ -376,20 +378,21 @@ public partial class CustomizationOptions // Hair Row from CustomizeSheet might not be set in case of unlockable hair. var hairRow = _customizeSheet.GetRow(customizeIdx); hairList.Add(hairRow != null - ? new Customization(CustomizationId.Hairstyle, hairRow.FeatureID, hairRow.Icon, (ushort)hairRow.RowId) - : new Customization(CustomizationId.Hairstyle, (byte)i, customizeIdx)); + ? new CustomizationData(CustomizationId.Hairstyle, (CustomizationByteValue)hairRow.FeatureID, hairRow.Icon, + (ushort)hairRow.RowId) + : new CustomizationData(CustomizationId.Hairstyle, (CustomizationByteValue)i, customizeIdx)); } return hairList.ToArray(); } // Get Features. - private Customization FromValueAndIndex(CustomizationId id, uint value, int index) + private CustomizationData FromValueAndIndex(CustomizationId id, uint value, int index) { var row = _customizeSheet.GetRow(value); return row == null - ? new Customization(id, (byte)(index + 1), value) - : new Customization(id, row.FeatureID, row.Icon, (ushort)row.RowId); + ? new CustomizationData(id, (CustomizationByteValue)(index + 1), value) + : new CustomizationData(id, (CustomizationByteValue)row.FeatureID, row.Icon, (ushort)row.RowId); } // Get List sizes. @@ -400,10 +403,10 @@ public partial class CustomizationOptions } // Get face paints from the hair sheet via reflection. - private Customization[] GetFacePaints(SubRace race, Gender gender) + private CustomizationData[] GetFacePaints(SubRace race, Gender gender) { var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; - var paintList = new List(row.Unknown37); + var paintList = new List(row.Unknown37); // Number of available face paints is at Unknown37. for (var i = 0; i < row.Unknown37; ++i) @@ -419,29 +422,30 @@ public partial class CustomizationOptions var paintRow = _customizeSheet.GetRow(customizeIdx); // Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints. paintList.Add(paintRow != null - ? new Customization(CustomizationId.FacePaint, paintRow.FeatureID, paintRow.Icon, (ushort)paintRow.RowId) - : new Customization(CustomizationId.FacePaint, (byte)i, customizeIdx)); + ? new CustomizationData(CustomizationId.FacePaint, (CustomizationByteValue)paintRow.FeatureID, paintRow.Icon, + (ushort)paintRow.RowId) + : new CustomizationData(CustomizationId.FacePaint, (CustomizationByteValue)i, customizeIdx)); } return paintList.ToArray(); } // Specific icons for tails or ears. - private Customization[] GetTailEarShapes(CharaMakeParams row) + private CustomizationData[] GetTailEarShapes(CharaMakeParams row) => row.Menus.Cast().FirstOrDefault(m => m!.Value.Customization == CustomizationId.TailEarShape)?.Values .Select((v, i) => FromValueAndIndex(CustomizationId.TailEarShape, v, i)).ToArray() - ?? Array.Empty(); + ?? Array.Empty(); // Specific icons for faces. - private Customization[] GetFaces(CharaMakeParams row) + private CustomizationData[] GetFaces(CharaMakeParams row) => row.Menus.Cast().FirstOrDefault(m => m!.Value.Customization == CustomizationId.Face)?.Values .Select((v, i) => FromValueAndIndex(CustomizationId.Face, v, i)).ToArray() - ?? Array.Empty(); + ?? Array.Empty(); // Specific icons for Hrothgar patterns. - private Customization[] HrothgarFurPattern(CharaMakeParams row) + private CustomizationData[] HrothgarFurPattern(CharaMakeParams row) => row.Menus.Cast().FirstOrDefault(m => m!.Value.Customization == CustomizationId.LipColor)?.Values .Select((v, i) => FromValueAndIndex(CustomizationId.LipColor, v, i)).ToArray() - ?? Array.Empty(); + ?? Array.Empty(); } } diff --git a/Glamourer.GameData/Customization/CustomizationSet.cs b/Glamourer.GameData/Customization/CustomizationSet.cs index 843bce2..69b0694 100644 --- a/Glamourer.GameData/Customization/CustomizationSet.cs +++ b/Glamourer.GameData/Customization/CustomizationSet.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using Microsoft.VisualBasic; +using OtterGui; using Penumbra.GameData.Enums; namespace Glamourer.Customization; @@ -15,16 +15,15 @@ public class CustomizationSet { Gender = gender; Clan = clan; - _settingAvailable = clan.ToRace() == Race.Hrothgar && gender == Gender.Female + Race = clan.ToRace(); + _settingAvailable = Race == Race.Hrothgar && gender == Gender.Female ? 0u : DefaultAvailable; } public Gender Gender { get; } public SubRace Clan { get; } - - public Race Race - => Clan.ToRace(); + public Race Race { get; } private uint _settingAvailable; @@ -34,11 +33,18 @@ public class CustomizationSet public bool IsAvailable(CustomizationId id) => (_settingAvailable & (1u << (int)id)) != 0; - 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; } + private const uint DefaultAvailable = + (1u << (int)CustomizationId.Height) + | (1u << (int)CustomizationId.Hairstyle) + | (1u << (int)CustomizationId.SkinColor) + | (1u << (int)CustomizationId.EyeColorR) + | (1u << (int)CustomizationId.EyeColorL) + | (1u << (int)CustomizationId.HairColor) + | (1u << (int)CustomizationId.HighlightColor) + | (1u << (int)CustomizationId.FacialFeaturesTattoos) + | (1u << (int)CustomizationId.TattooColor) + | (1u << (int)CustomizationId.LipColor) + | (1u << (int)CustomizationId.Height); public string ToHumanReadable(Customize customizationData) { @@ -49,45 +55,53 @@ public class CustomizationSet return sb.ToString(); } + // Meta + public IReadOnlyList OptionName { get; internal set; } = null!; - public IReadOnlyList OptionName { get; internal set; } = null!; - 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> FeaturesTattoos { get; internal set; } = null!; - public IReadOnlyList FacePaints { get; internal init; } = null!; - - 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 string Option(CustomizationId id) + => OptionName[(int)id]; public IReadOnlyList Types { get; internal set; } = null!; public IReadOnlyDictionary Order { get; internal set; } = null!; - public string Option(CustomizationId id) - => OptionName[(int)id]; + // 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; } - public Customization FacialFeature(int faceIdx, int idx) + + // 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> FeaturesTattoos { get; internal set; } = null!; + public IReadOnlyList FacePaints { get; internal init; } = null!; + + public CustomizationData FacialFeature(CustomizationByteValue face, int idx) { - faceIdx = HrothgarFaceHack((byte) faceIdx) - 1; - if (faceIdx < FeaturesTattoos.Count) - return FeaturesTattoos[HrothgarFaceHack((byte)faceIdx)][idx]; - - return FeaturesTattoos[0][idx]; + face = HrothgarFaceHack(face); + var faceIdx = Faces.IndexOf(p => p.Value == face); + return FeaturesTattoos[faceIdx != -1 ? faceIdx : 0][idx]; } - private byte HrothgarFaceHack(byte value) - => value is > 4 and < 9 && Clan.ToRace() == Race.Hrothgar ? (byte)(value - 4) : value; - public int DataByValue(CustomizationId id, byte value, out Customization? custom) + // 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 int DataByValue(CustomizationId id, CustomizationByteValue value, out CustomizationData? custom) { var type = id.ToType(); custom = null; @@ -95,16 +109,16 @@ public class CustomizationSet { if (value < Count(id)) { - custom = new Customization(id, value, 0, value); - return value; + custom = new CustomizationData(id, value, 0, value.Value); + return value.Value; } return -1; } - int Get(IEnumerable list, byte v, ref Customization? output) + int Get(IEnumerable list, CustomizationByteValue v, ref CustomizationData? output) { - var (val, idx) = list.Cast().Select((c, i) => (c, i)).FirstOrDefault(c => c.c!.Value.Value == v); + var (val, idx) = list.Cast().WithIndex().FirstOrDefault(p => p.Item1!.Value.Value == v); if (val == null) return -1; @@ -132,21 +146,24 @@ public class CustomizationSet }; } - public Customization Data(CustomizationId id, int idx, byte face = 0) + public CustomizationData Data(CustomizationId id, int idx) + => Data(id, idx, CustomizationByteValue.Zero); + + public CustomizationData Data(CustomizationId id, int idx, CustomizationByteValue face) { - if (idx > Count(id, face = HrothgarFaceHack(face))) + if (idx >= Count(id, face = HrothgarFaceHack(face))) throw new IndexOutOfRangeException(); switch (id.ToType()) { - case CharaMakeParams.MenuType.Percentage: return new Customization(id, (byte)idx, 0, (ushort)idx); - case CharaMakeParams.MenuType.ListSelector: return new Customization(id, (byte)idx, 0, (ushort)idx); + case CharaMakeParams.MenuType.Percentage: return new CustomizationData(id, (CustomizationByteValue)idx, 0, (ushort)idx); + case CharaMakeParams.MenuType.ListSelector: return new CustomizationData(id, (CustomizationByteValue)idx, 0, (ushort)idx); } return id switch { CustomizationId.Face => Faces[idx], - CustomizationId.Hairstyle => face < HairByFace.Count ? HairByFace[face][idx] : HairStyles[idx], + CustomizationId.Hairstyle => face < HairByFace.Count ? HairByFace[face.Value][idx] : HairStyles[idx], CustomizationId.TailEarShape => TailEarShapes[idx], CustomizationId.FacePaint => FacePaints[idx], CustomizationId.FacialFeaturesTattoos => FeaturesTattoos[0][idx], @@ -159,7 +176,7 @@ public class CustomizationSet CustomizationId.TattooColor => TattooColors[idx], CustomizationId.LipColor => idx < 96 ? LipColorsDark[idx] : LipColorsLight[idx - 96], CustomizationId.FacePaintColor => idx < 96 ? FacePaintColorsDark[idx] : FacePaintColorsLight[idx - 96], - _ => new Customization(0, 0), + _ => new CustomizationData(0, CustomizationByteValue.Zero), }; } @@ -179,7 +196,10 @@ public class CustomizationSet return dict; } - public int Count(CustomizationId id, byte face = 0) + public int Count(CustomizationId id) + => Count(id, CustomizationByteValue.Zero); + + public int Count(CustomizationId id, CustomizationByteValue face) { if (!IsAvailable(id)) return 0; @@ -190,7 +210,7 @@ public class CustomizationSet return id switch { CustomizationId.Face => Faces.Count, - CustomizationId.Hairstyle => (face = HrothgarFaceHack(face)) < HairByFace.Count ? HairByFace[face].Count : 0, + CustomizationId.Hairstyle => (face = HrothgarFaceHack(face)) < HairByFace.Count ? HairByFace[face.Value].Count : 0, CustomizationId.HighlightsOnFlag => 2, CustomizationId.SkinColor => SkinColors.Count, CustomizationId.EyeColorR => EyeColors.Count, @@ -212,16 +232,6 @@ public class CustomizationSet }; } - private const uint DefaultAvailable = - (1u << (int)CustomizationId.Height) - | (1u << (int)CustomizationId.Hairstyle) - | (1u << (int)CustomizationId.SkinColor) - | (1u << (int)CustomizationId.EyeColorR) - | (1u << (int)CustomizationId.EyeColorL) - | (1u << (int)CustomizationId.HairColor) - | (1u << (int)CustomizationId.HighlightColor) - | (1u << (int)CustomizationId.FacialFeaturesTattoos) - | (1u << (int)CustomizationId.TattooColor) - | (1u << (int)CustomizationId.LipColor) - | (1u << (int)CustomizationId.Height); + private CustomizationByteValue HrothgarFaceHack(CustomizationByteValue value) + => Race == Race.Hrothgar && value.Value is > 4 and < 9 ? value - 4 : value; } diff --git a/Glamourer.GameData/Customization/Customize.cs b/Glamourer.GameData/Customization/Customize.cs new file mode 100644 index 0000000..3a708cd --- /dev/null +++ b/Glamourer.GameData/Customization/Customize.cs @@ -0,0 +1,358 @@ +using System; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Customization; + +public record struct CustomizationByteValue(byte Value) +{ + public static readonly CustomizationByteValue Zero = new(0); + + public static explicit operator CustomizationByteValue(byte value) + => new(value); + + public static CustomizationByteValue operator ++(CustomizationByteValue v) + => new(++v.Value); + + public static CustomizationByteValue operator --(CustomizationByteValue v) + => new(--v.Value); + + public static bool operator <(CustomizationByteValue v, int count) + => v.Value < count; + + public static bool operator >(CustomizationByteValue v, int count) + => v.Value > count; + + public static CustomizationByteValue operator +(CustomizationByteValue v, int rhs) + => new((byte)(v.Value + rhs)); + + public static CustomizationByteValue operator -(CustomizationByteValue v, int rhs) + => new((byte)(v.Value - rhs)); + + public override string ToString() + => Value.ToString(); +} + +public unsafe struct Customize +{ + public readonly CustomizeData* Data; + + public Customize(CustomizeData* data) + => Data = data; + + public Race Race + { + get => (Race)Data->Data[0]; + set => Data->Data[0] = (byte)value; + } + + // Skip Unknown Gender + public Gender Gender + { + get => (Gender)(Data->Data[1] + 1); + set => Data->Data[1] = (byte)(value - 1); + } + + public CustomizationByteValue BodyType + { + get => (CustomizationByteValue)Data->Data[2]; + set => Data->Data[2] = value.Value; + } + + public CustomizationByteValue Height + { + get => (CustomizationByteValue)Data->Data[3]; + set => Data->Data[3] = value.Value; + } + + public SubRace Clan + { + get => (SubRace)Data->Data[4]; + set => Data->Data[4] = (byte)value; + } + + public CustomizationByteValue Face + { + get => (CustomizationByteValue)Data->Data[5]; + set => Data->Data[5] = value.Value; + } + + public CustomizationByteValue Hairstyle + { + get => (CustomizationByteValue)Data->Data[6]; + set => Data->Data[6] = value.Value; + } + + public bool HighlightsOn + { + get => Data->Data[7] >> 7 == 1; + set => Data->Data[7] = (byte)(value ? Data->Data[7] | 0x80 : Data->Data[7] & 0x7F); + } + + public CustomizationByteValue SkinColor + { + get => (CustomizationByteValue)Data->Data[8]; + set => Data->Data[8] = value.Value; + } + + public CustomizationByteValue EyeColorRight + { + get => (CustomizationByteValue)Data->Data[9]; + set => Data->Data[9] = value.Value; + } + + public CustomizationByteValue HairColor + { + get => (CustomizationByteValue)Data->Data[10]; + set => Data->Data[10] = value.Value; + } + + public CustomizationByteValue HighlightsColor + { + get => (CustomizationByteValue)Data->Data[11]; + set => Data->Data[11] = value.Value; + } + + public readonly ref struct FacialFeatureStruct + { + private readonly byte* _bitfield; + + public FacialFeatureStruct(byte* data) + => _bitfield = data; + + public bool this[int idx] + { + get => (*_bitfield & (1 << idx)) != 0; + set => Set(idx, value); + } + + public void Clear() + => *_bitfield = 0; + + public void All() + => *_bitfield = 0xFF; + + public void Set(int idx, bool value) + => *_bitfield = (byte)(value ? *_bitfield | (1 << idx) : *_bitfield & ~(1 << idx)); + } + + public FacialFeatureStruct FacialFeatures + => new(Data->Data + 12); + + public CustomizationByteValue TattooColor + { + get => (CustomizationByteValue)Data->Data[13]; + set => Data->Data[13] = value.Value; + } + + public CustomizationByteValue Eyebrows + { + get => (CustomizationByteValue)Data->Data[14]; + set => Data->Data[14] = value.Value; + } + + public CustomizationByteValue EyeColorLeft + { + get => (CustomizationByteValue)Data->Data[15]; + set => Data->Data[15] = value.Value; + } + + public CustomizationByteValue EyeShape + { + get => (CustomizationByteValue)(Data->Data[16] & 0x7F); + set => Data->Data[16] = (byte)((value.Value & 0x7F) | (Data->Data[16] & 0x80)); + } + + public bool SmallIris + { + get => Data->Data[16] >> 7 == 1; + set => Data->Data[16] = (byte)(value ? Data->Data[16] | 0x80 : Data->Data[16] & 0x7F); + } + + public CustomizationByteValue Nose + { + get => (CustomizationByteValue)Data->Data[17]; + set => Data->Data[17] = value.Value; + } + + public CustomizationByteValue Jaw + { + get => (CustomizationByteValue)Data->Data[18]; + set => Data->Data[18] = value.Value; + } + + public CustomizationByteValue Mouth + { + get => (CustomizationByteValue)(Data->Data[19] & 0x7F); + set => Data->Data[19] = (byte)((value.Value & 0x7F) | (Data->Data[19] & 0x80)); + } + + public bool Lipstick + { + get => Data->Data[19] >> 7 == 1; + set => Data->Data[19] = (byte)(value ? Data->Data[19] | 0x80 : Data->Data[19] & 0x7F); + } + + public CustomizationByteValue LipColor + { + get => (CustomizationByteValue)Data->Data[20]; + set => Data->Data[20] = value.Value; + } + + public CustomizationByteValue MuscleMass + { + get => (CustomizationByteValue)Data->Data[21]; + set => Data->Data[21] = value.Value; + } + + public CustomizationByteValue TailShape + { + get => (CustomizationByteValue)Data->Data[22]; + set => Data->Data[22] = value.Value; + } + + public CustomizationByteValue BustSize + { + get => (CustomizationByteValue)Data->Data[23]; + set => Data->Data[23] = value.Value; + } + + public CustomizationByteValue FacePaint + { + get => (CustomizationByteValue)(Data->Data[24] & 0x7F); + set => Data->Data[24] = (byte)((value.Value & 0x7F) | (Data->Data[24] & 0x80)); + } + + public bool FacePaintReversed + { + get => Data->Data[24] >> 7 == 1; + set => Data->Data[24] = (byte)(value ? Data->Data[24] | 0x80 : Data->Data[24] & 0x7F); + } + + public CustomizationByteValue FacePaintColor + { + get => (CustomizationByteValue)Data->Data[25]; + set => Data->Data[25] = value.Value; + } + + public static readonly CustomizeData Default = GenerateDefault(); + public static readonly CustomizeData Empty = new(); + + public CustomizationByteValue Get(CustomizationId id) + => id switch + { + CustomizationId.Race => (CustomizationByteValue)(byte)Race, + CustomizationId.Gender => (CustomizationByteValue)(byte)Gender, + CustomizationId.BodyType => BodyType, + CustomizationId.Height => Height, + CustomizationId.Clan => (CustomizationByteValue)(byte)Clan, + CustomizationId.Face => Face, + CustomizationId.Hairstyle => Hairstyle, + CustomizationId.HighlightsOnFlag => (CustomizationByteValue)Data->Data[7], + CustomizationId.SkinColor => SkinColor, + CustomizationId.EyeColorR => EyeColorRight, + CustomizationId.HairColor => HairColor, + CustomizationId.HighlightColor => HighlightsColor, + CustomizationId.FacialFeaturesTattoos => (CustomizationByteValue)Data->Data[12], + CustomizationId.TattooColor => TattooColor, + CustomizationId.Eyebrows => Eyebrows, + CustomizationId.EyeColorL => EyeColorLeft, + CustomizationId.EyeShape => EyeShape, + CustomizationId.Nose => Nose, + CustomizationId.Jaw => Jaw, + CustomizationId.Mouth => Mouth, + CustomizationId.LipColor => LipColor, + CustomizationId.MuscleToneOrTailEarLength => MuscleMass, + CustomizationId.TailEarShape => TailShape, + CustomizationId.BustSize => BustSize, + CustomizationId.FacePaint => FacePaint, + CustomizationId.FacePaintColor => FacePaintColor, + _ => throw new ArgumentOutOfRangeException(nameof(id), id, null), + }; + + public void Set(CustomizationId id, CustomizationByteValue value) + { + switch (id) + { + // @formatter:off + case CustomizationId.Race: Race = (Race)value.Value; break; + case CustomizationId.Gender: Gender = (Gender)value.Value; break; + case CustomizationId.BodyType: BodyType = value; break; + case CustomizationId.Height: Height = value; break; + case CustomizationId.Clan: Clan = (SubRace)value.Value; break; + case CustomizationId.Face: Face = value; break; + case CustomizationId.Hairstyle: Hairstyle = value; break; + case CustomizationId.HighlightsOnFlag: HighlightsOn = (value.Value & 128) == 128; break; + case CustomizationId.SkinColor: SkinColor = value; break; + case CustomizationId.EyeColorR: EyeColorRight = value; break; + case CustomizationId.HairColor: HairColor = value; break; + case CustomizationId.HighlightColor: HighlightsColor = value; break; + case CustomizationId.FacialFeaturesTattoos: Data->Data[12] = value.Value; break; + case CustomizationId.TattooColor: TattooColor = value; break; + case CustomizationId.Eyebrows: Eyebrows = value; break; + case CustomizationId.EyeColorL: EyeColorLeft = value; break; + case CustomizationId.EyeShape: EyeShape = value; break; + case CustomizationId.Nose: Nose = value; break; + case CustomizationId.Jaw: Jaw = value; break; + case CustomizationId.Mouth: Mouth = value; break; + case CustomizationId.LipColor: LipColor = value; break; + case CustomizationId.MuscleToneOrTailEarLength: MuscleMass = value; break; + case CustomizationId.TailEarShape: TailShape = value; break; + case CustomizationId.BustSize: BustSize = value; break; + case CustomizationId.FacePaint: FacePaint = value; break; + case CustomizationId.FacePaintColor: FacePaintColor = value; break; + default: throw new ArgumentOutOfRangeException(nameof(id), id, null); + // @formatter:on + } + } + + public bool Equals(Customize other) + => CustomizeData.Equals(Data, other.Data); + + public CustomizationByteValue this[CustomizationId id] + { + get => Get(id); + set => Set(id, value); + } + + private static CustomizeData GenerateDefault() + { + var ret = new CustomizeData(); + var customize = new Customize(&ret) + { + Race = Race.Hyur, + Gender = Gender.Male, + BodyType = (CustomizationByteValue)1, + Height = (CustomizationByteValue)50, + Clan = SubRace.Midlander, + Face = (CustomizationByteValue)1, + Hairstyle = (CustomizationByteValue)1, + HighlightsOn = false, + SkinColor = (CustomizationByteValue)1, + EyeColorRight = (CustomizationByteValue)1, + HighlightsColor = (CustomizationByteValue)1, + TattooColor = (CustomizationByteValue)1, + Eyebrows = (CustomizationByteValue)1, + EyeColorLeft = (CustomizationByteValue)1, + EyeShape = (CustomizationByteValue)1, + Nose = (CustomizationByteValue)1, + Jaw = (CustomizationByteValue)1, + Mouth = (CustomizationByteValue)1, + LipColor = (CustomizationByteValue)1, + MuscleMass = (CustomizationByteValue)50, + TailShape = (CustomizationByteValue)1, + BustSize = (CustomizationByteValue)50, + FacePaint = (CustomizationByteValue)1, + FacePaintColor = (CustomizationByteValue)1, + }; + customize.FacialFeatures.Clear(); + + return ret; + } + + public void Load(Customize other) + => Data->Read(other.Data); + + public void Write(IntPtr target) + => Data->Write((void*)target); +} diff --git a/Glamourer.GameData/GameData.cs b/Glamourer.GameData/GameData.cs index b74f5a1..e361b57 100644 --- a/Glamourer.GameData/GameData.cs +++ b/Glamourer.GameData/GameData.cs @@ -6,6 +6,7 @@ using Dalamud.Data; using Glamourer.Structs; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Item = Glamourer.Structs.Item; using Stain = Glamourer.Structs.Stain; @@ -13,7 +14,7 @@ namespace Glamourer; public static class GameData { - private static Dictionary? _stains; + private static Dictionary? _stains; private static Dictionary>? _itemsBySlot; private static Dictionary? _jobs; private static Dictionary? _jobGroups; @@ -26,13 +27,13 @@ public static class GameData public static ModelData Models(DataManager dataManager) => _models ??= new ModelData(dataManager); - public static IReadOnlyDictionary Stains(DataManager dataManager) + public static IReadOnlyDictionary Stains(DataManager dataManager) { if (_stains != null) return _stains; var sheet = dataManager.GetExcelSheet()!; - _stains = sheet.Where(s => s.Color != 0).ToDictionary(s => (byte)s.RowId, s => new Stain((byte)s.RowId, s)); + _stains = sheet.Where(s => s.Color != 0).ToDictionary(s => (StainId)s.RowId, s => new Stain((byte)s.RowId, s)); return _stains; } diff --git a/Glamourer.sln b/Glamourer.sln index 381596c..e573beb 100644 --- a/Glamourer.sln +++ b/Glamourer.sln @@ -20,61 +20,25 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A5439F6B-83C1-4078-9371-354A147FF554}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A5439F6B-83C1-4078-9371-354A147FF554}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A5439F6B-83C1-4078-9371-354A147FF554}.Debug|x64.ActiveCfg = Debug|Any CPU - {A5439F6B-83C1-4078-9371-354A147FF554}.Debug|x64.Build.0 = Debug|Any CPU - {A5439F6B-83C1-4078-9371-354A147FF554}.Debug|x86.ActiveCfg = Debug|Any CPU - {A5439F6B-83C1-4078-9371-354A147FF554}.Debug|x86.Build.0 = Debug|Any CPU {A5439F6B-83C1-4078-9371-354A147FF554}.Release|Any CPU.ActiveCfg = Release|Any CPU {A5439F6B-83C1-4078-9371-354A147FF554}.Release|Any CPU.Build.0 = Release|Any CPU - {A5439F6B-83C1-4078-9371-354A147FF554}.Release|x64.ActiveCfg = Release|Any CPU - {A5439F6B-83C1-4078-9371-354A147FF554}.Release|x64.Build.0 = Release|Any CPU - {A5439F6B-83C1-4078-9371-354A147FF554}.Release|x86.ActiveCfg = Release|Any CPU - {A5439F6B-83C1-4078-9371-354A147FF554}.Release|x86.Build.0 = Release|Any CPU {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|x64.ActiveCfg = Debug|Any CPU - {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|x64.Build.0 = Debug|Any CPU - {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|x86.ActiveCfg = Debug|Any CPU - {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|x86.Build.0 = Debug|Any CPU {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|Any CPU.ActiveCfg = Release|Any CPU {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|Any CPU.Build.0 = Release|Any CPU - {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|x64.ActiveCfg = Release|Any CPU - {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|x64.Build.0 = Release|Any CPU - {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|x86.ActiveCfg = Release|Any CPU - {51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|x86.Build.0 = Release|Any CPU {9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Debug|x64.ActiveCfg = Debug|Any CPU - {9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Debug|x64.Build.0 = Debug|Any CPU - {9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Debug|x86.ActiveCfg = Debug|Any CPU - {9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Debug|x86.Build.0 = Debug|Any CPU {9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Release|Any CPU.ActiveCfg = Release|Any CPU {9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Release|Any CPU.Build.0 = Release|Any CPU - {9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Release|x64.ActiveCfg = Release|Any CPU - {9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Release|x64.Build.0 = Release|Any CPU - {9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Release|x86.ActiveCfg = Release|Any CPU - {9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Release|x86.Build.0 = Release|Any CPU {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Debug|x64.ActiveCfg = Debug|Any CPU - {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Debug|x64.Build.0 = Debug|Any CPU - {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Debug|x86.ActiveCfg = Debug|Any CPU - {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Debug|x86.Build.0 = Debug|Any CPU {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Release|Any CPU.Build.0 = Release|Any CPU - {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Release|x64.ActiveCfg = Release|Any CPU - {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Release|x64.Build.0 = Release|Any CPU - {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Release|x86.ActiveCfg = Release|Any CPU - {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Glamourer/Glamourer.cs b/Glamourer/Glamourer.cs index b7eb16f..f73074b 100644 --- a/Glamourer/Glamourer.cs +++ b/Glamourer/Glamourer.cs @@ -1,17 +1,13 @@ -using System; -using System.Reflection; +using System.Reflection; using Dalamud.Game.Command; -using Dalamud.Hooking; using Dalamud.Interface.Windowing; -using Dalamud.Logging; using Dalamud.Plugin; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Api; using Glamourer.Customization; using Glamourer.Gui; using Glamourer.Interop; using Glamourer.State; +using OtterGui.Log; using Penumbra.GameData; namespace Glamourer; @@ -32,6 +28,7 @@ public class Glamourer : IDalamudPlugin public static GlamourerConfig Config = null!; + public static Logger Log = null!; public static IObjectIdentifier Identifier = null!; public static PenumbraAttach Penumbra = null!; @@ -55,6 +52,7 @@ public class Glamourer : IDalamudPlugin try { Dalamud.Initialize(pluginInterface); + Log = new Logger(); Customization = CustomizationManager.Create(Dalamud.PluginInterface, Dalamud.GameData, Dalamud.ClientState.ClientLanguage); RestrictedGear = GameData.RestrictedGear(Dalamud.GameData); diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs new file mode 100644 index 0000000..9cf0281 --- /dev/null +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs @@ -0,0 +1,65 @@ +using System.Numerics; +using Glamourer.Customization; +using ImGuiNET; +using OtterGui.Raii; + +namespace Glamourer.Gui.Customization; + +internal partial class CustomizationDrawer +{ + private const string ColorPickerPopupName = "ColorPicker"; + + private void DrawColorPicker(CustomizationId id) + { + using var _ = SetId(id); + var (current, custom) = GetCurrentCustomization(id); + var color = ImGui.ColorConvertU32ToFloat4(custom.Color); + + // Print 1-based index instead of 0. + if (ImGui.ColorButton($"{current + 1}##color", color, ImGuiColorEditFlags.None, _framedIconSize)) + ImGui.OpenPopup(ColorPickerPopupName); + + ImGui.SameLine(); + + using (var group = ImRaii.Group()) + { + DataInputInt(current); + ImGui.TextUnformatted(_currentOption); + } + + DrawColorPickerPopup(); + } + + private void DrawColorPickerPopup() + { + using var popup = ImRaii.Popup(ColorPickerPopupName, ImGuiWindowFlags.AlwaysAutoResize); + if (!popup) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameRounding, 0); + for (var i = 0; i < _currentCount; ++i) + { + var custom = _set.Data(_currentId, i, _customize[CustomizationId.Face]); + if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color))) + { + UpdateValue(custom.Value); + ImGui.CloseCurrentPopup(); + } + + if (i % 8 != 7) + ImGui.SameLine(); + } + } + + // Obtain the current customization and print a warning if it is not known. + private (int, CustomizationData) GetCurrentCustomization(CustomizationId id) + { + var current = _set.DataByValue(id, _customize[id], out var custom); + if (!_set.IsAvailable(id) || current >= 0) + return (current, custom!.Value); + + Glamourer.Log.Warning($"Read invalid customization value {_customize[id]} for {id}."); + return (0, _set.Data(id, 0)); + } +} diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs b/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs new file mode 100644 index 0000000..fb712db --- /dev/null +++ b/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using Dalamud.Interface; +using Glamourer.Customization; +using Glamourer.Util; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Enums; + +namespace Glamourer.Gui.Customization; + +internal partial class CustomizationDrawer +{ + private void DrawRaceGenderSelector() + { + DrawGenderSelector(); + ImGui.SameLine(); + using var group = ImRaii.Group(); + DrawRaceCombo(); + var gender = Glamourer.Customization.GetName(CustomName.Gender); + var clan = Glamourer.Customization.GetName(CustomName.Clan); + ImGui.TextUnformatted($"{gender} & {clan}"); + } + + private void DrawGenderSelector() + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + var icon = _customize.Gender == Gender.Male ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus; + var restricted = _customize.Race == Race.Hrothgar; + if (restricted) + icon = FontAwesomeIcon.MarsDouble; + + if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, restricted, true)) + return; + + var gender = _customize.Gender == Gender.Male ? Gender.Female : Gender.Male; + if (!_customize.ChangeGender(_equip, gender)) + return; + + foreach (var actor in _actors.Where(a => a)) + Glamourer.Penumbra.RedrawObject(actor.Character, RedrawType.Redraw, false); + } + + private void DrawRaceCombo() + { + ImGui.SetNextItemWidth(_raceSelectorWidth); + using var combo = ImRaii.Combo("##subRaceCombo", _customize.ClanName()); + if (!combo) + return; + + foreach (var subRace in Enum.GetValues().Skip(1)) // Skip Unknown + { + if (!ImGui.Selectable(CustomizeExtensions.ClanName(subRace, _customize.Gender), subRace == _customize.Clan) + || !_customize.ChangeRace(_equip, subRace)) + continue; + + foreach (var actor in _actors.Where(a => a && a.DrawObject)) + Glamourer.Penumbra.RedrawObject(actor.Character, RedrawType.Redraw, false); + } + } +} diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs new file mode 100644 index 0000000..b83288d --- /dev/null +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs @@ -0,0 +1,109 @@ +using System; +using System.Linq; +using System.Numerics; +using Glamourer.Customization; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Enums; + +namespace Glamourer.Gui.Customization; + +internal partial class CustomizationDrawer +{ + private const string IconSelectorPopup = "Style Picker"; + + private void DrawIconSelector(CustomizationId id) + { + using var _ = SetId(id); + using var bigGroup = ImRaii.Group(); + var label = _currentOption; + + var current = _set.DataByValue(id, _currentByte, out var custom); + if (current < 0) + { + label = $"{_currentOption} (Custom #{_customize[id]})"; + current = 0; + custom = _set.Data(id, 0); + } + + var icon = Glamourer.Customization.GetIcon(custom!.Value.IconId); + if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) + ImGui.OpenPopup(IconSelectorPopup); + ImGuiUtil.HoverIconTooltip(icon, _iconSize); + + ImGui.SameLine(); + using (var group = ImRaii.Group()) + { + if (_currentId == CustomizationId.Face) + FaceInputInt(current); + else + DataInputInt(current); + ImGui.TextUnformatted($"{label} ({custom.Value.Value})"); + } + + DrawIconPickerPopup(); + } + + private void UpdateFace(CustomizationData data) + { + // Hrothgar Hack + var value = _set.Race == Race.Hrothgar ? data.Value + 4 : data.Value; + if (_customize.Face == value) + return; + + _customize.Face = value; + foreach (var actor in _actors) + Glamourer.Penumbra.RedrawObject(actor.Character, RedrawType.Redraw, false); + } + + private void FaceInputInt(int currentIndex) + { + ++currentIndex; + ImGui.SetNextItemWidth(_inputIntSize); + if (ImGui.InputInt("##text", ref currentIndex, 1, 1)) + { + currentIndex = Math.Clamp(currentIndex - 1, 0, _currentCount - 1); + var data = _set.Data(_currentId, currentIndex, _customize.Face); + UpdateFace(data); + } + + ImGuiUtil.HoverTooltip($"Input Range: [1, {_currentCount}]"); + } + + private void DrawIconPickerPopup() + { + using var popup = ImRaii.Popup(IconSelectorPopup, ImGuiWindowFlags.AlwaysAutoResize); + if (!popup) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameRounding, 0); + for (var i = 0; i < _currentCount; ++i) + { + var custom = _set.Data(_currentId, i, _customize.Face); + var icon = Glamourer.Customization.GetIcon(custom.IconId); + using (var _ = ImRaii.Group()) + { + if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) + { + if (_currentId == CustomizationId.Face) + UpdateFace(custom); + else + UpdateValue(custom.Value); + ImGui.CloseCurrentPopup(); + } + + ImGuiUtil.HoverIconTooltip(icon, _iconSize); + + var text = custom.Value.ToString(); + var textWidth = ImGui.CalcTextSize(text).X; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (_iconSize.X - textWidth + 2 * ImGui.GetStyle().FramePadding.X) / 2); + ImGui.TextUnformatted(text); + } + + if (i % 8 != 7) + ImGui.SameLine(); + } + } +} diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Main.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Main.cs new file mode 100644 index 0000000..c471bf7 --- /dev/null +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Main.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Reflection; +using Glamourer.Customization; +using Glamourer.Interop; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Customization; + +internal partial class CustomizationDrawer +{ + private static readonly Vector4 RedTint = new(0.6f, 0.3f, 0.3f, 1f); + private static readonly ImGuiScene.TextureWrap? LegacyTattoo; + + private readonly Vector2 _iconSize; + private readonly Vector2 _framedIconSize; + private readonly float _inputIntSize; + private readonly float _comboSelectorSize; + private readonly float _raceSelectorWidth; + + private Customize _customize; + private CharacterEquip _equip; + private IReadOnlyCollection _actors = Array.Empty(); + private CustomizationSet _set = null!; + + private CustomizationDrawer() + { + _iconSize = new Vector2(ImGui.GetTextLineHeightWithSpacing() * 2); + _framedIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding; + _inputIntSize = 2 * _framedIconSize.X + ImGui.GetStyle().ItemSpacing.X; + _comboSelectorSize = 4 * _framedIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X; + _raceSelectorWidth = _inputIntSize + _comboSelectorSize - _framedIconSize.X; + } + + static CustomizationDrawer() + => LegacyTattoo = GetLegacyTattooIcon(); + + public static void Dispose() + => LegacyTattoo?.Dispose(); + + private static ImGuiScene.TextureWrap? GetLegacyTattooIcon() + { + using var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("Glamourer.LegacyTattoo.raw"); + if (resource == null) + return null; + + var rawImage = new byte[resource.Length]; + var length = resource.Read(rawImage, 0, (int)resource.Length); + return length == resource.Length + ? Dalamud.PluginInterface.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4) + : null; + } + + public static void Draw(Customize customize, CharacterEquip equip, IReadOnlyCollection actors, bool locked) + { + var d = new CustomizationDrawer() + { + _customize = customize, + _equip = equip, + _actors = actors, + }; + + + if (!ImGui.CollapsingHeader("Character Customization")) + return; + + using var disabled = ImRaii.Disabled(locked); + + d.DrawRaceGenderSelector(); + + d._set = Glamourer.Customization.GetList(customize.Clan, customize.Gender); + + foreach (var id in d._set.Order[CharaMakeParams.MenuType.Percentage]) + d.PercentageSelector(id); + + Functions.IteratePairwise(d._set.Order[CharaMakeParams.MenuType.IconSelector], d.DrawIconSelector, ImGui.SameLine); + + d.DrawMultiIconSelector(); + + foreach (var id in d._set.Order[CharaMakeParams.MenuType.ListSelector]) + d.DrawListSelector(id); + + Functions.IteratePairwise(d._set.Order[CharaMakeParams.MenuType.ColorPicker], d.DrawColorPicker, ImGui.SameLine); + + d.Checkbox(d._set.Option(CustomizationId.HighlightsOnFlag), customize.HighlightsOn, b => customize.HighlightsOn = b); + var xPos = d._inputIntSize + d._framedIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X; + ImGui.SameLine(xPos); + d.Checkbox($"{Glamourer.Customization.GetName(CustomName.Reverse)} {d._set.Option(CustomizationId.FacePaint)}", + customize.FacePaintReversed, b => customize.FacePaintReversed = b); + d.Checkbox($"{Glamourer.Customization.GetName(CustomName.IrisSmall)} {Glamourer.Customization.GetName(CustomName.IrisSize)}", + customize.SmallIris, b => customize.SmallIris = b); + + if (customize.Race != Race.Hrothgar) + { + ImGui.SameLine(xPos); + d.Checkbox(d._set.Option(CustomizationId.LipColor), customize.Lipstick, b => customize.Lipstick = b); + } + } + + public static void Draw(Customize customize, IReadOnlyCollection actors, bool locked = false) + => Draw(customize, CharacterEquip.Null, actors, locked); + + public static void Draw(Customize customize, CharacterEquip equip, bool locked = false) + => Draw(customize, equip, Array.Empty(), locked); + + public static void Draw(Customize customize, bool locked = false) + => Draw(customize, CharacterEquip.Null, Array.Empty(), locked); + + // Set state for drawing of current customization. + private CustomizationId _currentId; + private CustomizationByteValue _currentByte = CustomizationByteValue.Zero; + private int _currentCount; + private string _currentOption = string.Empty; + + // Prepare a new customization option. + private ImRaii.Id SetId(CustomizationId id) + { + _currentId = id; + _currentByte = _customize[id]; + _currentCount = _set.Count(id, _customize.Face); + _currentOption = _set.Option(id); + return ImRaii.PushId((int)id); + } + + // Update the current id with a value, + // also update actors if any. + private void UpdateValue(CustomizationByteValue value) + { + if (_customize[_currentId] == value) + return; + + _customize[_currentId] = value; + UpdateActors(); + } + + // Update all relevant Actors by calling the UpdateCustomize game function. + private void UpdateActors() + { + foreach (var actor in _actors.Where(a => a && a.DrawObject)) + Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); + } +} diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Multi.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Multi.cs new file mode 100644 index 0000000..b06ccdf --- /dev/null +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Multi.cs @@ -0,0 +1,51 @@ +using System.Linq; +using System.Numerics; +using Glamourer.Customization; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; + +namespace Glamourer.Gui.Customization; + +internal partial class CustomizationDrawer +{ + // Only used for facial features, so fixed ID. + private void DrawMultiIconSelector() + { + using var _ = SetId(CustomizationId.FacialFeaturesTattoos); + using var bigGroup = ImRaii.Group(); + + DrawMultiIcons(); + ImGui.SameLine(); + using var group = ImRaii.Group(); + ImGui.Dummy(new Vector2(0, ImGui.GetTextLineHeightWithSpacing() + ImGui.GetStyle().ItemSpacing.Y / 2)); + + _currentCount = 256; + PercentageInputInt(); + + ImGui.TextUnformatted(_set.Option(CustomizationId.FacialFeaturesTattoos)); + } + + private void DrawMultiIcons() + { + using var _ = ImRaii.Group(); + for (var i = 0; i < _currentCount; ++i) + { + var enabled = _customize.FacialFeatures[i]; + var feature = _set.FacialFeature(_customize.Face, i); + var icon = i == _currentCount - 1 + ? LegacyTattoo ?? Glamourer.Customization.GetIcon(feature.IconId) + : Glamourer.Customization.GetIcon(feature.IconId); + if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X, + Vector4.Zero, enabled ? Vector4.One : RedTint)) + { + _customize.FacialFeatures.Set(i, !enabled); + UpdateActors(); + } + + ImGuiUtil.HoverIconTooltip(icon, _iconSize); + if (i % 4 != 3) + ImGui.SameLine(); + } + } +} diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs new file mode 100644 index 0000000..8172051 --- /dev/null +++ b/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq; +using Glamourer.Customization; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Raii; + +namespace Glamourer.Gui.Customization; + +internal partial class CustomizationDrawer +{ + private void DrawListSelector(CustomizationId id) + { + using var _ = SetId(id); + using var bigGroup = ImRaii.Group(); + + ListCombo(); + ImGui.SameLine(); + ListInputInt(); + ImGui.SameLine(); + ImGui.TextUnformatted(_currentOption); + } + + private void ListCombo() + { + ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale); + using var combo = ImRaii.Combo("##combo", $"{_currentOption} #{_currentByte.Value + 1}"); + + if (!combo) + return; + + for (var i = 0; i < _currentCount; ++i) + { + if (ImGui.Selectable($"{_currentOption} #{i + 1}##combo", i == _currentByte.Value)) + UpdateValue((CustomizationByteValue)i); + } + } + + private void ListInputInt() + { + var tmp = _currentByte.Value + 1; + ImGui.SetNextItemWidth(_inputIntSize); + if (ImGui.InputInt("##text", ref tmp, 1, 1) && tmp > 0 && tmp <= _currentCount) + UpdateValue((CustomizationByteValue)Math.Clamp(tmp - 1, 0, _currentCount - 1)); + ImGuiUtil.HoverTooltip($"Input Range: [1, {_currentCount}]"); + } + + private void PercentageSelector(CustomizationId id) + { + using var _ = SetId(id); + using var bigGroup = ImRaii.Group(); + + DrawPercentageSlider(); + ImGui.SameLine(); + PercentageInputInt(); + ImGui.SameLine(); + ImGui.TextUnformatted(_currentOption); + } + + private void DrawPercentageSlider() + { + var tmp = (int)_currentByte.Value; + ImGui.SetNextItemWidth(_comboSelectorSize); + if (ImGui.SliderInt("##slider", ref tmp, 0, _currentCount - 1, "%i", ImGuiSliderFlags.AlwaysClamp)) + UpdateValue((CustomizationByteValue)tmp); + } + + private void PercentageInputInt() + { + var tmp = (int)_currentByte.Value; + ImGui.SetNextItemWidth(_inputIntSize); + if (ImGui.InputInt("##text", ref tmp, 1, 1)) + UpdateValue((CustomizationByteValue)Math.Clamp(tmp, 0, _currentCount - 1)); + ImGuiUtil.HoverTooltip($"Input Range: [0, {_currentCount - 1}]"); + } + + + // Draw one of the four checkboxes for single bool customization options. + private void Checkbox(string label, bool current, Action setter) + { + var tmp = current; + if (ImGui.Checkbox(label, ref tmp) && tmp != current) + { + setter(tmp); + foreach (var actor in _actors.Where(a => a && a.DrawObject)) + Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); + } + } + + // Integral input for an icon- or color based item. + private void DataInputInt(int currentIndex) + { + ++currentIndex; + ImGui.SetNextItemWidth(_inputIntSize); + if (ImGui.InputInt("##text", ref currentIndex, 1, 1)) + { + currentIndex = Math.Clamp(currentIndex - 1, 0, _currentCount - 1); + var data = _set.Data(_currentId, currentIndex, _customize.Face); + UpdateValue(data.Value); + } + ImGuiUtil.HoverTooltip($"Input Range: [1, {_currentCount}]"); + } +} diff --git a/Glamourer/Gui/Interface.Equipment.cs b/Glamourer/Gui/Equipment/Interface.Equipment.cs similarity index 93% rename from Glamourer/Gui/Interface.Equipment.cs rename to Glamourer/Gui/Equipment/Interface.Equipment.cs index 251ad60..2b56e92 100644 --- a/Glamourer/Gui/Interface.Equipment.cs +++ b/Glamourer/Gui/Equipment/Interface.Equipment.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Dalamud.Interface; using Glamourer.Customization; using Glamourer.Interop; using Glamourer.Structs; @@ -10,8 +9,6 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using static Glamourer.Interop.Actor; -using static Lumina.Data.Parsing.Layer.LayerCommon; namespace Glamourer.Gui; @@ -19,13 +16,16 @@ internal partial class Interface { //public class EquipmentDrawer //{ - // private static + // private static readonly IReadOnlyDictionary Stains; // // private Race _race; // private Gender _gender; // private CharacterEquip _equip; // private IReadOnlyCollection _actors = Array.Empty(); // + // static EquipmentDrawer() + // => Stains = GameData.Stains(Dalamud.GameData); + // // public static void Draw(Customize customize, CharacterEquip equip, IReadOnlyCollection actors, bool locked) // { // var d = new EquipmentDrawer() @@ -44,13 +44,10 @@ internal partial class Interface // private bool DrawStainSelector(ComboWithFilter stainCombo, EquipSlot slot, StainId stainIdx) // { // stainCombo.PostPreview = null; - // if (_stains.TryGetValue((byte)stainIdx, out var stain)) - // { - // var previewPush = PushColor(stain, ImGuiCol.FrameBg); - // stainCombo.PostPreview = () => ImGui.PopStyleColor(previewPush); - // } - // + // var found = Stains.TryGetValue(stainIdx, out var stain); + // using var color = ImRaii.PushColor(ImGuiCol.FrameBg, stain.RgbaColor, found); // var change = stainCombo.Draw(string.Empty, out var newStain) && !newStain.RowIndex.Equals(stainIdx); + // if () // if (!change && (byte)stainIdx != 0) // { // ImGuiUtil.HoverTooltip("Right-click to clear."); diff --git a/Glamourer/Gui/Interface.Actors.cs b/Glamourer/Gui/Interface.Actors.cs index 8f6b1ad..828ecfc 100644 --- a/Glamourer/Gui/Interface.Actors.cs +++ b/Glamourer/Gui/Interface.Actors.cs @@ -2,6 +2,7 @@ using System.Numerics; using Dalamud.Interface; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Glamourer.Gui.Customization; using Glamourer.Interop; using Glamourer.State; using ImGuiNET; @@ -57,12 +58,8 @@ internal partial class Interface if (_currentData.Valid) _currentSave.Update(_currentData.Objects[0]); - var d = _currentData.Objects[0].DrawObject.Pointer; - var x = (*(delegate* unmanaged**)d)[50](d); - ImGui.Text($"{x} {_currentData.Objects[0].ModelId}"); - if (x == 1) - CustomizationDrawer.Draw(_currentSave.Data.Customize, _currentSave.Data.Equipment, _currentData.Objects, - _identifier is Actor.SpecialIdentifier); + CustomizationDrawer.Draw(_currentSave.Data.Customize, _currentSave.Data.Equipment, _currentData.Objects, + _identifier is Actor.SpecialIdentifier); } private const uint RedHeaderColor = 0xFF1818C0; diff --git a/Glamourer/Gui/Interface.Customization.cs b/Glamourer/Gui/Interface.Customization.cs deleted file mode 100644 index f966bfc..0000000 --- a/Glamourer/Gui/Interface.Customization.cs +++ /dev/null @@ -1,402 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Logging; -using Glamourer.Customization; -using Glamourer.Interop; -using Glamourer.Util; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Race = Penumbra.GameData.Enums.Race; - -namespace Glamourer.Gui; - -internal partial class Interface -{ - private class CustomizationDrawer - { - private Customize _customize; - private CharacterEquip _equip; - private IReadOnlyCollection _actors = Array.Empty(); - private CustomizationSet _set = null!; - - public static void Draw(Customize customize, CharacterEquip equip, IReadOnlyCollection actors, bool locked) - { - var d = new CustomizationDrawer() - { - _customize = customize, - _equip = equip, - _actors = actors, - }; - - - if (!ImGui.CollapsingHeader("Character Customization")) - return; - - using var disabled = ImRaii.Disabled(locked); - - d.DrawRaceGenderSelector(); - - d._set = Glamourer.Customization.GetList(customize.Clan, customize.Gender); - - foreach (var id in d._set.Order[CharaMakeParams.MenuType.Percentage]) - d.PercentageSelector(id); - - Functions.IteratePairwise(d._set.Order[CharaMakeParams.MenuType.IconSelector], d.DrawIconSelector, ImGui.SameLine); - - d.DrawMultiIconSelector(); - - foreach (var id in d._set.Order[CharaMakeParams.MenuType.ListSelector]) - d.DrawListSelector(id); - - Functions.IteratePairwise(d._set.Order[CharaMakeParams.MenuType.ColorPicker], d.DrawColorPicker, ImGui.SameLine); - - d.Checkbox(d._set.Option(CustomizationId.HighlightsOnFlag), customize.HighlightsOn, b => customize.HighlightsOn = b); - var xPos = _inputIntSize + _framedIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X; - ImGui.SameLine(xPos); - d.Checkbox($"{Glamourer.Customization.GetName(CustomName.Reverse)} {d._set.Option(CustomizationId.FacePaint)}", - customize.FacePaintReversed, b => customize.FacePaintReversed = b); - d.Checkbox($"{Glamourer.Customization.GetName(CustomName.IrisSmall)} {Glamourer.Customization.GetName(CustomName.IrisSize)}", - customize.SmallIris, b => customize.SmallIris = b); - - if (customize.Race != Race.Hrothgar) - { - ImGui.SameLine(xPos); - d.Checkbox(d._set.Option(CustomizationId.LipColor), customize.Lipstick, b => customize.Lipstick = b); - } - } - - private void DrawRaceGenderSelector() - { - DrawGenderSelector(); - ImGui.SameLine(); - using var group = ImRaii.Group(); - DrawRaceCombo(); - var gender = Glamourer.Customization.GetName(CustomName.Gender); - var clan = Glamourer.Customization.GetName(CustomName.Clan); - ImGui.TextUnformatted($"{gender} & {clan}"); - } - - private void DrawGenderSelector() - { - using var font = ImRaii.PushFont(UiBuilder.IconFont); - var icon = _customize.Gender == Gender.Male ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus; - var restricted = _customize.Race == Race.Hrothgar; - if (restricted) - icon = FontAwesomeIcon.MarsDouble; - - if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, restricted, true)) - return; - - var gender = _customize.Gender == Gender.Male ? Gender.Female : Gender.Male; - if (!_customize.ChangeGender(_equip, gender)) - return; - - foreach (var actor in _actors.Where(a => a)) - Glamourer.Penumbra.RedrawObject(actor.Character, RedrawType.Redraw, false); - } - - private void DrawRaceCombo() - { - ImGui.SetNextItemWidth(_raceSelectorWidth); - using var combo = ImRaii.Combo("##subRaceCombo", _customize.ClanName()); - if (!combo) - return; - - foreach (var subRace in Enum.GetValues().Skip(1)) // Skip Unknown - { - if (ImGui.Selectable(CustomizeExtensions.ClanName(subRace, _customize.Gender), subRace == _customize.Clan) - && _customize.ChangeRace(_equip, subRace)) - foreach (var actor in _actors.Where(a => a && a.DrawObject)) - Glamourer.Penumbra.RedrawObject(actor.Character, RedrawType.Redraw, false); - } - } - - private void Checkbox(string label, bool current, Action setter) - { - var tmp = current; - if (ImGui.Checkbox($"##{label}", ref tmp) && tmp != current) - { - setter(tmp); - foreach (var actor in _actors.Where(a => a && a.DrawObject)) - Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); - } - - ImGui.SameLine(); - ImGui.TextUnformatted(label); - } - - private void PercentageSelector(CustomizationId id) - { - using var bigGroup = ImRaii.Group(); - using var _ = ImRaii.PushId((int)id); - int value = _customize[id]; - var count = _set.Count(id); - ImGui.SetNextItemWidth(_comboSelectorSize); - - void OnChange(int v) - { - _customize[id] = (byte)v; - foreach (var actor in _actors.Where(a => a && a.DrawObject)) - Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); - } - - if (ImGui.SliderInt("##slider", ref value, 0, count - 1, "%i", ImGuiSliderFlags.AlwaysClamp)) - OnChange(value); - - ImGui.SameLine(); - InputInt("##input", --value, 0, count - 1, OnChange); - - ImGui.SameLine(); - ImGui.TextUnformatted(_set.OptionName[(int)id]); - } - - private static void InputInt(string label, int startValue, int minValue, int maxValue, Action setter) - { - var tmp = startValue + 1; - ImGui.SetNextItemWidth(_inputIntSize); - if (ImGui.InputInt(label, ref tmp, 1, 1, ImGuiInputTextFlags.EnterReturnsTrue) - && tmp != startValue + 1 - && tmp >= minValue - && tmp <= maxValue) - setter(tmp); - - ImGuiUtil.HoverTooltip($"Input Range: [{minValue}, {maxValue}]"); - } - - private void DrawIconSelector(CustomizationId id) - { - const string popupName = "Style Picker"; - - using var bigGroup = ImRaii.Group(); - using var _ = ImRaii.PushId((int)id); - var count = _set.Count(id, _customize.Face); - var label = _set.Option(id); - - var current = _set.DataByValue(id, _customize[id], out var custom); - if (current < 0) - { - label = $"{label} (Custom #{_customize[id]})"; - current = 0; - custom = _set.Data(id, 0); - } - - var icon = Glamourer.Customization.GetIcon(custom!.Value.IconId); - if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) - ImGui.OpenPopup(popupName); - - ImGuiUtil.HoverIconTooltip(icon, _iconSize); - - void OnChange(int v) - { - var value = _set.Data(id, v - 1).Value; - // Hrothgar hack - if (_set.Race == Race.Hrothgar && id == CustomizationId.Face) - value += 4; - - if (_customize[id] == value) - return; - - _customize[id] = value; - foreach (var actor in _actors.Where(a => a && a.DrawObject)) - Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); - } - - ImGui.SameLine(); - using var group = ImRaii.Group(); - InputInt("##text", current, 1, count, OnChange); - - ImGui.TextUnformatted($"{label} ({custom.Value.Value})"); - - DrawIconPickerPopup(popupName, id, OnChange); - } - - private void DrawIconPickerPopup(string label, CustomizationId id, Action setter) - { - using var popup = ImRaii.Popup(label, ImGuiWindowFlags.AlwaysAutoResize); - if (!popup) - return; - - var count = _set.Count(id, _customize.Face); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) - .Push(ImGuiStyleVar.FrameRounding, 0); - for (var i = 0; i < count; ++i) - { - var custom = _set.Data(id, i, _customize.Face); - var icon = Glamourer.Customization.GetIcon(custom.IconId); - using var group = ImRaii.Group(); - if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) - { - setter(custom.Value); - ImGui.CloseCurrentPopup(); - } - - ImGuiUtil.HoverIconTooltip(icon, _iconSize); - - var text = custom.Value.ToString(); - var textWidth = ImGui.CalcTextSize(text).X; - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (_iconSize.X - textWidth + 2 * ImGui.GetStyle().FramePadding.X) / 2); - ImGui.TextUnformatted(text); - group.Dispose(); - - if (i % 8 != 7) - ImGui.SameLine(); - } - } - - private void DrawColorPicker(CustomizationId id) - { - const string popupName = "Color Picker"; - using var _ = ImRaii.PushId((int)id); - var count = _set.Count(id); - var label = _set.Option(id); - var (current, custom) = GetCurrentCustomization(id); - - if (ImGui.ColorButton($"{current + 1}##color", ImGui.ColorConvertU32ToFloat4(custom.Color), ImGuiColorEditFlags.None, - _framedIconSize)) - ImGui.OpenPopup(popupName); - - ImGui.SameLine(); - - void OnChange(int v) - { - _customize[id] = _set.Data(id, v).Value; - foreach (var actor in _actors.Where(a => a && a.DrawObject)) - Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); - } - - using (var group = ImRaii.Group()) - { - InputInt("##text", current, 1, count, OnChange); - ImGui.TextUnformatted(label); - } - - DrawColorPickerPopup(popupName, id, OnChange); - } - - private (int, Customization.Customization) GetCurrentCustomization(CustomizationId id) - { - var current = _set.DataByValue(id, _customize[id], out var custom); - if (_set.IsAvailable(id) && current < 0) - { - PluginLog.Warning($"Read invalid customization value {_customize[id]} for {id}."); - current = 0; - custom = _set.Data(id, 0); - } - - return (current, custom!.Value); - } - - private void DrawColorPickerPopup(string label, CustomizationId id, Action setter) - { - using var popup = ImRaii.Popup(label, ImGuiWindowFlags.AlwaysAutoResize); - if (!popup) - return; - - var count = _set.Count(id); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) - .Push(ImGuiStyleVar.FrameRounding, 0); - for (var i = 0; i < count; ++i) - { - var custom = _set.Data(id, i); - if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color))) - { - setter(custom.Value); - ImGui.CloseCurrentPopup(); - } - - if (i % 8 != 7) - ImGui.SameLine(); - } - } - - private void DrawMultiIconSelector() - { - using var bigGroup = ImRaii.Group(); - using var _ = ImRaii.PushId((int)CustomizationId.FacialFeaturesTattoos); - - void OnChange(int v) - { - _customize[CustomizationId.FacialFeaturesTattoos] = (byte)v; - foreach (var actor in _actors.Where(a => a && a.DrawObject)) - Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); - } - - DrawMultiIcons(); - ImGui.SameLine(); - int value = _customize[CustomizationId.FacialFeaturesTattoos]; - using var group = ImRaii.Group(); - ImGui.Dummy(new Vector2(0, ImGui.GetTextLineHeightWithSpacing() + ImGui.GetStyle().ItemSpacing.Y / 2)); - InputInt(string.Empty, --value, 0, 255, OnChange); - - ImGui.TextUnformatted(_set.Option(CustomizationId.FacialFeaturesTattoos)); - } - - private void DrawMultiIcons() - { - using var _ = ImRaii.Group(); - var face = _customize.Face; - - var ret = false; - var count = _set.Count(CustomizationId.FacialFeaturesTattoos); - for (var i = 0; i < count; ++i) - { - var enabled = _customize.FacialFeatures[i]; - var feature = _set.FacialFeature(face, i); - var icon = i == count - 1 - ? LegacyTattoo ?? Glamourer.Customization.GetIcon(feature.IconId) - : Glamourer.Customization.GetIcon(feature.IconId); - if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X, - Vector4.Zero, enabled ? Vector4.One : RedTint)) - { - _customize.FacialFeatures.Set(i, !enabled); - foreach (var actor in _actors.Where(a => a && a.DrawObject)) - Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); - } - - ImGuiUtil.HoverIconTooltip(icon, _iconSize); - if (i % 4 != 3) - ImGui.SameLine(); - } - } - - private void DrawListSelector(CustomizationId id) - { - using var _ = ImRaii.PushId((int)id); - using var bigGroup = ImRaii.Group(); - int current = _customize[id]; - var count = _set.Count(id); - - void OnChange(int v) - { - _customize[id] = (byte)v; - foreach (var actor in _actors.Where(a => a && a.DrawObject)) - Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); - } - - ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale); - using (var combo = ImRaii.Combo("##combo", $"{_set.Option(id)} #{current + 1}")) - { - if (combo) - for (var i = 0; i < count; ++i) - { - if (!ImGui.Selectable($"{_set.Option(id)} #{i + 1}##combo", i == current) || i == current) - continue; - - OnChange(i); - } - } - - ImGui.SameLine(); - InputInt("##text", current, 1, count, OnChange); - - ImGui.SameLine(); - ImGui.TextUnformatted(_set.Option(id)); - } - } -} diff --git a/Glamourer/Gui/Interface.DebugStateTab.cs b/Glamourer/Gui/Interface.DebugStateTab.cs index bfe0aeb..5fd7a7a 100644 --- a/Glamourer/Gui/Interface.DebugStateTab.cs +++ b/Glamourer/Gui/Interface.DebugStateTab.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Numerics; +using Glamourer.Gui.Customization; using Glamourer.Interop; using Glamourer.State; using ImGuiNET; diff --git a/Glamourer/Gui/Interface.State.cs b/Glamourer/Gui/Interface.State.cs index a9b07b8..24b84fa 100644 --- a/Glamourer/Gui/Interface.State.cs +++ b/Glamourer/Gui/Interface.State.cs @@ -1,6 +1,4 @@ -using System; -using System.Numerics; -using System.Reflection; +using System.Numerics; using Dalamud.Interface; using ImGuiNET; @@ -8,41 +6,12 @@ namespace Glamourer.Gui; internal partial class Interface { - private static readonly ImGuiScene.TextureWrap? LegacyTattoo = GetLegacyTattooIcon(); - private static readonly Vector4 RedTint = new(0.6f, 0.3f, 0.3f, 1f); - - private static Vector2 _iconSize = Vector2.Zero; - private static Vector2 _framedIconSize = Vector2.Zero; private static Vector2 _spacing = Vector2.Zero; private static float _actorSelectorWidth; - private static float _inputIntSize; - private static float _comboSelectorSize; - private static float _raceSelectorWidth; private static void UpdateState() { - // General _spacing = _spacing with { Y = ImGui.GetTextLineHeightWithSpacing() / 2 }; _actorSelectorWidth = 200 * ImGuiHelpers.GlobalScale; - - // Customize - _iconSize = new Vector2(ImGui.GetTextLineHeightWithSpacing() * 2); - _framedIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding; - _inputIntSize = 2 * _framedIconSize.X + ImGui.GetStyle().ItemSpacing.X; - _comboSelectorSize = 4 * _framedIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X; - _raceSelectorWidth = _inputIntSize + _comboSelectorSize - _framedIconSize.X; - } - - private static ImGuiScene.TextureWrap? GetLegacyTattooIcon() - { - using var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("Glamourer.LegacyTattoo.raw"); - if (resource == null) - return null; - - var rawImage = new byte[resource.Length]; - var length = resource.Read(rawImage, 0, (int)resource.Length); - return length == resource.Length - ? Dalamud.PluginInterface.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4) - : null; } } diff --git a/Glamourer/Gui/Interface.cs b/Glamourer/Gui/Interface.cs index df81459..07acd44 100644 --- a/Glamourer/Gui/Interface.cs +++ b/Glamourer/Gui/Interface.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Numerics; using Dalamud.Interface.Windowing; using Dalamud.Logging; +using Glamourer.Gui.Customization; using ImGuiNET; using OtterGui.Raii; @@ -56,6 +57,7 @@ internal partial class Interface : Window, IDisposable public void Dispose() { Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= Toggle; + CustomizationDrawer.Dispose(); } private static string GetLabel() diff --git a/Glamourer/State/CharacterSave.cs b/Glamourer/State/CharacterSave.cs index 2fce80c..47f5884 100644 --- a/Glamourer/State/CharacterSave.cs +++ b/Glamourer/State/CharacterSave.cs @@ -159,7 +159,7 @@ public struct CharacterData public interface ICharacterData { - public CharacterData Data { get; } + public ref CharacterData Data { get; } //public bool ApplyModel(); //public bool ApplyCustomize(Customize target); diff --git a/Glamourer/State/CurrentDesign.cs b/Glamourer/State/CurrentDesign.cs index 34fc6ea..b29b956 100644 --- a/Glamourer/State/CurrentDesign.cs +++ b/Glamourer/State/CurrentDesign.cs @@ -8,8 +8,8 @@ namespace Glamourer.State; public unsafe class CurrentDesign : ICharacterData { - public CharacterData Data - => _drawData; + public ref CharacterData Data + => ref _drawData; private CharacterData _drawData; private CharacterData _initialData; diff --git a/Glamourer/Util/CustomizeExtensions.cs b/Glamourer/Util/CustomizeExtensions.cs index 973c309..a900e6b 100644 --- a/Glamourer/Util/CustomizeExtensions.cs +++ b/Glamourer/Util/CustomizeExtensions.cs @@ -90,12 +90,12 @@ public static unsafe class CustomizeExtensions customize.Load(newCustomize); } - public static bool ChangeCustomization(this Customize customize, CharacterEquip equip, CustomizationId id, byte value) + public static bool ChangeCustomization(this Customize customize, CharacterEquip equip, CustomizationId id, CustomizationByteValue value) { switch (id) { - case CustomizationId.Race: return customize.ChangeRace(equip, (SubRace)value); - case CustomizationId.Gender: return customize.ChangeGender(equip, (Gender)value); + case CustomizationId.Race: return customize.ChangeRace(equip, (SubRace)value.Value); + case CustomizationId.Gender: return customize.ChangeGender(equip, (Gender)value.Value); } if (customize[id] == value) @@ -113,17 +113,17 @@ public static unsafe class CustomizeExtensions { switch (id) { - case CustomizationId.Race: break; - case CustomizationId.Clan: break; - case CustomizationId.BodyType: break; - case CustomizationId.Gender: break; - case CustomizationId.FacialFeaturesTattoos: break; - case CustomizationId.HighlightsOnFlag: break; - case CustomizationId.Face: break; + case CustomizationId.Race: break; + case CustomizationId.Clan: break; + case CustomizationId.BodyType: break; + case CustomizationId.Gender: break; + case CustomizationId.FacialFeaturesTattoos: break; + case CustomizationId.HighlightsOnFlag: break; + case CustomizationId.Face: break; default: var count = set.Count(id); if (set.DataByValue(id, customize[id], out _) < 0) - customize[id] = count == 0 ? (byte)0 : set.Data(id, 0).Value; + customize[id] = count == 0 ? CustomizationByteValue.Zero : set.Data(id, 0).Value; break; } } @@ -131,7 +131,7 @@ public static unsafe class CustomizeExtensions private static void FixRestrictedGear(Customize customize, CharacterEquip equip, Gender gender, Race race) { - if (race == customize.Race && gender == customize.Gender) + if (!equip || race == customize.Race && gender == customize.Gender) return; foreach (var slot in EquipSlotExtensions.EqdpSlots)