Do the Reworkings

This commit is contained in:
Ottermandias 2022-09-29 23:57:37 +02:00
parent a67cd8ad89
commit e3a58340b3
23 changed files with 1067 additions and 943 deletions

View file

@ -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;
}
}

View file

@ -1,314 +1,33 @@
using System; using System.Runtime.InteropServices;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Customization; 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) [FieldOffset(1)]
=> Data = data; 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]; Id = id;
set => Data->Data[0] = (byte)value; Value = value;
} IconId = data;
Color = data;
// Skip Unknown Gender CustomizeId = customizeId;
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);
}

View file

@ -149,7 +149,7 @@ public partial class CustomizationOptions
HighlightColors = _highlightPicker, HighlightColors = _highlightPicker,
TattooColors = _tattooColorPicker, TattooColors = _tattooColorPicker,
LipColorsDark = hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark, LipColorsDark = hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark,
LipColorsLight = hrothgar ? Array.Empty<Customization>() : _lipColorPickerLight, LipColorsLight = hrothgar ? Array.Empty<CustomizationData>() : _lipColorPickerLight,
FacePaintColorsDark = _facePaintColorPickerDark, FacePaintColorsDark = _facePaintColorPickerDark,
FacePaintColorsLight = _facePaintColorPickerLight, FacePaintColorsLight = _facePaintColorPickerLight,
Faces = GetFaces(row), Faces = GetFaces(row),
@ -203,19 +203,19 @@ public partial class CustomizationOptions
private readonly CmpFile _cmpFile; private readonly CmpFile _cmpFile;
// Those values are shared between all races. // Those values are shared between all races.
private readonly Customization[] _highlightPicker; private readonly CustomizationData[] _highlightPicker;
private readonly Customization[] _eyeColorPicker; private readonly CustomizationData[] _eyeColorPicker;
private readonly Customization[] _facePaintColorPickerDark; private readonly CustomizationData[] _facePaintColorPickerDark;
private readonly Customization[] _facePaintColorPickerLight; private readonly CustomizationData[] _facePaintColorPickerLight;
private readonly Customization[] _lipColorPickerDark; private readonly CustomizationData[] _lipColorPickerDark;
private readonly Customization[] _lipColorPickerLight; private readonly CustomizationData[] _lipColorPickerLight;
private readonly Customization[] _tattooColorPicker; private readonly CustomizationData[] _tattooColorPicker;
private readonly CustomizationOptions _options; 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) => _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(); .ToArray();
@ -227,12 +227,12 @@ public partial class CustomizationOptions
return; return;
} }
var tmp = new IReadOnlyList<Customization>[set.Faces.Count + 1]; var tmp = new IReadOnlyList<CustomizationData>[set.Faces.Count + 1];
tmp[0] = set.HairStyles; tmp[0] = set.HairStyles;
for (var i = 1; i <= set.Faces.Count; ++i) 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; var data = _customizeSheet.GetRow(c.CustomizeId)?.Unknown6 ?? 0;
return data == 0 || data == i + set.Faces.Count; return data == 0 || data == i + set.Faces.Count;
@ -295,13 +295,15 @@ public partial class CustomizationOptions
private static void SetFacialFeatures(CustomizationSet set, CharaMakeParams row) private static void SetFacialFeatures(CustomizationSet set, CharaMakeParams row)
{ {
var count = set.Faces.Count; var count = set.Faces.Count;
var featureDict = new List<IReadOnlyList<Customization>>(count); var featureDict = new List<IReadOnlyList<CustomizationData>>(count);
for (var i = 0; i < count; ++i) 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) 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) .Append(legacyTattoo)
.ToArray()); .ToArray());
} }
@ -346,7 +348,7 @@ public partial class CustomizationOptions
} }
// Obtain available skin and hair colors for the given subrace and gender. // 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) if (race is > SubRace.Veena or SubRace.Unknown)
throw new ArgumentOutOfRangeException(nameof(race), race, null); 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. // 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)!; var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
// Unknown30 is the number of available hairstyles. // Unknown30 is the number of available hairstyles.
var hairList = new List<Customization>(row.Unknown30); var hairList = new List<CustomizationData>(row.Unknown30);
// Hairstyles can be found starting at Unknown66. // Hairstyles can be found starting at Unknown66.
for (var i = 0; i < row.Unknown30; ++i) 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. // Hair Row from CustomizeSheet might not be set in case of unlockable hair.
var hairRow = _customizeSheet.GetRow(customizeIdx); var hairRow = _customizeSheet.GetRow(customizeIdx);
hairList.Add(hairRow != null hairList.Add(hairRow != null
? new Customization(CustomizationId.Hairstyle, hairRow.FeatureID, hairRow.Icon, (ushort)hairRow.RowId) ? new CustomizationData(CustomizationId.Hairstyle, (CustomizationByteValue)hairRow.FeatureID, hairRow.Icon,
: new Customization(CustomizationId.Hairstyle, (byte)i, customizeIdx)); (ushort)hairRow.RowId)
: new CustomizationData(CustomizationId.Hairstyle, (CustomizationByteValue)i, customizeIdx));
} }
return hairList.ToArray(); return hairList.ToArray();
} }
// Get Features. // 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); var row = _customizeSheet.GetRow(value);
return row == null return row == null
? new Customization(id, (byte)(index + 1), value) ? new CustomizationData(id, (CustomizationByteValue)(index + 1), value)
: new Customization(id, row.FeatureID, row.Icon, (ushort)row.RowId); : new CustomizationData(id, (CustomizationByteValue)row.FeatureID, row.Icon, (ushort)row.RowId);
} }
// Get List sizes. // Get List sizes.
@ -400,10 +403,10 @@ public partial class CustomizationOptions
} }
// Get face paints from the hair sheet via reflection. // 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 row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var paintList = new List<Customization>(row.Unknown37); var paintList = new List<CustomizationData>(row.Unknown37);
// Number of available face paints is at Unknown37. // Number of available face paints is at Unknown37.
for (var i = 0; i < row.Unknown37; ++i) for (var i = 0; i < row.Unknown37; ++i)
@ -419,29 +422,30 @@ public partial class CustomizationOptions
var paintRow = _customizeSheet.GetRow(customizeIdx); var paintRow = _customizeSheet.GetRow(customizeIdx);
// Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints. // Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints.
paintList.Add(paintRow != null paintList.Add(paintRow != null
? new Customization(CustomizationId.FacePaint, paintRow.FeatureID, paintRow.Icon, (ushort)paintRow.RowId) ? new CustomizationData(CustomizationId.FacePaint, (CustomizationByteValue)paintRow.FeatureID, paintRow.Icon,
: new Customization(CustomizationId.FacePaint, (byte)i, customizeIdx)); (ushort)paintRow.RowId)
: new CustomizationData(CustomizationId.FacePaint, (CustomizationByteValue)i, customizeIdx));
} }
return paintList.ToArray(); return paintList.ToArray();
} }
// Specific icons for tails or ears. // Specific icons for tails or ears.
private Customization[] GetTailEarShapes(CharaMakeParams row) private CustomizationData[] GetTailEarShapes(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.TailEarShape)?.Values => row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.TailEarShape)?.Values
.Select((v, i) => FromValueAndIndex(CustomizationId.TailEarShape, v, i)).ToArray() .Select((v, i) => FromValueAndIndex(CustomizationId.TailEarShape, v, i)).ToArray()
?? Array.Empty<Customization>(); ?? Array.Empty<CustomizationData>();
// Specific icons for faces. // Specific icons for faces.
private Customization[] GetFaces(CharaMakeParams row) private CustomizationData[] GetFaces(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.Face)?.Values => row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.Face)?.Values
.Select((v, i) => FromValueAndIndex(CustomizationId.Face, v, i)).ToArray() .Select((v, i) => FromValueAndIndex(CustomizationId.Face, v, i)).ToArray()
?? Array.Empty<Customization>(); ?? Array.Empty<CustomizationData>();
// Specific icons for Hrothgar patterns. // Specific icons for Hrothgar patterns.
private Customization[] HrothgarFurPattern(CharaMakeParams row) private CustomizationData[] HrothgarFurPattern(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.LipColor)?.Values => row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.LipColor)?.Values
.Select((v, i) => FromValueAndIndex(CustomizationId.LipColor, v, i)).ToArray() .Select((v, i) => FromValueAndIndex(CustomizationId.LipColor, v, i)).ToArray()
?? Array.Empty<Customization>(); ?? Array.Empty<CustomizationData>();
} }
} }

View file

@ -2,7 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Microsoft.VisualBasic; using OtterGui;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
namespace Glamourer.Customization; namespace Glamourer.Customization;
@ -15,16 +15,15 @@ public class CustomizationSet
{ {
Gender = gender; Gender = gender;
Clan = clan; Clan = clan;
_settingAvailable = clan.ToRace() == Race.Hrothgar && gender == Gender.Female Race = clan.ToRace();
_settingAvailable = Race == Race.Hrothgar && gender == Gender.Female
? 0u ? 0u
: DefaultAvailable; : DefaultAvailable;
} }
public Gender Gender { get; } public Gender Gender { get; }
public SubRace Clan { get; } public SubRace Clan { get; }
public Race Race { get; }
public Race Race
=> Clan.ToRace();
private uint _settingAvailable; private uint _settingAvailable;
@ -34,11 +33,18 @@ public class CustomizationSet
public bool IsAvailable(CustomizationId id) public bool IsAvailable(CustomizationId id)
=> (_settingAvailable & (1u << (int)id)) != 0; => (_settingAvailable & (1u << (int)id)) != 0;
public int NumEyebrows { get; internal init; } private const uint DefaultAvailable =
public int NumEyeShapes { get; internal init; } (1u << (int)CustomizationId.Height)
public int NumNoseShapes { get; internal init; } | (1u << (int)CustomizationId.Hairstyle)
public int NumJawShapes { get; internal init; } | (1u << (int)CustomizationId.SkinColor)
public int NumMouthShapes { get; internal init; } | (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) public string ToHumanReadable(Customize customizationData)
{ {
@ -49,45 +55,53 @@ public class CustomizationSet
return sb.ToString(); return sb.ToString();
} }
// Meta
public IReadOnlyList<string> OptionName { get; internal set; } = null!; public IReadOnlyList<string> OptionName { get; internal set; } = null!;
public IReadOnlyList<Customization> Faces { get; internal init; } = null!;
public IReadOnlyList<Customization> HairStyles { get; internal init; } = null!;
public IReadOnlyList<IReadOnlyList<Customization>> HairByFace { get; internal set; } = null!;
public IReadOnlyList<Customization> TailEarShapes { get; internal init; } = null!;
public IReadOnlyList<IReadOnlyList<Customization>> FeaturesTattoos { get; internal set; } = null!;
public IReadOnlyList<Customization> FacePaints { get; internal init; } = null!;
public IReadOnlyList<Customization> SkinColors { get; internal init; } = null!; public string Option(CustomizationId id)
public IReadOnlyList<Customization> HairColors { get; internal init; } = null!; => OptionName[(int)id];
public IReadOnlyList<Customization> HighlightColors { get; internal init; } = null!;
public IReadOnlyList<Customization> EyeColors { get; internal init; } = null!;
public IReadOnlyList<Customization> TattooColors { get; internal init; } = null!;
public IReadOnlyList<Customization> FacePaintColorsLight { get; internal init; } = null!;
public IReadOnlyList<Customization> FacePaintColorsDark { get; internal init; } = null!;
public IReadOnlyList<Customization> LipColorsLight { get; internal init; } = null!;
public IReadOnlyList<Customization> LipColorsDark { get; internal init; } = null!;
public IReadOnlyList<CharaMakeParams.MenuType> Types { get; internal set; } = null!; public IReadOnlyList<CharaMakeParams.MenuType> Types { get; internal set; } = null!;
public IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizationId[]> Order { get; internal set; } = null!; public IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizationId[]> Order { get; internal set; } = null!;
public string Option(CustomizationId id) // Always list selector.
=> OptionName[(int)id]; 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<CustomizationData> Faces { get; internal init; } = null!;
public IReadOnlyList<CustomizationData> HairStyles { get; internal init; } = null!;
public IReadOnlyList<IReadOnlyList<CustomizationData>> HairByFace { get; internal set; } = null!;
public IReadOnlyList<CustomizationData> TailEarShapes { get; internal init; } = null!;
public IReadOnlyList<IReadOnlyList<CustomizationData>> FeaturesTattoos { get; internal set; } = null!;
public IReadOnlyList<CustomizationData> FacePaints { get; internal init; } = null!;
public CustomizationData FacialFeature(CustomizationByteValue face, int idx)
{ {
faceIdx = HrothgarFaceHack((byte) faceIdx) - 1; face = HrothgarFaceHack(face);
if (faceIdx < FeaturesTattoos.Count) var faceIdx = Faces.IndexOf(p => p.Value == face);
return FeaturesTattoos[HrothgarFaceHack((byte)faceIdx)][idx]; return FeaturesTattoos[faceIdx != -1 ? faceIdx : 0][idx];
return FeaturesTattoos[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<CustomizationData> SkinColors { get; internal init; } = null!;
public IReadOnlyList<CustomizationData> HairColors { get; internal init; } = null!;
public IReadOnlyList<CustomizationData> HighlightColors { get; internal init; } = null!;
public IReadOnlyList<CustomizationData> EyeColors { get; internal init; } = null!;
public IReadOnlyList<CustomizationData> TattooColors { get; internal init; } = null!;
public IReadOnlyList<CustomizationData> FacePaintColorsLight { get; internal init; } = null!;
public IReadOnlyList<CustomizationData> FacePaintColorsDark { get; internal init; } = null!;
public IReadOnlyList<CustomizationData> LipColorsLight { get; internal init; } = null!;
public IReadOnlyList<CustomizationData> LipColorsDark { get; internal init; } = null!;
public int DataByValue(CustomizationId id, CustomizationByteValue value, out CustomizationData? custom)
{ {
var type = id.ToType(); var type = id.ToType();
custom = null; custom = null;
@ -95,16 +109,16 @@ public class CustomizationSet
{ {
if (value < Count(id)) if (value < Count(id))
{ {
custom = new Customization(id, value, 0, value); custom = new CustomizationData(id, value, 0, value.Value);
return value; return value.Value;
} }
return -1; return -1;
} }
int Get(IEnumerable<Customization> list, byte v, ref Customization? output) int Get(IEnumerable<CustomizationData> list, CustomizationByteValue v, ref CustomizationData? output)
{ {
var (val, idx) = list.Cast<Customization?>().Select((c, i) => (c, i)).FirstOrDefault(c => c.c!.Value.Value == v); var (val, idx) = list.Cast<CustomizationData?>().WithIndex().FirstOrDefault(p => p.Item1!.Value.Value == v);
if (val == null) if (val == null)
return -1; 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(); throw new IndexOutOfRangeException();
switch (id.ToType()) switch (id.ToType())
{ {
case CharaMakeParams.MenuType.Percentage: 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 Customization(id, (byte)idx, 0, (ushort)idx); case CharaMakeParams.MenuType.ListSelector: return new CustomizationData(id, (CustomizationByteValue)idx, 0, (ushort)idx);
} }
return id switch return id switch
{ {
CustomizationId.Face => Faces[idx], 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.TailEarShape => TailEarShapes[idx],
CustomizationId.FacePaint => FacePaints[idx], CustomizationId.FacePaint => FacePaints[idx],
CustomizationId.FacialFeaturesTattoos => FeaturesTattoos[0][idx], CustomizationId.FacialFeaturesTattoos => FeaturesTattoos[0][idx],
@ -159,7 +176,7 @@ public class CustomizationSet
CustomizationId.TattooColor => TattooColors[idx], CustomizationId.TattooColor => TattooColors[idx],
CustomizationId.LipColor => idx < 96 ? LipColorsDark[idx] : LipColorsLight[idx - 96], CustomizationId.LipColor => idx < 96 ? LipColorsDark[idx] : LipColorsLight[idx - 96],
CustomizationId.FacePaintColor => idx < 96 ? FacePaintColorsDark[idx] : FacePaintColorsLight[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; 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)) if (!IsAvailable(id))
return 0; return 0;
@ -190,7 +210,7 @@ public class CustomizationSet
return id switch return id switch
{ {
CustomizationId.Face => Faces.Count, 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.HighlightsOnFlag => 2,
CustomizationId.SkinColor => SkinColors.Count, CustomizationId.SkinColor => SkinColors.Count,
CustomizationId.EyeColorR => EyeColors.Count, CustomizationId.EyeColorR => EyeColors.Count,
@ -212,16 +232,6 @@ public class CustomizationSet
}; };
} }
private const uint DefaultAvailable = private CustomizationByteValue HrothgarFaceHack(CustomizationByteValue value)
(1u << (int)CustomizationId.Height) => Race == Race.Hrothgar && value.Value is > 4 and < 9 ? value - 4 : value;
| (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);
} }

View file

@ -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);
}

View file

@ -6,6 +6,7 @@ using Dalamud.Data;
using Glamourer.Structs; using Glamourer.Structs;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Item = Glamourer.Structs.Item; using Item = Glamourer.Structs.Item;
using Stain = Glamourer.Structs.Stain; using Stain = Glamourer.Structs.Stain;
@ -13,7 +14,7 @@ namespace Glamourer;
public static class GameData public static class GameData
{ {
private static Dictionary<byte, Stain>? _stains; private static Dictionary<StainId, Stain>? _stains;
private static Dictionary<EquipSlot, List<Item>>? _itemsBySlot; private static Dictionary<EquipSlot, List<Item>>? _itemsBySlot;
private static Dictionary<byte, Job>? _jobs; private static Dictionary<byte, Job>? _jobs;
private static Dictionary<ushort, JobGroup>? _jobGroups; private static Dictionary<ushort, JobGroup>? _jobGroups;
@ -26,13 +27,13 @@ public static class GameData
public static ModelData Models(DataManager dataManager) public static ModelData Models(DataManager dataManager)
=> _models ??= new ModelData(dataManager); => _models ??= new ModelData(dataManager);
public static IReadOnlyDictionary<byte, Stain> Stains(DataManager dataManager) public static IReadOnlyDictionary<StainId, Stain> Stains(DataManager dataManager)
{ {
if (_stains != null) if (_stains != null)
return _stains; return _stains;
var sheet = dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Stain>()!; var sheet = dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Stain>()!;
_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; return _stains;
} }

View file

@ -20,61 +20,25 @@ EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A5439F6B-83C1-4078-9371-354A147FF554}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {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|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.ActiveCfg = Release|Any CPU
{A5439F6B-83C1-4078-9371-354A147FF554}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View file

@ -1,17 +1,13 @@
using System; using System.Reflection;
using System.Reflection;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Hooking;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Logging;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Api; using Glamourer.Api;
using Glamourer.Customization; using Glamourer.Customization;
using Glamourer.Gui; using Glamourer.Gui;
using Glamourer.Interop; using Glamourer.Interop;
using Glamourer.State; using Glamourer.State;
using OtterGui.Log;
using Penumbra.GameData; using Penumbra.GameData;
namespace Glamourer; namespace Glamourer;
@ -32,6 +28,7 @@ public class Glamourer : IDalamudPlugin
public static GlamourerConfig Config = null!; public static GlamourerConfig Config = null!;
public static Logger Log = null!;
public static IObjectIdentifier Identifier = null!; public static IObjectIdentifier Identifier = null!;
public static PenumbraAttach Penumbra = null!; public static PenumbraAttach Penumbra = null!;
@ -55,6 +52,7 @@ public class Glamourer : IDalamudPlugin
try try
{ {
Dalamud.Initialize(pluginInterface); Dalamud.Initialize(pluginInterface);
Log = new Logger();
Customization = CustomizationManager.Create(Dalamud.PluginInterface, Dalamud.GameData, Dalamud.ClientState.ClientLanguage); Customization = CustomizationManager.Create(Dalamud.PluginInterface, Dalamud.GameData, Dalamud.ClientState.ClientLanguage);
RestrictedGear = GameData.RestrictedGear(Dalamud.GameData); RestrictedGear = GameData.RestrictedGear(Dalamud.GameData);

View file

@ -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));
}
}

View file

@ -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<SubRace>().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);
}
}
}

View file

@ -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();
}
}
}

View file

@ -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<Actor> _actors = Array.Empty<Actor>();
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<Actor> 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<Actor> 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<Actor>(), locked);
public static void Draw(Customize customize, bool locked = false)
=> Draw(customize, CharacterEquip.Null, Array.Empty<Actor>(), 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);
}
}

View file

@ -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();
}
}
}

View file

@ -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<bool> 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}]");
}
}

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Dalamud.Interface;
using Glamourer.Customization; using Glamourer.Customization;
using Glamourer.Interop; using Glamourer.Interop;
using Glamourer.Structs; using Glamourer.Structs;
@ -10,8 +9,6 @@ using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using static Glamourer.Interop.Actor;
using static Lumina.Data.Parsing.Layer.LayerCommon;
namespace Glamourer.Gui; namespace Glamourer.Gui;
@ -19,13 +16,16 @@ internal partial class Interface
{ {
//public class EquipmentDrawer //public class EquipmentDrawer
//{ //{
// private static // private static readonly IReadOnlyDictionary<StainId, Stain> Stains;
// //
// private Race _race; // private Race _race;
// private Gender _gender; // private Gender _gender;
// private CharacterEquip _equip; // private CharacterEquip _equip;
// private IReadOnlyCollection<Actor> _actors = Array.Empty<Actor>(); // private IReadOnlyCollection<Actor> _actors = Array.Empty<Actor>();
// //
// static EquipmentDrawer()
// => Stains = GameData.Stains(Dalamud.GameData);
//
// public static void Draw(Customize customize, CharacterEquip equip, IReadOnlyCollection<Actor> actors, bool locked) // public static void Draw(Customize customize, CharacterEquip equip, IReadOnlyCollection<Actor> actors, bool locked)
// { // {
// var d = new EquipmentDrawer() // var d = new EquipmentDrawer()
@ -44,13 +44,10 @@ internal partial class Interface
// private bool DrawStainSelector(ComboWithFilter<Stain> stainCombo, EquipSlot slot, StainId stainIdx) // private bool DrawStainSelector(ComboWithFilter<Stain> stainCombo, EquipSlot slot, StainId stainIdx)
// { // {
// stainCombo.PostPreview = null; // stainCombo.PostPreview = null;
// if (_stains.TryGetValue((byte)stainIdx, out var stain)) // var found = Stains.TryGetValue(stainIdx, out var stain);
// { // using var color = ImRaii.PushColor(ImGuiCol.FrameBg, stain.RgbaColor, found);
// var previewPush = PushColor(stain, ImGuiCol.FrameBg);
// stainCombo.PostPreview = () => ImGui.PopStyleColor(previewPush);
// }
//
// var change = stainCombo.Draw(string.Empty, out var newStain) && !newStain.RowIndex.Equals(stainIdx); // var change = stainCombo.Draw(string.Empty, out var newStain) && !newStain.RowIndex.Equals(stainIdx);
// if ()
// if (!change && (byte)stainIdx != 0) // if (!change && (byte)stainIdx != 0)
// { // {
// ImGuiUtil.HoverTooltip("Right-click to clear."); // ImGuiUtil.HoverTooltip("Right-click to clear.");

View file

@ -2,6 +2,7 @@
using System.Numerics; using System.Numerics;
using Dalamud.Interface; using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Gui.Customization;
using Glamourer.Interop; using Glamourer.Interop;
using Glamourer.State; using Glamourer.State;
using ImGuiNET; using ImGuiNET;
@ -57,10 +58,6 @@ internal partial class Interface
if (_currentData.Valid) if (_currentData.Valid)
_currentSave.Update(_currentData.Objects[0]); _currentSave.Update(_currentData.Objects[0]);
var d = _currentData.Objects[0].DrawObject.Pointer;
var x = (*(delegate* unmanaged<Human*, byte>**)d)[50](d);
ImGui.Text($"{x} {_currentData.Objects[0].ModelId}");
if (x == 1)
CustomizationDrawer.Draw(_currentSave.Data.Customize, _currentSave.Data.Equipment, _currentData.Objects, CustomizationDrawer.Draw(_currentSave.Data.Customize, _currentSave.Data.Equipment, _currentData.Objects,
_identifier is Actor.SpecialIdentifier); _identifier is Actor.SpecialIdentifier);
} }

View file

@ -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<Actor> _actors = Array.Empty<Actor>();
private CustomizationSet _set = null!;
public static void Draw(Customize customize, CharacterEquip equip, IReadOnlyCollection<Actor> 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<SubRace>().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<bool> 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<int> 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<int> 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<int> 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));
}
}
}

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Numerics; using System.Numerics;
using Glamourer.Gui.Customization;
using Glamourer.Interop; using Glamourer.Interop;
using Glamourer.State; using Glamourer.State;
using ImGuiNET; using ImGuiNET;

View file

@ -1,6 +1,4 @@
using System; using System.Numerics;
using System.Numerics;
using System.Reflection;
using Dalamud.Interface; using Dalamud.Interface;
using ImGuiNET; using ImGuiNET;
@ -8,41 +6,12 @@ namespace Glamourer.Gui;
internal partial class Interface 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 Vector2 _spacing = Vector2.Zero;
private static float _actorSelectorWidth; private static float _actorSelectorWidth;
private static float _inputIntSize;
private static float _comboSelectorSize;
private static float _raceSelectorWidth;
private static void UpdateState() private static void UpdateState()
{ {
// General
_spacing = _spacing with { Y = ImGui.GetTextLineHeightWithSpacing() / 2 }; _spacing = _spacing with { Y = ImGui.GetTextLineHeightWithSpacing() / 2 };
_actorSelectorWidth = 200 * ImGuiHelpers.GlobalScale; _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;
} }
} }

View file

@ -3,6 +3,7 @@ using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Logging; using Dalamud.Logging;
using Glamourer.Gui.Customization;
using ImGuiNET; using ImGuiNET;
using OtterGui.Raii; using OtterGui.Raii;
@ -56,6 +57,7 @@ internal partial class Interface : Window, IDisposable
public void Dispose() public void Dispose()
{ {
Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= Toggle; Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= Toggle;
CustomizationDrawer.Dispose();
} }
private static string GetLabel() private static string GetLabel()

View file

@ -159,7 +159,7 @@ public struct CharacterData
public interface ICharacterData public interface ICharacterData
{ {
public CharacterData Data { get; } public ref CharacterData Data { get; }
//public bool ApplyModel(); //public bool ApplyModel();
//public bool ApplyCustomize(Customize target); //public bool ApplyCustomize(Customize target);

View file

@ -8,8 +8,8 @@ namespace Glamourer.State;
public unsafe class CurrentDesign : ICharacterData public unsafe class CurrentDesign : ICharacterData
{ {
public CharacterData Data public ref CharacterData Data
=> _drawData; => ref _drawData;
private CharacterData _drawData; private CharacterData _drawData;
private CharacterData _initialData; private CharacterData _initialData;

View file

@ -90,12 +90,12 @@ public static unsafe class CustomizeExtensions
customize.Load(newCustomize); 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) switch (id)
{ {
case CustomizationId.Race: return customize.ChangeRace(equip, (SubRace)value); case CustomizationId.Race: return customize.ChangeRace(equip, (SubRace)value.Value);
case CustomizationId.Gender: return customize.ChangeGender(equip, (Gender)value); case CustomizationId.Gender: return customize.ChangeGender(equip, (Gender)value.Value);
} }
if (customize[id] == value) if (customize[id] == value)
@ -123,7 +123,7 @@ public static unsafe class CustomizeExtensions
default: default:
var count = set.Count(id); var count = set.Count(id);
if (set.DataByValue(id, customize[id], out _) < 0) 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; break;
} }
} }
@ -131,7 +131,7 @@ public static unsafe class CustomizeExtensions
private static void FixRestrictedGear(Customize customize, CharacterEquip equip, Gender gender, Race race) 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; return;
foreach (var slot in EquipSlotExtensions.EqdpSlots) foreach (var slot in EquipSlotExtensions.EqdpSlots)