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

View file

@ -149,7 +149,7 @@ public partial class CustomizationOptions
HighlightColors = _highlightPicker,
TattooColors = _tattooColorPicker,
LipColorsDark = hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark,
LipColorsLight = hrothgar ? Array.Empty<Customization>() : _lipColorPickerLight,
LipColorsLight = hrothgar ? Array.Empty<CustomizationData>() : _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<Customization>[set.Faces.Count + 1];
var tmp = new IReadOnlyList<CustomizationData>[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<IReadOnlyList<Customization>>(count);
var featureDict = new List<IReadOnlyList<CustomizationData>>(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<Customization>(row.Unknown30);
var hairList = new List<CustomizationData>(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<Customization>(row.Unknown37);
var paintList = new List<CustomizationData>(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<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.TailEarShape)?.Values
.Select((v, i) => FromValueAndIndex(CustomizationId.TailEarShape, v, i)).ToArray()
?? Array.Empty<Customization>();
?? Array.Empty<CustomizationData>();
// 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
.Select((v, i) => FromValueAndIndex(CustomizationId.Face, v, i)).ToArray()
?? Array.Empty<Customization>();
?? Array.Empty<CustomizationData>();
// 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
.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.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<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 IReadOnlyList<Customization> HairColors { get; internal init; } = null!;
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 string Option(CustomizationId id)
=> OptionName[(int)id];
public IReadOnlyList<CharaMakeParams.MenuType> Types { get; internal set; } = null!;
public IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizationId[]> 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<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;
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<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();
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<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)
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;
}

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