Starting rework.

This commit is contained in:
Ottermandias 2022-07-15 13:09:38 +02:00
parent 0fc8992271
commit 7af38aa2ce
58 changed files with 8857 additions and 4923 deletions

View file

@ -0,0 +1,159 @@
using System;
using Glamourer.Customization;
using Penumbra.GameData.Enums;
namespace Glamourer;
public readonly unsafe struct CharacterCustomization
{
public static readonly CharacterCustomization Null = new(null);
private readonly CustomizationData* _data;
public IntPtr Address
=> (IntPtr)_data;
public CharacterCustomization(CustomizationData* data)
=> _data = data;
public ref Race Race
=> ref _data->Race;
public ref SubRace Clan
=> ref _data->Clan;
public Gender Gender
{
get => _data->Gender;
set => _data->Gender = value;
}
public ref byte BodyType
=> ref _data->BodyType;
public ref byte Height
=> ref _data->Height;
public ref byte Face
=> ref _data->Face;
public ref byte Hairstyle
=> ref _data->Hairstyle;
public bool HighlightsOn
{
get => _data->HighlightsOn;
set => _data->HighlightsOn = value;
}
public ref byte SkinColor
=> ref _data->SkinColor;
public ref byte EyeColorRight
=> ref _data->EyeColorRight;
public ref byte HairColor
=> ref _data->HairColor;
public ref byte HighlightsColor
=> ref _data->HighlightsColor;
public ref byte FacialFeatures
=> ref _data->FacialFeatures;
public ref byte TattooColor
=> ref _data->TattooColor;
public ref byte Eyebrow
=> ref _data->Eyebrow;
public ref byte EyeColorLeft
=> ref _data->EyeColorLeft;
public byte EyeShape
{
get => _data->EyeShape;
set => _data->EyeShape = value;
}
public byte FacePaint
{
get => _data->FacePaint;
set => _data->FacePaint = value;
}
public bool FacePaintReversed
{
get => _data->FacePaintReversed;
set => _data->FacePaintReversed = value;
}
public byte Mouth
{
get => _data->Mouth;
set => _data->Mouth = value;
}
public bool SmallIris
{
get => _data->SmallIris;
set => _data->SmallIris = value;
}
public bool Lipstick
{
get => _data->Lipstick;
set => _data->Lipstick = value;
}
public ref byte Nose
=> ref _data->Nose;
public ref byte Jaw
=> ref _data->Jaw;
public ref byte LipColor
=> ref _data->LipColor;
public ref byte MuscleMass
=> ref _data->MuscleMass;
public ref byte TailShape
=> ref _data->TailShape;
public ref byte BustSize
=> ref _data->BustSize;
public ref byte FacePaintColor
=> ref _data->FacePaintColor;
public bool FacialFeature(int idx)
=> _data->FacialFeature(idx);
public void FacialFeature(int idx, bool set)
=> _data->FacialFeature(idx, set);
public byte this[CustomizationId id]
{
get => _data->Get(id);
set => _data->Set(id, value);
}
public static implicit operator CharacterCustomization(CustomizationData* val)
=> new(val);
public static implicit operator CharacterCustomization(IntPtr val)
=> new((CustomizationData*)val);
public static implicit operator bool(CharacterCustomization customize)
=> customize._data != null;
public static bool operator true(CharacterCustomization customize)
=> customize._data != null;
public static bool operator false(CharacterCustomization customize)
=> customize._data == null;
public static bool operator !(CharacterCustomization customize)
=> customize._data == null;
}

View file

@ -0,0 +1,105 @@
using System;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer;
public readonly unsafe struct CharacterEquip
{
public static readonly CharacterEquip Null = new(null);
private readonly CharacterArmor* _armor;
public IntPtr Address
=> (IntPtr)_armor;
public ref CharacterArmor this[int idx]
=> ref _armor[idx];
public ref CharacterArmor this[uint idx]
=> ref _armor[idx];
public ref CharacterArmor this[EquipSlot slot]
=> ref _armor[IndexOf(slot)];
public ref CharacterArmor Head
=> ref _armor[0];
public ref CharacterArmor Body
=> ref _armor[1];
public ref CharacterArmor Hands
=> ref _armor[2];
public ref CharacterArmor Legs
=> ref _armor[3];
public ref CharacterArmor Feet
=> ref _armor[4];
public ref CharacterArmor Ears
=> ref _armor[5];
public ref CharacterArmor Neck
=> ref _armor[6];
public ref CharacterArmor Wrists
=> ref _armor[7];
public ref CharacterArmor RFinger
=> ref _armor[8];
public ref CharacterArmor LFinger
=> ref _armor[9];
public CharacterEquip(CharacterArmor* val)
=> _armor = val;
public static implicit operator CharacterEquip(CharacterArmor* val)
=> new(val);
public static implicit operator CharacterEquip(IntPtr val)
=> new((CharacterArmor*)val);
public static implicit operator CharacterEquip(ReadOnlySpan<CharacterArmor> val)
{
if (val.Length != 10)
throw new ArgumentException("Invalid number of equipment pieces in span.");
fixed (CharacterArmor* ptr = val)
{
return new CharacterEquip(ptr);
}
}
public static implicit operator bool(CharacterEquip equip)
=> equip._armor != null;
public static bool operator true(CharacterEquip equip)
=> equip._armor != null;
public static bool operator false(CharacterEquip equip)
=> equip._armor == null;
public static bool operator !(CharacterEquip equip)
=> equip._armor == null;
private static int IndexOf(EquipSlot slot)
{
return slot switch
{
EquipSlot.Head => 0,
EquipSlot.Body => 1,
EquipSlot.Hands => 2,
EquipSlot.Legs => 3,
EquipSlot.Feet => 4,
EquipSlot.Ears => 5,
EquipSlot.Neck => 6,
EquipSlot.Wrists => 7,
EquipSlot.RFinger => 8,
EquipSlot.LFinger => 9,
_ => throw new ArgumentOutOfRangeException(nameof(slot), slot, null),
};
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using Glamourer.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;

View file

@ -4,6 +4,7 @@ using Lumina.Excel.GeneratedSheets;
namespace Glamourer.Customization;
// A custom version of CharaMakeParams that is easier to parse.
[Sheet("CharaMakeParams")]
public class CharaMakeParams : ExcelRow
{
@ -43,10 +44,9 @@ public class CharaMakeParams : ExcelRow
public uint[] Icons;
}
public LazyRow<Race> Race { get; set; } = null!;
public LazyRow<Tribe> Tribe { get; set; } = null!;
public sbyte Gender { get; set; }
public LazyRow<Race> Race { get; set; } = null!;
public LazyRow<Tribe> Tribe { get; set; } = null!;
public sbyte Gender { get; set; }
public Menu[] Menus { get; set; } = new Menu[NumMenus];
public byte[] Voices { get; set; } = new byte[NumVoices];
@ -60,15 +60,17 @@ public class CharaMakeParams : ExcelRow
Race = new LazyRow<Race>(gameData, parser.ReadColumn<uint>(0), language);
Tribe = new LazyRow<Tribe>(gameData, parser.ReadColumn<uint>(1), language);
Gender = parser.ReadColumn<sbyte>(2);
var currentOffset = 0;
for (var i = 0; i < NumMenus; ++i)
{
Menus[i].Id = parser.ReadColumn<uint>(3 + 0 * NumMenus + i);
Menus[i].InitVal = parser.ReadColumn<byte>(3 + 1 * NumMenus + i);
Menus[i].Type = (MenuType)parser.ReadColumn<byte>(3 + 2 * NumMenus + i);
Menus[i].Size = parser.ReadColumn<byte>(3 + 3 * NumMenus + i);
Menus[i].LookAt = parser.ReadColumn<byte>(3 + 4 * NumMenus + i);
Menus[i].Mask = parser.ReadColumn<uint>(3 + 5 * NumMenus + i);
Menus[i].Customization = (CustomizationId)parser.ReadColumn<uint>(3 + 6 * NumMenus + i);
currentOffset = 3 + i;
Menus[i].Id = parser.ReadColumn<uint>(0 * NumMenus + currentOffset);
Menus[i].InitVal = parser.ReadColumn<byte>(1 * NumMenus + currentOffset);
Menus[i].Type = (MenuType)parser.ReadColumn<byte>(2 * NumMenus + currentOffset);
Menus[i].Size = parser.ReadColumn<byte>(3 * NumMenus + currentOffset);
Menus[i].LookAt = parser.ReadColumn<byte>(4 * NumMenus + currentOffset);
Menus[i].Mask = parser.ReadColumn<uint>(5 * NumMenus + currentOffset);
Menus[i].Customization = (CustomizationId)parser.ReadColumn<uint>(6 * NumMenus + currentOffset);
Menus[i].Values = new uint[Menus[i].Size];
switch (Menus[i].Type)
@ -78,47 +80,42 @@ public class CharaMakeParams : ExcelRow
case MenuType.Percentage:
break;
default:
currentOffset += 7 * NumMenus;
for (var j = 0; j < Menus[i].Size; ++j)
Menus[i].Values[j] = parser.ReadColumn<uint>(3 + (7 + j) * NumMenus + i);
Menus[i].Values[j] = parser.ReadColumn<uint>(j * NumMenus + currentOffset);
break;
}
Menus[i].Graphic = new byte[NumGraphics];
currentOffset = 3 + (MaxNumValues + 7) * NumMenus + i;
for (var j = 0; j < NumGraphics; ++j)
Menus[i].Graphic[j] = parser.ReadColumn<byte>(3 + (MaxNumValues + 7 + j) * NumMenus + i);
Menus[i].Graphic[j] = parser.ReadColumn<byte>(j * NumMenus + currentOffset);
}
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus;
for (var i = 0; i < NumVoices; ++i)
Voices[i] = parser.ReadColumn<byte>(3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + i);
Voices[i] = parser.ReadColumn<byte>(currentOffset++);
for (var i = 0; i < NumFaces; ++i)
{
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + i;
FacialFeatureByFace[i].Icons = new uint[NumFeatures];
for (var j = 0; j < NumFeatures; ++j)
{
FacialFeatureByFace[i].Icons[j] =
(uint)parser.ReadColumn<int>(3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + j * NumFaces + i);
}
FacialFeatureByFace[i].Icons[j] = (uint)parser.ReadColumn<int>(j * NumFaces + currentOffset);
}
for (var i = 0; i < NumEquip; ++i)
{
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7;
Equip[i] = new CharaMakeType.UnkData3347Obj()
{
Helmet = parser.ReadColumn<ulong>(
3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 0),
Top = parser.ReadColumn<ulong>(
3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 1),
Gloves = parser.ReadColumn<ulong>(
3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 2),
Legs = parser.ReadColumn<ulong>(
3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 3),
Shoes = parser.ReadColumn<ulong>(
3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 4),
Weapon = parser.ReadColumn<ulong>(
3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 5),
SubWeapon = parser.ReadColumn<ulong>(
3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 6),
Helmet = parser.ReadColumn<ulong>(currentOffset + 0),
Top = parser.ReadColumn<ulong>(currentOffset + 1),
Gloves = parser.ReadColumn<ulong>(currentOffset + 2),
Legs = parser.ReadColumn<ulong>(currentOffset + 3),
Shoes = parser.ReadColumn<ulong>(currentOffset + 4),
Weapon = parser.ReadColumn<ulong>(currentOffset + 5),
SubWeapon = parser.ReadColumn<ulong>(currentOffset + 6),
};
}
}

View file

@ -1,23 +1,46 @@
using Dalamud.Data;
using Dalamud.Plugin;
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Data;
using Dalamud.Logging;
namespace Glamourer;
namespace Glamourer.Customization;
public class CmpFile
// Convert the Human.Cmp file into color sets.
// If the file can not be read due to TexTools corruption, create a 0-array of size MinSize.
internal class CmpFile
{
public readonly Lumina.Data.FileResource File;
public readonly uint[] RgbaColors;
private readonly Lumina.Data.FileResource? _file;
private readonly uint[] _rgbaColors;
// No error checking since only called internally.
public IEnumerable<uint> GetSlice(int offset, int count)
=> _rgbaColors.Length >= offset + count ? _rgbaColors.Skip(offset).Take(count) : Enumerable.Repeat(0u, count);
public bool Valid
=> _file != null;
public CmpFile(DataManager gameData)
{
File = gameData.GetFile("chara/xls/charamake/human.cmp")!;
RgbaColors = new uint[File.Data.Length >> 2];
for (var i = 0; i < File.Data.Length; i += 4)
try
{
RgbaColors[i >> 2] = File.Data[i]
| (uint)(File.Data[i + 1] << 8)
| (uint)(File.Data[i + 2] << 16)
| (uint)(File.Data[i + 3] << 24);
_file = gameData.GetFile("chara/xls/charamake/human.cmp")!;
_rgbaColors = new uint[_file.Data.Length >> 2];
for (var i = 0; i < _file.Data.Length; i += 4)
{
_rgbaColors[i >> 2] = _file.Data[i]
| (uint)(_file.Data[i + 1] << 8)
| (uint)(_file.Data[i + 2] << 16)
| (uint)(_file.Data[i + 3] << 24);
}
}
catch (Exception e)
{
PluginLog.Error("READ THIS\n======== Could not obtain the human.cmp file which is necessary for color sets.\n"
+ "======== This usually indicates an error with your index files caused by TexTools modifications.\n"
+ "======== If you have used TexTools before, you will probably need to start over in it to use Glamourer.", e);
_file = null;
_rgbaColors = Array.Empty<uint>();
}
}
}

View file

@ -1,47 +1,45 @@
namespace Glamourer.Customization
{
public enum CustomName
{
Clan = 0,
Gender,
Reverse,
OddEyes,
IrisSmall,
IrisLarge,
IrisSize,
MidlanderM,
HighlanderM,
WildwoodM,
DuskwightM,
PlainsfolkM,
DunesfolkM,
SeekerOfTheSunM,
KeeperOfTheMoonM,
SeawolfM,
HellsguardM,
RaenM,
XaelaM,
HelionM,
LostM,
RavaM,
VeenaM,
MidlanderF,
HighlanderF,
WildwoodF,
DuskwightF,
PlainsfolkF,
DunesfolkF,
SeekerOfTheSunF,
KeeperOfTheMoonF,
SeawolfF,
HellsguardF,
RaenF,
XaelaF,
HelionF,
LostF,
RavaF,
VeenaF,
namespace Glamourer.Customization;
Num,
}
// Localization from the game files directly.
public enum CustomName
{
Clan = 0,
Gender,
Reverse,
OddEyes,
IrisSmall,
IrisLarge,
IrisSize,
MidlanderM,
HighlanderM,
WildwoodM,
DuskwightM,
PlainsfolkM,
DunesfolkM,
SeekerOfTheSunM,
KeeperOfTheMoonM,
SeawolfM,
HellsguardM,
RaenM,
XaelaM,
HelionM,
LostM,
RavaM,
VeenaM,
MidlanderF,
HighlanderF,
WildwoodF,
DuskwightF,
PlainsfolkF,
DunesfolkF,
SeekerOfTheSunF,
KeeperOfTheMoonF,
SeawolfF,
HellsguardF,
RaenF,
XaelaF,
HelionF,
LostF,
RavaF,
VeenaF,
}

View file

@ -1,32 +1,33 @@
using System.Runtime.InteropServices;
namespace Glamourer.Customization
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
{
[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)
{
[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;
}
Id = id;
Value = value;
IconId = data;
Color = data;
CustomizeId = customizeId;
}
}

View file

@ -2,59 +2,17 @@
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Game.ClientState.Objects.Types;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization;
public unsafe struct LazyCustomization
{
public CharacterCustomization* Address;
public LazyCustomization(IntPtr characterPtr)
=> Address = (CharacterCustomization*)(characterPtr + CharacterCustomization.CustomizationOffset);
public ref CharacterCustomization Value
=> ref *Address;
public LazyCustomization(CharacterCustomization data)
=> Address = &data;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct CharacterCustomization
public struct CustomizationData
{
public const int CustomizationOffset = 0x830;
public const int CustomizationBytes = 26;
public static CharacterCustomization Default = new()
{
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,
FacialFeatures = 0,
TattooColor = 1,
Eyebrow = 1,
EyeColorLeft = 1,
EyeShape = 1,
Nose = 1,
Jaw = 1,
Mouth = 1,
LipColor = 1,
MuscleMass = 50,
TailShape = 1,
BustSize = 50,
FacePaint = 1,
FacePaintColor = 1,
};
public Race Race;
private byte _gender;
public byte BodyType;
@ -82,21 +40,25 @@ public struct CharacterCustomization
private byte _facePaint;
public byte FacePaintColor;
// Skip Unknown Gender
public Gender Gender
{
get => (Gender)(_gender + 1);
set => _gender = (byte)(value - 1);
}
// Single bit flag.
public bool HighlightsOn
{
get => (_highlightsOn & 128) == 128;
set => _highlightsOn = (byte)(value ? _highlightsOn | 128 : _highlightsOn & 127);
}
// Get status of specific facial feature 0-7.
public bool FacialFeature(int idx)
=> (FacialFeatures & (1 << idx)) != 0;
// Set value of specific facial feature 0-7.
public void FacialFeature(int idx, bool set)
{
if (set)
@ -105,66 +67,108 @@ public struct CharacterCustomization
FacialFeatures &= (byte)~(1 << idx);
}
// Lower 7 bits
public byte EyeShape
{
get => (byte)(_eyeShape & 127);
set => _eyeShape = (byte)((value & 127) | (_eyeShape & 128));
}
// Uppermost bit flag.
public bool SmallIris
{
get => (_eyeShape & 128) == 128;
set => _eyeShape = (byte)(value ? _eyeShape | 128 : _eyeShape & 127);
}
// Lower 7 bits.
public byte Mouth
{
get => (byte)(_mouth & 127);
set => _mouth = (byte)((value & 127) | (_mouth & 128));
}
// Uppermost bit flag.
public bool Lipstick
{
get => (_mouth & 128) == 128;
set => _mouth = (byte)(value ? _mouth | 128 : _mouth & 127);
}
// Lower 7 bits.
public byte FacePaint
{
get => (byte)(_facePaint & 127);
set => _facePaint = (byte)((value & 127) | (_facePaint & 128));
}
// Uppermost bit flag.
public bool FacePaintReversed
{
get => (_facePaint & 128) == 128;
set => _facePaint = (byte)(value ? _facePaint | 128 : _facePaint & 127);
}
public unsafe void Read(IntPtr customizeAddress)
public static CustomizationData Default = new()
{
fixed (Race* ptr = &Race)
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,
FacialFeatures = 0,
TattooColor = 1,
Eyebrow = 1,
EyeColorLeft = 1,
EyeShape = 1,
Nose = 1,
Jaw = 1,
Mouth = 1,
LipColor = 1,
MuscleMass = 50,
TailShape = 1,
BustSize = 50,
FacePaint = 1,
FacePaintColor = 1,
};
public unsafe void Read(CustomizationData* customize)
{
fixed (CustomizationData* ptr = &this)
{
Buffer.MemoryCopy(customizeAddress.ToPointer(), ptr, CustomizationBytes, CustomizationBytes);
*ptr = *customize;
}
}
public unsafe void Read(Customization* customize)
=> Read((IntPtr)customize);
public unsafe void Read(IntPtr customizeAddress)
=> Read((CustomizationData*)customizeAddress);
public void Read(Character character)
=> Read(character.Address + CustomizationOffset);
public CharacterCustomization(Character character)
public unsafe void Read(Human* human)
=> Read((CustomizationData*)human->CustomizeData);
public CustomizationData(Character character)
: this()
{
Read(character.Address + CustomizationOffset);
}
public byte this[CustomizationId id]
public unsafe CustomizationData(Human* human)
: this()
{
get => id switch
Read(human);
}
public byte Get(CustomizationId id)
=> id switch
{
CustomizationId.Race => (byte)Race,
CustomizationId.Gender => (byte)Gender,
@ -194,100 +198,59 @@ public struct CharacterCustomization
CustomizationId.FacePaintColor => FacePaintColor,
_ => throw new ArgumentOutOfRangeException(nameof(id), id, null),
};
set
public void Set(CustomizationId id, byte value)
{
switch (id)
{
switch (id)
{
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:
FacialFeatures = value;
break;
case CustomizationId.TattooColor:
TattooColor = value;
break;
case CustomizationId.Eyebrows:
Eyebrow = 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: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: FacialFeatures = value; break;
case CustomizationId.TattooColor: TattooColor = value; break;
case CustomizationId.Eyebrows: Eyebrow = 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 byte this[CustomizationId id]
{
get => Get(id);
set => Set(id, value);
}
public unsafe void Write(FFXIVClientStructs.FFXIV.Client.Game.Character.Character* character)
{
fixed (CustomizationData* ptr = &this)
{
Buffer.MemoryCopy(ptr, character->CustomizeData, CustomizationBytes, CustomizationBytes);
}
}
public unsafe void Write(IntPtr characterAddress)
{
fixed (Race* ptr = &Race)
{
Buffer.MemoryCopy(ptr, (byte*)characterAddress + CustomizationOffset, CustomizationBytes, CustomizationBytes);
}
}
=> Write((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)characterAddress);
public unsafe void WriteBytes(byte[] array, int offset = 0)
{

View file

@ -1,108 +1,107 @@
using System;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization
namespace Glamourer.Customization;
public enum CustomizationId : byte
{
public enum CustomizationId : byte
{
Race = 0,
Gender = 1,
BodyType = 2,
Height = 3,
Clan = 4,
Face = 5,
Hairstyle = 6,
HighlightsOnFlag = 7,
SkinColor = 8,
EyeColorR = 9,
HairColor = 10,
HighlightColor = 11,
FacialFeaturesTattoos = 12, // Bitmask, 1-7 per face, 8 is 1.0 tattoo
TattooColor = 13,
Eyebrows = 14,
EyeColorL = 15,
EyeShape = 16, // Flag 128 for Small
Nose = 17,
Jaw = 18,
Mouth = 19, // Flag 128 for Lip Color set
LipColor = 20, // Flag 128 for Light instead of Dark
MuscleToneOrTailEarLength = 21,
TailEarShape = 22,
BustSize = 23,
FacePaint = 24,
FacePaintColor = 25, // Flag 128 for Light instead of Dark.
}
public static class CustomizationExtensions
{
public static string ToDefaultName(this CustomizationId customizationId)
=> customizationId switch
{
CustomizationId.Race => "Race",
CustomizationId.Gender => "Gender",
CustomizationId.BodyType => "Body Type",
CustomizationId.Height => "Height",
CustomizationId.Clan => "Clan",
CustomizationId.Face => "Head Style",
CustomizationId.Hairstyle => "Hair Style",
CustomizationId.HighlightsOnFlag => "Highlights",
CustomizationId.SkinColor => "Skin Color",
CustomizationId.EyeColorR => "Right Eye Color",
CustomizationId.HairColor => "Hair Color",
CustomizationId.HighlightColor => "Highlights Color",
CustomizationId.FacialFeaturesTattoos => "Facial Features",
CustomizationId.TattooColor => "Tattoo Color",
CustomizationId.Eyebrows => "Eyebrow Style",
CustomizationId.EyeColorL => "Left Eye Color",
CustomizationId.EyeShape => "Eye Shape",
CustomizationId.Nose => "Nose Style",
CustomizationId.Jaw => "Jaw Style",
CustomizationId.Mouth => "Mouth Style",
CustomizationId.MuscleToneOrTailEarLength => "Muscle Tone",
CustomizationId.TailEarShape => "Tail Shape",
CustomizationId.BustSize => "Bust Size",
CustomizationId.FacePaint => "Face Paint",
CustomizationId.FacePaintColor => "Face Paint Color",
CustomizationId.LipColor => "Lip Color",
_ => throw new ArgumentOutOfRangeException(nameof(customizationId), customizationId, null),
};
public static CharaMakeParams.MenuType ToType(this CustomizationId customizationId, Race race = Race.Hyur)
=> customizationId switch
{
CustomizationId.Race => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Gender => CharaMakeParams.MenuType.IconSelector,
CustomizationId.BodyType => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Height => CharaMakeParams.MenuType.Percentage,
CustomizationId.Clan => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Face => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Hairstyle => CharaMakeParams.MenuType.IconSelector,
CustomizationId.HighlightsOnFlag => CharaMakeParams.MenuType.ListSelector,
CustomizationId.SkinColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.EyeColorR => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.HairColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.HighlightColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.FacialFeaturesTattoos => CharaMakeParams.MenuType.MultiIconSelector,
CustomizationId.TattooColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.Eyebrows => CharaMakeParams.MenuType.ListSelector,
CustomizationId.EyeColorL => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.EyeShape => CharaMakeParams.MenuType.ListSelector,
CustomizationId.Nose => CharaMakeParams.MenuType.ListSelector,
CustomizationId.Jaw => CharaMakeParams.MenuType.ListSelector,
CustomizationId.Mouth => CharaMakeParams.MenuType.ListSelector,
CustomizationId.MuscleToneOrTailEarLength => CharaMakeParams.MenuType.Percentage,
CustomizationId.BustSize => CharaMakeParams.MenuType.Percentage,
CustomizationId.FacePaint => CharaMakeParams.MenuType.IconSelector,
CustomizationId.FacePaintColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.TailEarShape => race == Race.Elezen || race == Race.Lalafell
? CharaMakeParams.MenuType.ListSelector
: CharaMakeParams.MenuType.IconSelector,
CustomizationId.LipColor => race == Race.Hrothgar
? CharaMakeParams.MenuType.IconSelector
: CharaMakeParams.MenuType.ColorPicker,
_ => throw new ArgumentOutOfRangeException(nameof(customizationId), customizationId, null),
};
}
Race = 0,
Gender = 1,
BodyType = 2,
Height = 3,
Clan = 4,
Face = 5,
Hairstyle = 6,
HighlightsOnFlag = 7,
SkinColor = 8,
EyeColorR = 9,
HairColor = 10,
HighlightColor = 11,
FacialFeaturesTattoos = 12, // Bitmask, 1-7 per face, 8 is 1.0 tattoo
TattooColor = 13,
Eyebrows = 14,
EyeColorL = 15,
EyeShape = 16, // Flag 128 for Small
Nose = 17,
Jaw = 18,
Mouth = 19, // Flag 128 for Lip Color set
LipColor = 20, // Flag 128 for Light instead of Dark
MuscleToneOrTailEarLength = 21,
TailEarShape = 22,
BustSize = 23,
FacePaint = 24,
FacePaintColor = 25, // Flag 128 for Light instead of Dark.
}
public static class CustomizationExtensions
{
public static string ToDefaultName(this CustomizationId customizationId)
=> customizationId switch
{
CustomizationId.Race => "Race",
CustomizationId.Gender => "Gender",
CustomizationId.BodyType => "Body Type",
CustomizationId.Height => "Height",
CustomizationId.Clan => "Clan",
CustomizationId.Face => "Head Style",
CustomizationId.Hairstyle => "Hair Style",
CustomizationId.HighlightsOnFlag => "Highlights",
CustomizationId.SkinColor => "Skin Color",
CustomizationId.EyeColorR => "Right Eye Color",
CustomizationId.HairColor => "Hair Color",
CustomizationId.HighlightColor => "Highlights Color",
CustomizationId.FacialFeaturesTattoos => "Facial Features",
CustomizationId.TattooColor => "Tattoo Color",
CustomizationId.Eyebrows => "Eyebrow Style",
CustomizationId.EyeColorL => "Left Eye Color",
CustomizationId.EyeShape => "Eye Shape",
CustomizationId.Nose => "Nose Style",
CustomizationId.Jaw => "Jaw Style",
CustomizationId.Mouth => "Mouth Style",
CustomizationId.MuscleToneOrTailEarLength => "Muscle Tone",
CustomizationId.TailEarShape => "Tail Shape",
CustomizationId.BustSize => "Bust Size",
CustomizationId.FacePaint => "Face Paint",
CustomizationId.FacePaintColor => "Face Paint Color",
CustomizationId.LipColor => "Lip Color",
_ => throw new ArgumentOutOfRangeException(nameof(customizationId), customizationId, null),
};
public static CharaMakeParams.MenuType ToType(this CustomizationId customizationId, Race race = Race.Hyur)
=> customizationId switch
{
CustomizationId.Race => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Gender => CharaMakeParams.MenuType.IconSelector,
CustomizationId.BodyType => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Height => CharaMakeParams.MenuType.Percentage,
CustomizationId.Clan => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Face => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Hairstyle => CharaMakeParams.MenuType.IconSelector,
CustomizationId.HighlightsOnFlag => CharaMakeParams.MenuType.ListSelector,
CustomizationId.SkinColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.EyeColorR => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.HairColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.HighlightColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.FacialFeaturesTattoos => CharaMakeParams.MenuType.MultiIconSelector,
CustomizationId.TattooColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.Eyebrows => CharaMakeParams.MenuType.ListSelector,
CustomizationId.EyeColorL => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.EyeShape => CharaMakeParams.MenuType.ListSelector,
CustomizationId.Nose => CharaMakeParams.MenuType.ListSelector,
CustomizationId.Jaw => CharaMakeParams.MenuType.ListSelector,
CustomizationId.Mouth => CharaMakeParams.MenuType.ListSelector,
CustomizationId.MuscleToneOrTailEarLength => CharaMakeParams.MenuType.Percentage,
CustomizationId.BustSize => CharaMakeParams.MenuType.Percentage,
CustomizationId.FacePaint => CharaMakeParams.MenuType.IconSelector,
CustomizationId.FacePaintColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.TailEarShape => race is Race.Elezen or Race.Lalafell
? CharaMakeParams.MenuType.ListSelector
: CharaMakeParams.MenuType.IconSelector,
CustomizationId.LipColor => race == Race.Hrothgar
? CharaMakeParams.MenuType.IconSelector
: CharaMakeParams.MenuType.ColorPicker,
_ => throw new ArgumentOutOfRangeException(nameof(customizationId), customizationId, null),
};
}

View file

@ -5,7 +5,7 @@ using System.Reflection;
using Dalamud;
using Dalamud.Data;
using Dalamud.Plugin;
using Lumina.Data;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using OtterGui.Classes;
@ -14,360 +14,407 @@ using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.Customization;
// Generate everything about customization per tribe and gender.
public partial class CustomizationOptions
{
internal static readonly Race[] Races = ((Race[])Enum.GetValues(typeof(Race))).Skip(1).ToArray();
// All races except for Unknown
internal static readonly Race[] Races = ((Race[])Enum.GetValues(typeof(Race))).Skip(1).ToArray();
// All tribes except for Unknown
internal static readonly SubRace[] Clans = ((SubRace[])Enum.GetValues(typeof(SubRace))).Skip(1).ToArray();
// Two genders.
internal static readonly Gender[] Genders =
{
Gender.Male,
Gender.Female,
};
// Every tribe and gender has a separate set of available customizations.
internal CustomizationSet GetList(SubRace race, Gender gender)
=> _list[ToIndex(race, gender)];
=> _customizationSets[ToIndex(race, gender)];
// Get specific icons.
internal ImGuiScene.TextureWrap GetIcon(uint id)
=> _icons.LoadIcon(id);
private static readonly int ListSize = Clans.Length * Genders.Length;
private readonly IconStorage _icons;
private readonly CustomizationSet[] _list = new CustomizationSet[ListSize];
private readonly IconStorage _icons;
private static readonly int ListSize = Clans.Length * Genders.Length;
private readonly CustomizationSet[] _customizationSets = new CustomizationSet[ListSize];
// Get the index for the given pair of tribe and gender.
private static int ToIndex(SubRace race, Gender gender)
{
var idx = ((int)race - 1) * Genders.Length + (gender == Gender.Female ? 1 : 0);
if (idx < 0 || idx >= ListSize)
ThrowException(race, gender);
return idx;
}
private static void ThrowException(SubRace race, Gender gender)
=> throw new Exception($"Invalid customization requested for {race} {gender}.");
}
private static int ToIndex(SubRace race, Gender gender)
{
if (race == SubRace.Unknown || gender != Gender.Female && gender != Gender.Male)
ThrowException(race, gender);
var ret = (int)race - 1;
ret = ret * Genders.Length + (gender == Gender.Female ? 1 : 0);
return ret;
}
private Customization[] GetHairStyles(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var hairList = new List<Customization>(row.Unknown30);
for (var i = 0; i < row.Unknown30; ++i)
{
var name = $"Unknown{66 + i * 9}";
var customizeIdx =
(uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue)
continue;
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, 0));
}
return hairList.ToArray();
}
private Customization[] CreateColorPicker(CustomizationId id, int offset, int num, bool light = false)
=> _cmpFile.RgbaColors.Skip(offset).Take(num)
.Select((c, i) => new Customization(id, (byte)(light ? 128 + i : 0 + i), c, (ushort)(offset + i)))
.ToArray();
private (Customization[], Customization[]) GetColors(SubRace race, Gender gender)
{
if (race > SubRace.Veena || race == SubRace.Unknown)
throw new ArgumentOutOfRangeException(nameof(race), race, null);
var gv = gender == Gender.Male ? 0 : 1;
var idx = ((int)race * 2 + gv) * 5 + 3;
return (CreateColorPicker(CustomizationId.SkinColor, idx << 8, 192),
CreateColorPicker(CustomizationId.HairColor, (idx + 1) << 8, 192));
}
private Customization FromValueAndIndex(CustomizationId id, uint value, int index)
{
var row = _customizeSheet.GetRow(value);
return row == null
? new Customization(id, (byte)(index + 1), value, 0)
: new Customization(id, row.FeatureID, row.Icon, (ushort)row.RowId);
}
private static int GetListSize(CharaMakeParams row, CustomizationId id)
{
var menu = row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == id);
return menu?.Size ?? 0;
}
private Customization[] GetFacePaints(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var paintList = new List<Customization>(row.Unknown37);
for (var i = 0; i < row.Unknown37; ++i)
{
var name = $"Unknown{73 + i * 9}";
var customizeIdx =
(uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue)
continue;
var paintRow = _customizeSheet.GetRow(customizeIdx);
paintList.Add(paintRow != null
? new Customization(CustomizationId.FacePaint, paintRow.FeatureID, paintRow.Icon, (ushort)paintRow.RowId)
: new Customization(CustomizationId.FacePaint, (byte)i, customizeIdx, 0));
}
return paintList.ToArray();
}
private Customization[] 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>();
private Customization[] 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>();
private Customization[] 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>();
private Customization[] HrothgarFaces(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.Hairstyle)?.Values
.Select((v, i) => FromValueAndIndex(CustomizationId.Hairstyle, v, i)).ToArray()
?? Array.Empty<Customization>();
private CustomizationSet GetSet(SubRace race, Gender gender)
{
var (skin, hair) = GetColors(race, gender);
var row = _listSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var set = new CustomizationSet(race, gender)
{
HairStyles = GetHairStyles(race, gender),
HairColors = hair,
SkinColors = skin,
EyeColors = _eyeColorPicker,
HighlightColors = _highlightPicker,
TattooColors = _tattooColorPicker,
LipColorsDark = race.ToRace() == Race.Hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark,
LipColorsLight = race.ToRace() == Race.Hrothgar ? Array.Empty<Customization>() : _lipColorPickerLight,
FacePaintColorsDark = _facePaintColorPickerDark,
FacePaintColorsLight = _facePaintColorPickerLight,
Faces = GetFaces(row),
NumEyebrows = GetListSize(row, CustomizationId.Eyebrows),
NumEyeShapes = GetListSize(row, CustomizationId.EyeShape),
NumNoseShapes = GetListSize(row, CustomizationId.Nose),
NumJawShapes = GetListSize(row, CustomizationId.Jaw),
NumMouthShapes = GetListSize(row, CustomizationId.Mouth),
FacePaints = GetFacePaints(race, gender),
TailEarShapes = GetTailEarShapes(row),
};
if (GetListSize(row, CustomizationId.BustSize) > 0)
set.SetAvailable(CustomizationId.BustSize);
if (GetListSize(row, CustomizationId.MuscleToneOrTailEarLength) > 0)
set.SetAvailable(CustomizationId.MuscleToneOrTailEarLength);
if (set.NumEyebrows > 0)
set.SetAvailable(CustomizationId.Eyebrows);
if (set.NumEyeShapes > 0)
set.SetAvailable(CustomizationId.EyeShape);
if (set.NumNoseShapes > 0)
set.SetAvailable(CustomizationId.Nose);
if (set.NumJawShapes > 0)
set.SetAvailable(CustomizationId.Jaw);
if (set.NumMouthShapes > 0)
set.SetAvailable(CustomizationId.Mouth);
if (set.FacePaints.Count > 0)
{
set.SetAvailable(CustomizationId.FacePaint);
set.SetAvailable(CustomizationId.FacePaintColor);
}
if (set.TailEarShapes.Count > 0)
set.SetAvailable(CustomizationId.TailEarShape);
if (set.Faces.Count > 0)
set.SetAvailable(CustomizationId.Face);
var count = set.Faces.Count;
var featureDict = new List<IReadOnlyList<Customization>>(count);
for (var i = 0; i < count; ++i)
{
featureDict.Add(row.FacialFeatureByFace[i].Icons.Select((val, idx)
=> new Customization(CustomizationId.FacialFeaturesTattoos, (byte)(1 << idx), val, (ushort)(i * 8 + idx)))
.Append(new Customization(CustomizationId.FacialFeaturesTattoos, 1 << 7, 137905, (ushort)((i + 1) * 8)))
.ToArray());
}
set.FeaturesTattoos = featureDict;
var nameArray = ((CustomizationId[])Enum.GetValues(typeof(CustomizationId))).Select(c =>
{
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customization == c);
if (menu == null)
{
if (c == CustomizationId.HighlightsOnFlag)
return _lobby.GetRow(237)?.Text.ToString() ?? "Highlights";
return c.ToDefaultName();
}
if (c == CustomizationId.FacialFeaturesTattoos)
return
$"{_lobby.GetRow(1741)?.Text.ToString() ?? "Facial Features"} & {_lobby.GetRow(1742)?.Text.ToString() ?? "Tattoos"}";
var textRow = _lobby.GetRow(menu.Value.Id);
return textRow?.Text.ToString() ?? c.ToDefaultName();
}).ToArray();
nameArray[(int)CustomizationId.EyeColorL] = nameArray[(int)CustomizationId.EyeColorR];
nameArray[(int)CustomizationId.EyeColorR] = GetName(CustomName.OddEyes);
set.OptionName = nameArray;
set.Types = ((CustomizationId[])Enum.GetValues(typeof(CustomizationId))).Select(c =>
{
switch (c)
{
case CustomizationId.HighlightColor:
case CustomizationId.EyeColorL:
case CustomizationId.EyeColorR:
return CharaMakeParams.MenuType.ColorPicker;
}
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customization == c);
return menu?.Type ?? CharaMakeParams.MenuType.ListSelector;
}).ToArray();
return set;
}
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet;
private readonly ExcelSheet<CharaMakeParams> _listSheet;
private readonly ExcelSheet<HairMakeType> _hairSheet;
private readonly ExcelSheet<Lobby> _lobby;
private readonly CmpFile _cmpFile;
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 string[] _names = new string[(int)CustomName.Num];
public partial class CustomizationOptions
{
internal readonly bool Valid;
public string GetName(CustomName name)
=> _names[(int)name];
private static Language FromClientLanguage(ClientLanguage language)
=> language switch
{
ClientLanguage.English => Language.English,
ClientLanguage.French => Language.French,
ClientLanguage.German => Language.German,
ClientLanguage.Japanese => Language.Japanese,
_ => Language.English,
};
internal CustomizationOptions(DalamudPluginInterface pi, DataManager gameData, ClientLanguage language)
{
try
{
_cmpFile = new CmpFile(gameData);
}
catch (Exception e)
{
throw new Exception("READ THIS\n======== Could not obtain the human.cmp file which is necessary for color sets.\n"
+ "======== This usually indicates an error with your index files caused by TexTools modifications.\n"
+ "======== If you have used TexTools before, you will probably need to start over in it to use Glamourer.", e);
}
_customizeSheet = gameData.GetExcelSheet<CharaMakeCustomize>()!;
_lobby = gameData.GetExcelSheet<Lobby>()!;
var tmp = gameData.Excel.GetType()!.GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)!
.MakeGenericMethod(typeof(CharaMakeParams))!.Invoke(gameData.Excel, new object?[]
{
"charamaketype",
FromClientLanguage(language),
null,
}) as ExcelSheet<CharaMakeParams>;
_listSheet = tmp!;
_hairSheet = gameData.GetExcelSheet<HairMakeType>()!;
SetNames(gameData);
_highlightPicker = CreateColorPicker(CustomizationId.HighlightColor, 256, 192);
_lipColorPickerDark = CreateColorPicker(CustomizationId.LipColor, 512, 96);
_lipColorPickerLight = CreateColorPicker(CustomizationId.LipColor, 1024, 96, true);
_eyeColorPicker = CreateColorPicker(CustomizationId.EyeColorL, 0, 192);
_facePaintColorPickerDark = CreateColorPicker(CustomizationId.FacePaintColor, 640, 96);
_facePaintColorPickerLight = CreateColorPicker(CustomizationId.FacePaintColor, 1152, 96, true);
_tattooColorPicker = CreateColorPicker(CustomizationId.TattooColor, 0, 192);
_icons = new IconStorage(pi, gameData, _list.Length * 50);
var tmp = new TemporaryData(gameData, this, language);
_icons = new IconStorage(pi, gameData, _customizationSets.Length * 50);
Valid = tmp.Valid;
SetNames(gameData, tmp);
foreach (var race in Clans)
{
foreach (var gender in Genders)
_list[ToIndex(race, gender)] = GetSet(race, gender);
_customizationSets[ToIndex(race, gender)] = tmp.GetSet(race, gender);
}
}
private void SetNames(DataManager gameData)
// Obtain localized names of customization options and race names from the game data.
private readonly string[] _names = new string[Enum.GetValues<CustomName>().Length];
private void SetNames(DataManager gameData, TemporaryData tmp)
{
var subRace = gameData.GetExcelSheet<Tribe>()!;
_names[(int)CustomName.Clan] = _lobby.GetRow(102)?.Text ?? "Clan";
_names[(int)CustomName.Gender] = _lobby.GetRow(103)?.Text ?? "Gender";
_names[(int)CustomName.Reverse] = _lobby.GetRow(2135)?.Text ?? "Reverse";
_names[(int)CustomName.OddEyes] = _lobby.GetRow(2125)?.Text ?? "Odd Eyes";
_names[(int)CustomName.IrisSmall] = _lobby.GetRow(1076)?.Text ?? "Small";
_names[(int)CustomName.IrisLarge] = _lobby.GetRow(1075)?.Text ?? "Large";
_names[(int)CustomName.IrisSize] = _lobby.GetRow(244)?.Text ?? "Iris Size";
_names[(int)CustomName.MidlanderM] = subRace.GetRow((int)SubRace.Midlander)?.Masculine.ToString() ?? SubRace.Midlander.ToName();
_names[(int)CustomName.MidlanderF] = subRace.GetRow((int)SubRace.Midlander)?.Feminine.ToString() ?? SubRace.Midlander.ToName();
_names[(int)CustomName.HighlanderM] =
subRace.GetRow((int)SubRace.Highlander)?.Masculine.ToString() ?? SubRace.Highlander.ToName();
_names[(int)CustomName.HighlanderF] = subRace.GetRow((int)SubRace.Highlander)?.Feminine.ToString() ?? SubRace.Highlander.ToName();
_names[(int)CustomName.WildwoodM] = subRace.GetRow((int)SubRace.Wildwood)?.Masculine.ToString() ?? SubRace.Wildwood.ToName();
_names[(int)CustomName.WildwoodF] = subRace.GetRow((int)SubRace.Wildwood)?.Feminine.ToString() ?? SubRace.Wildwood.ToName();
_names[(int)CustomName.DuskwightM] = subRace.GetRow((int)SubRace.Duskwight)?.Masculine.ToString() ?? SubRace.Duskwight.ToName();
_names[(int)CustomName.DuskwightF] = subRace.GetRow((int)SubRace.Duskwight)?.Feminine.ToString() ?? SubRace.Duskwight.ToName();
_names[(int)CustomName.PlainsfolkM] =
subRace.GetRow((int)SubRace.Plainsfolk)?.Masculine.ToString() ?? SubRace.Plainsfolk.ToName();
_names[(int)CustomName.PlainsfolkF] = subRace.GetRow((int)SubRace.Plainsfolk)?.Feminine.ToString() ?? SubRace.Plainsfolk.ToName();
_names[(int)CustomName.DunesfolkM] = subRace.GetRow((int)SubRace.Dunesfolk)?.Masculine.ToString() ?? SubRace.Dunesfolk.ToName();
_names[(int)CustomName.DunesfolkF] = subRace.GetRow((int)SubRace.Dunesfolk)?.Feminine.ToString() ?? SubRace.Dunesfolk.ToName();
_names[(int)CustomName.SeekerOfTheSunM] =
subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Masculine.ToString() ?? SubRace.SeekerOfTheSun.ToName();
_names[(int)CustomName.SeekerOfTheSunF] =
subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Feminine.ToString() ?? SubRace.SeekerOfTheSun.ToName();
_names[(int)CustomName.KeeperOfTheMoonM] =
subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Masculine.ToString() ?? SubRace.KeeperOfTheMoon.ToName();
_names[(int)CustomName.KeeperOfTheMoonF] =
subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Feminine.ToString() ?? SubRace.KeeperOfTheMoon.ToName();
_names[(int)CustomName.SeawolfM] = subRace.GetRow((int)SubRace.Seawolf)?.Masculine.ToString() ?? SubRace.Seawolf.ToName();
_names[(int)CustomName.SeawolfF] = subRace.GetRow((int)SubRace.Seawolf)?.Feminine.ToString() ?? SubRace.Seawolf.ToName();
_names[(int)CustomName.HellsguardM] =
subRace.GetRow((int)SubRace.Hellsguard)?.Masculine.ToString() ?? SubRace.Hellsguard.ToName();
_names[(int)CustomName.HellsguardF] = subRace.GetRow((int)SubRace.Hellsguard)?.Feminine.ToString() ?? SubRace.Hellsguard.ToName();
_names[(int)CustomName.RaenM] = subRace.GetRow((int)SubRace.Raen)?.Masculine.ToString() ?? SubRace.Raen.ToName();
_names[(int)CustomName.RaenF] = subRace.GetRow((int)SubRace.Raen)?.Feminine.ToString() ?? SubRace.Raen.ToName();
_names[(int)CustomName.XaelaM] = subRace.GetRow((int)SubRace.Xaela)?.Masculine.ToString() ?? SubRace.Xaela.ToName();
_names[(int)CustomName.XaelaF] = subRace.GetRow((int)SubRace.Xaela)?.Feminine.ToString() ?? SubRace.Xaela.ToName();
_names[(int)CustomName.HelionM] = subRace.GetRow((int)SubRace.Helion)?.Masculine.ToString() ?? SubRace.Helion.ToName();
_names[(int)CustomName.HelionF] = subRace.GetRow((int)SubRace.Helion)?.Feminine.ToString() ?? SubRace.Helion.ToName();
_names[(int)CustomName.LostM] = subRace.GetRow((int)SubRace.Lost)?.Masculine.ToString() ?? SubRace.Lost.ToName();
_names[(int)CustomName.LostF] = subRace.GetRow((int)SubRace.Lost)?.Feminine.ToString() ?? SubRace.Lost.ToName();
_names[(int)CustomName.RavaM] = subRace.GetRow((int)SubRace.Rava)?.Masculine.ToString() ?? SubRace.Rava.ToName();
_names[(int)CustomName.RavaF] = subRace.GetRow((int)SubRace.Rava)?.Feminine.ToString() ?? SubRace.Rava.ToName();
_names[(int)CustomName.VeenaM] = subRace.GetRow((int)SubRace.Veena)?.Masculine.ToString() ?? SubRace.Veena.ToName();
_names[(int)CustomName.VeenaF] = subRace.GetRow((int)SubRace.Veena)?.Feminine.ToString() ?? SubRace.Veena.ToName();
void Set(CustomName id, Lumina.Text.SeString? s, string def)
=> _names[(int)id] = s?.ToDalamudString().TextValue ?? def;
Set(CustomName.Clan, tmp.Lobby.GetRow(102)?.Text, "Clan");
Set(CustomName.Gender, tmp.Lobby.GetRow(103)?.Text, "Gender");
Set(CustomName.Reverse, tmp.Lobby.GetRow(2135)?.Text, "Reverse");
Set(CustomName.OddEyes, tmp.Lobby.GetRow(2125)?.Text, "Odd Eyes");
Set(CustomName.IrisSmall, tmp.Lobby.GetRow(1076)?.Text, "Small");
Set(CustomName.IrisLarge, tmp.Lobby.GetRow(1075)?.Text, "Large");
Set(CustomName.IrisSize, tmp.Lobby.GetRow(244)?.Text, "Iris Size");
Set(CustomName.MidlanderM, subRace.GetRow((int)SubRace.Midlander)?.Masculine, SubRace.Midlander.ToName());
Set(CustomName.MidlanderF, subRace.GetRow((int)SubRace.Midlander)?.Feminine, SubRace.Midlander.ToName());
Set(CustomName.HighlanderM, subRace.GetRow((int)SubRace.Highlander)?.Masculine, SubRace.Highlander.ToName());
Set(CustomName.HighlanderF, subRace.GetRow((int)SubRace.Highlander)?.Feminine, SubRace.Highlander.ToName());
Set(CustomName.WildwoodM, subRace.GetRow((int)SubRace.Wildwood)?.Masculine, SubRace.Wildwood.ToName());
Set(CustomName.WildwoodF, subRace.GetRow((int)SubRace.Wildwood)?.Feminine, SubRace.Wildwood.ToName());
Set(CustomName.DuskwightM, subRace.GetRow((int)SubRace.Duskwight)?.Masculine, SubRace.Duskwight.ToName());
Set(CustomName.DuskwightF, subRace.GetRow((int)SubRace.Duskwight)?.Feminine, SubRace.Duskwight.ToName());
Set(CustomName.PlainsfolkM, subRace.GetRow((int)SubRace.Plainsfolk)?.Masculine, SubRace.Plainsfolk.ToName());
Set(CustomName.PlainsfolkF, subRace.GetRow((int)SubRace.Plainsfolk)?.Feminine, SubRace.Plainsfolk.ToName());
Set(CustomName.DunesfolkM, subRace.GetRow((int)SubRace.Dunesfolk)?.Masculine, SubRace.Dunesfolk.ToName());
Set(CustomName.DunesfolkF, subRace.GetRow((int)SubRace.Dunesfolk)?.Feminine, SubRace.Dunesfolk.ToName());
Set(CustomName.SeekerOfTheSunM, subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Masculine, SubRace.SeekerOfTheSun.ToName());
Set(CustomName.SeekerOfTheSunF, subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Feminine, SubRace.SeekerOfTheSun.ToName());
Set(CustomName.KeeperOfTheMoonM, subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Masculine, SubRace.KeeperOfTheMoon.ToName());
Set(CustomName.KeeperOfTheMoonF, subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Feminine, SubRace.KeeperOfTheMoon.ToName());
Set(CustomName.SeawolfM, subRace.GetRow((int)SubRace.Seawolf)?.Masculine, SubRace.Seawolf.ToName());
Set(CustomName.SeawolfF, subRace.GetRow((int)SubRace.Seawolf)?.Feminine, SubRace.Seawolf.ToName());
Set(CustomName.HellsguardM, subRace.GetRow((int)SubRace.Hellsguard)?.Masculine, SubRace.Hellsguard.ToName());
Set(CustomName.HellsguardF, subRace.GetRow((int)SubRace.Hellsguard)?.Feminine, SubRace.Hellsguard.ToName());
Set(CustomName.RaenM, subRace.GetRow((int)SubRace.Raen)?.Masculine, SubRace.Raen.ToName());
Set(CustomName.RaenF, subRace.GetRow((int)SubRace.Raen)?.Feminine, SubRace.Raen.ToName());
Set(CustomName.XaelaM, subRace.GetRow((int)SubRace.Xaela)?.Masculine, SubRace.Xaela.ToName());
Set(CustomName.XaelaF, subRace.GetRow((int)SubRace.Xaela)?.Feminine, SubRace.Xaela.ToName());
Set(CustomName.HelionM, subRace.GetRow((int)SubRace.Helion)?.Masculine, SubRace.Helion.ToName());
Set(CustomName.HelionF, subRace.GetRow((int)SubRace.Helion)?.Feminine, SubRace.Helion.ToName());
Set(CustomName.LostM, subRace.GetRow((int)SubRace.Lost)?.Masculine, SubRace.Lost.ToName());
Set(CustomName.LostF, subRace.GetRow((int)SubRace.Lost)?.Feminine, SubRace.Lost.ToName());
Set(CustomName.RavaM, subRace.GetRow((int)SubRace.Rava)?.Masculine, SubRace.Rava.ToName());
Set(CustomName.RavaF, subRace.GetRow((int)SubRace.Rava)?.Feminine, SubRace.Rava.ToName());
Set(CustomName.VeenaM, subRace.GetRow((int)SubRace.Veena)?.Masculine, SubRace.Veena.ToName());
Set(CustomName.VeenaF, subRace.GetRow((int)SubRace.Veena)?.Feminine, SubRace.Veena.ToName());
}
private class TemporaryData
{
public bool Valid
=> _cmpFile.Valid;
public CustomizationSet GetSet(SubRace race, Gender gender)
{
var (skin, hair) = GetColors(race, gender);
var row = _listSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
// Create the initial set with all the easily accessible parameters available for anyone.
var set = new CustomizationSet(race, gender)
{
HairStyles = GetHairStyles(race, gender),
HairColors = hair,
SkinColors = skin,
EyeColors = _eyeColorPicker,
HighlightColors = _highlightPicker,
TattooColors = _tattooColorPicker,
LipColorsDark = race.ToRace() == Race.Hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark,
LipColorsLight = race.ToRace() == Race.Hrothgar ? Array.Empty<Customization>() : _lipColorPickerLight,
FacePaintColorsDark = _facePaintColorPickerDark,
FacePaintColorsLight = _facePaintColorPickerLight,
Faces = GetFaces(row),
NumEyebrows = GetListSize(row, CustomizationId.Eyebrows),
NumEyeShapes = GetListSize(row, CustomizationId.EyeShape),
NumNoseShapes = GetListSize(row, CustomizationId.Nose),
NumJawShapes = GetListSize(row, CustomizationId.Jaw),
NumMouthShapes = GetListSize(row, CustomizationId.Mouth),
FacePaints = GetFacePaints(race, gender),
TailEarShapes = GetTailEarShapes(row),
};
SetAvailability(set, row);
SetFacialFeatures(set, row);
SetMenuTypes(set, row);
SetNames(set, row);
return set;
}
public TemporaryData(DataManager gameData, CustomizationOptions options, ClientLanguage language)
{
_options = options;
_cmpFile = new CmpFile(gameData);
_customizeSheet = gameData.GetExcelSheet<CharaMakeCustomize>()!;
Lobby = gameData.GetExcelSheet<Lobby>()!;
var tmp = gameData.Excel.GetType().GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)?
.MakeGenericMethod(typeof(CharaMakeParams)).Invoke(gameData.Excel, new object?[]
{
"charamaketype",
language.ToLumina(),
null,
}) as ExcelSheet<CharaMakeParams>;
_listSheet = tmp!;
_hairSheet = gameData.GetExcelSheet<HairMakeType>()!;
_highlightPicker = CreateColorPicker(CustomizationId.HighlightColor, 256, 192);
_lipColorPickerDark = CreateColorPicker(CustomizationId.LipColor, 512, 96);
_lipColorPickerLight = CreateColorPicker(CustomizationId.LipColor, 1024, 96, true);
_eyeColorPicker = CreateColorPicker(CustomizationId.EyeColorL, 0, 192);
_facePaintColorPickerDark = CreateColorPicker(CustomizationId.FacePaintColor, 640, 96);
_facePaintColorPickerLight = CreateColorPicker(CustomizationId.FacePaintColor, 1152, 96, true);
_tattooColorPicker = CreateColorPicker(CustomizationId.TattooColor, 0, 192);
}
// Required sheets.
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet;
private readonly ExcelSheet<CharaMakeParams> _listSheet;
private readonly ExcelSheet<HairMakeType> _hairSheet;
public readonly ExcelSheet<Lobby> Lobby;
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 CustomizationOptions _options;
private Customization[] 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)))
.ToArray();
private static void SetMenuTypes(CustomizationSet set, CharaMakeParams row)
{
// Set up the menu types for all customizations.
set.Types = ((CustomizationId[])Enum.GetValues(typeof(CustomizationId))).Select(c =>
{
// Those types are not correctly given in the menu, so special case them to color pickers.
switch (c)
{
case CustomizationId.HighlightColor:
case CustomizationId.EyeColorL:
case CustomizationId.EyeColorR:
return CharaMakeParams.MenuType.ColorPicker;
}
// Otherwise find the first menu corresponding to the id.
// If there is none, assume a list.
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customization == c);
return menu?.Type ?? CharaMakeParams.MenuType.ListSelector;
}).ToArray();
set.Order = CustomizationSet.ComputeOrder(set);
}
// Set customizations available if they have any options.
private static void SetAvailability(CustomizationSet set, CharaMakeParams row)
{
void Set(bool available, CustomizationId flag)
{
if (available)
set.SetAvailable(flag);
}
// Both are percentages that are either unavailable or 0-100.
Set(GetListSize(row, CustomizationId.BustSize) > 0, CustomizationId.BustSize);
Set(GetListSize(row, CustomizationId.MuscleToneOrTailEarLength) > 0, CustomizationId.MuscleToneOrTailEarLength);
Set(set.NumEyebrows > 0, CustomizationId.Eyebrows);
Set(set.NumEyeShapes > 0, CustomizationId.EyeShape);
Set(set.NumNoseShapes > 0, CustomizationId.Nose);
Set(set.NumJawShapes > 0, CustomizationId.Jaw);
Set(set.NumMouthShapes > 0, CustomizationId.Mouth);
Set(set.TailEarShapes.Count > 0, CustomizationId.TailEarShape);
Set(set.Faces.Count > 0, CustomizationId.Face);
Set(set.FacePaints.Count > 0, CustomizationId.FacePaint);
Set(set.FacePaints.Count > 0, CustomizationId.FacePaintColor);
}
// Create a list of lists of facial features and the legacy tattoo.
private static void SetFacialFeatures(CustomizationSet set, CharaMakeParams row)
{
var count = set.Faces.Count;
var featureDict = new List<IReadOnlyList<Customization>>(count);
for (var i = 0; i < count; ++i)
{
var legacyTattoo = new Customization(CustomizationId.FacialFeaturesTattoos, 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)))
.Append(legacyTattoo)
.ToArray());
}
set.FeaturesTattoos = featureDict.ToArray();
}
// Set the names for the given set of parameters.
private void SetNames(CustomizationSet set, CharaMakeParams row)
{
var nameArray = ((CustomizationId[])Enum.GetValues(typeof(CustomizationId))).Select(c =>
{
// Find the first menu that corresponds to the Id.
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customization == c);
if (menu == null)
{
// If none exists and the id corresponds to highlights, set the Highlights name.
if (c == CustomizationId.HighlightsOnFlag)
return Lobby.GetRow(237)?.Text.ToDalamudString().ToString() ?? "Highlights";
// Otherwise there is an error and we use the default name.
return c.ToDefaultName();
}
// Facial Features and Tattoos is created by combining two strings.
if (c == CustomizationId.FacialFeaturesTattoos)
return
$"{Lobby.GetRow(1741)?.Text.ToDalamudString().ToString() ?? "Facial Features"} & {Lobby.GetRow(1742)?.Text.ToDalamudString().ToString() ?? "Tattoos"}";
// Otherwise all is normal, get the menu name or if it does not work the default name.
var textRow = Lobby.GetRow(menu.Value.Id);
return textRow?.Text.ToDalamudString().ToString() ?? c.ToDefaultName();
}).ToArray();
// Add names for both eye colors.
nameArray[(int)CustomizationId.EyeColorL] = nameArray[(int)CustomizationId.EyeColorR];
nameArray[(int)CustomizationId.EyeColorR] = _options.GetName(CustomName.OddEyes);
set.OptionName = nameArray;
}
// Obtain available skin and hair colors for the given subrace and gender.
private (Customization[], Customization[]) GetColors(SubRace race, Gender gender)
{
if (race is > SubRace.Veena or SubRace.Unknown)
throw new ArgumentOutOfRangeException(nameof(race), race, null);
var gv = gender == Gender.Male ? 0 : 1;
var idx = ((int)race * 2 + gv) * 5 + 3;
return (CreateColorPicker(CustomizationId.SkinColor, idx << 8, 192),
CreateColorPicker(CustomizationId.HairColor, (idx + 1) << 8, 192));
}
// Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender.
private Customization[] 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);
// Hairstyles can be found starting at Unknown66.
for (var i = 0; i < row.Unknown30; ++i)
{
var name = $"Unknown{66 + i * 9}";
var customizeIdx = (uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue)
continue;
// Hair Row from CustomizeSheet might not be set in case of unlockable hair.
var hairRow = _customizeSheet.GetRow(customizeIdx);
hairList.Add(hairRow != null
? new Customization(CustomizationId.Hairstyle, hairRow.FeatureID, hairRow.Icon, (ushort)hairRow.RowId)
: new Customization(CustomizationId.Hairstyle, (byte)i, customizeIdx));
}
return hairList.ToArray();
}
// Get Features.
private Customization 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);
}
// Get List sizes.
private static int GetListSize(CharaMakeParams row, CustomizationId id)
{
var menu = row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == id);
return menu?.Size ?? 0;
}
// Get face paints from the hair sheet via reflection.
private Customization[] GetFacePaints(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var paintList = new List<Customization>(row.Unknown37);
// Number of available face paints is at Unknown37.
for (var i = 0; i < row.Unknown37; ++i)
{
// Face paints start at Unknown73.
var name = $"Unknown{73 + i * 9}";
var customizeIdx =
(uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue)
continue;
var paintRow = _customizeSheet.GetRow(customizeIdx);
// Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints.
paintList.Add(paintRow != null
? new Customization(CustomizationId.FacePaint, paintRow.FeatureID, paintRow.Icon, (ushort)paintRow.RowId)
: new Customization(CustomizationId.FacePaint, (byte)i, customizeIdx));
}
return paintList.ToArray();
}
// Specific icons for tails or ears.
private Customization[] 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>();
// Specific icons for faces.
private Customization[] 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>();
// Specific icons for Hrothgar patterns.
private Customization[] 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>();
}
}

View file

@ -1,31 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization;
// Each Subrace and Gender combo has a customization set.
// This describes the available customizations, their types and their names.
public class CustomizationSet
{
public const int DefaultAvailable =
(1 << (int)CustomizationId.Height)
| (1 << (int)CustomizationId.Hairstyle)
| (1 << (int)CustomizationId.SkinColor)
| (1 << (int)CustomizationId.EyeColorR)
| (1 << (int)CustomizationId.EyeColorL)
| (1 << (int)CustomizationId.HairColor)
| (1 << (int)CustomizationId.HighlightColor)
| (1 << (int)CustomizationId.FacialFeaturesTattoos)
| (1 << (int)CustomizationId.TattooColor)
| (1 << (int)CustomizationId.LipColor)
| (1 << (int)CustomizationId.Height);
internal CustomizationSet(SubRace clan, Gender gender)
{
Gender = gender;
Clan = clan;
_settingAvailable = clan.ToRace() == Race.Hrothgar && gender == Gender.Female
? 0
? 0u
: DefaultAvailable;
}
@ -35,39 +25,50 @@ public class CustomizationSet
public Race Race
=> Clan.ToRace();
private int _settingAvailable;
private uint _settingAvailable;
internal void SetAvailable(CustomizationId id)
=> _settingAvailable |= 1 << (int)id;
=> _settingAvailable |= 1u << (int)id;
public bool IsAvailable(CustomizationId id)
=> (_settingAvailable & (1 << (int)id)) != 0;
=> (_settingAvailable & (1u << (int)id)) != 0;
public int NumEyebrows { get; internal set; }
public int NumEyeShapes { get; internal set; }
public int NumNoseShapes { get; internal set; }
public int NumJawShapes { get; internal set; }
public int NumMouthShapes { get; internal set; }
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 string ToHumanReadable(CustomizationData customizationData)
{
var sb = new StringBuilder();
foreach (var id in Enum.GetValues<CustomizationId>().Where(IsAvailable))
sb.AppendFormat("{0,-20}", Option(id)).Append(customizationData[id]);
return sb.ToString();
}
public IReadOnlyList<string> OptionName { get; internal set; } = null!;
public IReadOnlyList<Customization> Faces { get; internal set; } = null!;
public IReadOnlyList<Customization> HairStyles { get; internal set; } = null!;
public IReadOnlyList<Customization> TailEarShapes { get; internal set; } = null!;
public IReadOnlyList<IReadOnlyList<Customization>> FeaturesTattoos { get; internal set; } = null!;
public IReadOnlyList<Customization> FacePaints { 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<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 set; } = null!;
public IReadOnlyList<Customization> HairColors { get; internal set; } = null!;
public IReadOnlyList<Customization> HighlightColors { get; internal set; } = null!;
public IReadOnlyList<Customization> EyeColors { get; internal set; } = null!;
public IReadOnlyList<Customization> TattooColors { get; internal set; } = null!;
public IReadOnlyList<Customization> FacePaintColorsLight { get; internal set; } = null!;
public IReadOnlyList<Customization> FacePaintColorsDark { get; internal set; } = null!;
public IReadOnlyList<Customization> LipColorsLight { get; internal set; } = null!;
public IReadOnlyList<Customization> LipColorsDark { get; internal set; } = 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 IReadOnlyList<CharaMakeParams.MenuType> Types { get; internal set; } = null!;
public IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizationId[]> Order { get; internal set; } = null!;
public IReadOnlyList<CharaMakeParams.MenuType> Types { get; internal set; } = null!;
public string Option(CustomizationId id)
=> OptionName[(int)id];
@ -154,6 +155,15 @@ public class CustomizationSet
public CharaMakeParams.MenuType Type(CustomizationId id)
=> Types[(int)id];
internal static IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizationId[]> ComputeOrder(CustomizationSet set)
{
var ret = (CustomizationId[])Enum.GetValues(typeof(CustomizationId));
ret[(int)CustomizationId.TattooColor] = CustomizationId.EyeColorL;
ret[(int)CustomizationId.EyeColorL] = CustomizationId.EyeColorR;
ret[(int)CustomizationId.EyeColorR] = CustomizationId.TattooColor;
return ret.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray());
}
public int Count(CustomizationId id)
{
@ -187,4 +197,17 @@ public class CustomizationSet
_ => throw new ArgumentOutOfRangeException(nameof(id), id, null),
};
}
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);
}

View file

@ -1,17 +1,16 @@
using System.Collections.Generic;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization
namespace Glamourer.Customization;
public interface ICustomizationManager
{
public interface ICustomizationManager
{
public IReadOnlyList<Race> Races { get; }
public IReadOnlyList<SubRace> Clans { get; }
public IReadOnlyList<Gender> Genders { get; }
public IReadOnlyList<Race> Races { get; }
public IReadOnlyList<SubRace> Clans { get; }
public IReadOnlyList<Gender> Genders { get; }
public CustomizationSet GetList(SubRace race, Gender gender);
public CustomizationSet GetList(SubRace race, Gender gender);
public ImGuiScene.TextureWrap GetIcon(uint iconId);
public string GetName(CustomName name);
}
public ImGuiScene.TextureWrap GetIcon(uint iconId);
public string GetName(CustomName name);
}

View file

@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Glamourer.Structs;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
@ -17,20 +20,14 @@ public static class GameData
private static Dictionary<EquipSlot, List<Item>>? _itemsBySlot;
private static Dictionary<byte, Job>? _jobs;
private static Dictionary<ushort, JobGroup>? _jobGroups;
private static SortedList<uint, ModelChara>? _models;
private static ModelData? _models;
private static RestrictedGear? _restrictedGear;
public static IReadOnlyDictionary<uint, ModelChara> Models(DataManager dataManager)
{
if (_models != null)
return _models;
public static RestrictedGear RestrictedGear(DataManager dataManager)
=> _restrictedGear ??= new RestrictedGear(dataManager);
var sheet = dataManager.GetExcelSheet<ModelChara>()!;
_models = new SortedList<uint, ModelChara>((int)sheet.RowCount);
foreach (var model in sheet.Where(m => m.Type != 0))
_models.Add(model.RowId, model);
return _models;
}
public static ModelData Models(DataManager dataManager)
=> _models ??= new ModelData(dataManager);
public static IReadOnlyDictionary<byte, Stain> Stains(DataManager dataManager)
{

View file

@ -39,6 +39,10 @@
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
<Private>False</Private>

View file

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Lumina.Excel.GeneratedSheets;
using Companion = Lumina.Excel.GeneratedSheets.Companion;
namespace Glamourer;
public class ModelData
{
public struct Data
{
public readonly ModelChara Model;
public string FirstName { get; }
public string AllNames { get; internal set; }
public Data(ModelChara model, string name)
{
Model = model;
FirstName = $"{name} #{model.RowId:D4}";
AllNames = $"#{model.RowId:D4}\n{name}";
}
}
private readonly SortedList<uint, Data> _models;
public IReadOnlyDictionary<uint, Data> Models
=> _models;
public ModelData(DataManager dataManager)
{
var modelSheet = dataManager.GetExcelSheet<ModelChara>();
_models = new SortedList<uint, Data>(NpcNames.ModelCharas.Count);
void UpdateData(uint model, string name)
{
name = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(name);
if (_models.TryGetValue(model, out var data))
data.AllNames = $"{data.AllNames}\n{name}";
else
data = new Data(modelSheet!.GetRow(model)!, name);
_models[model] = data;
}
var companionSheet = dataManager.GetExcelSheet<Companion>()!;
foreach (var companion in companionSheet.Where(c => c.Model.Row != 0 && c.Singular.RawData.Length > 0))
UpdateData(companion.Model.Row, companion.Singular.ToDalamudString().TextValue);
var mountSheet = dataManager.GetExcelSheet<Mount>()!;
foreach (var mount in mountSheet.Where(c => c.ModelChara.Row != 0 && c.Singular.RawData.Length > 0))
UpdateData(mount.ModelChara.Row, mount.Singular.ToDalamudString().TextValue);
var bNpcNames = dataManager.GetExcelSheet<BNpcName>()!;
foreach (var (model, list) in NpcNames.ModelCharas)
{
foreach (var nameId in list)
{
var name = nameId >= 0
? bNpcNames.GetRow((uint)nameId)?.Singular.ToDalamudString().TextValue ?? string.Empty
: NpcNames.Names[~nameId];
if (name.Length == 0)
continue;
UpdateData(model, name);
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,441 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Data;
using Dalamud.Logging;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer;
// Handle gender- or race-locked gear in the draw model itself.
// Racial gear gets swapped to the correct current race and gender (it is one set each).
// Gender-locked gear gets swapped to the equivalent set if it exists (most of them do),
// with some items getting send to emperor's new clothes and a few funny entries.
public class RestrictedGear
{
private readonly ExcelSheet<Item> _items;
private readonly ExcelSheet<EquipRaceCategory> _categories;
private readonly HashSet<uint> _raceGenderSet = RaceGenderGroup.Where(c => c != 0).ToHashSet();
private readonly Dictionary<uint, uint> _maleToFemale = new();
private readonly Dictionary<uint, uint> _femaleToMale = new();
internal RestrictedGear(DataManager gameData)
{
_items = gameData.GetExcelSheet<Item>()!;
_categories = gameData.GetExcelSheet<EquipRaceCategory>()!;
AddKnown();
UnhandledRestrictedGear(false); // Set this to true to create a print of unassigned gear on launch.
}
// Resolve a model given by its model id, variant and slot for your current race and gender.
public (bool Replaced, SetId ModelId, byte Variant) ResolveRestricted(SetId modelId, byte variant, EquipSlot slot, Race race,
Gender gender)
{
var quad = modelId.Value | ((uint)variant << 16);
// Check racial gear, this does not need slots.
if (RaceGenderGroup.Contains(quad))
{
var idx = ((int)race - 1) * 2 + (gender is Gender.Female or Gender.FemaleNpc ? 1 : 0);
var value = RaceGenderGroup[idx];
return (value != quad, (ushort)value, (byte)(value >> 16));
}
// Check gender slots. If current gender is female, check if anything needs to be changed from male to female,
// and vice versa.
// Some items lead to the exact same model- and variant id just gender specified,
// so check for actual difference in the Replaced bool.
var needle = quad | ((uint)slot.ToSlot() << 24);
if (gender is Gender.Female or Gender.FemaleNpc && _maleToFemale.TryGetValue(needle, out var newValue)
|| gender is Gender.Male or Gender.MaleNpc && _femaleToMale.TryGetValue(needle, out newValue))
return (quad != newValue, (ushort)newValue, (byte)(newValue >> 16));
// The gear is not restricted.
return (false, modelId, variant);
}
// Add all unknown restricted gear and pair it with emperor's new gear on start up.
// Can also print unhandled items.
public void UnhandledRestrictedGear(bool print = false)
{
if (print)
PluginLog.Information("#### MALE ONLY ######");
void AddEmperor(Item item, bool male, bool female)
{
var slot = ((EquipSlot)item.EquipSlotCategory.Row).ToSlot();
var emperor = slot switch
{
EquipSlot.Head => 10032u,
EquipSlot.Body => 10033u,
EquipSlot.Hands => 10034u,
EquipSlot.Legs => 10035u,
EquipSlot.Feet => 10036u,
EquipSlot.Ears => 09293u,
EquipSlot.Neck => 09292u,
EquipSlot.Wrists => 09294u,
EquipSlot.RFinger => 09295u,
EquipSlot.LFinger => 09295u,
_ => 0u,
};
if (emperor == 0)
return;
if (male)
AddItem(item.RowId, emperor, true, false);
if (female)
AddItem(emperor, item.RowId, false, true);
}
var unhandled = 0;
foreach (var item in _items.Where(i => i.EquipRestriction == 2))
{
if (_maleToFemale.ContainsKey((uint)item.ModelMain | ((uint)((EquipSlot)item.EquipSlotCategory.Row).ToSlot() << 24)))
continue;
++unhandled;
AddEmperor(item, true, false);
if (print)
PluginLog.Information($"{item.RowId:D5} {item.Name.ToDalamudString().TextValue}");
}
if (print)
PluginLog.Information("#### FEMALE ONLY ####");
foreach (var item in _items.Where(i => i.EquipRestriction == 3))
{
if (_femaleToMale.ContainsKey((uint)item.ModelMain | ((uint)((EquipSlot)item.EquipSlotCategory.Row).ToSlot() << 24)))
continue;
++unhandled;
AddEmperor(item, false, true);
if (print)
PluginLog.Information($"{item.RowId:D5} {item.Name.ToDalamudString().TextValue}");
}
if (print)
PluginLog.Information("#### OTHER #########");
foreach (var item in _items.Where(i => i.EquipRestriction > 3))
{
if (_raceGenderSet.Contains((uint)item.ModelMain))
continue;
++unhandled;
if (print)
PluginLog.Information(
$"{item.RowId:D5} {item.Name.ToDalamudString().TextValue} RestrictionGroup {_categories.GetRow(item.EquipRestriction)!.RowId:D2}");
}
if (unhandled > 0)
PluginLog.Warning("There were {Num} restricted items not handled and directed to Emperor's New Set.", unhandled);
}
// Add a item redirection by its item - NOT MODEL - id.
// This uses the items model as well as its slot.
// Creates a <-> redirection by default but can add -> or <- redirections by setting the corresponding bools to false.
// Prints warnings if anything does not make sense.
private void AddItem(uint itemIdMale, uint itemIdFemale, bool addMale = true, bool addFemale = true)
{
if (!addMale && !addFemale)
return;
var mItem = _items.GetRow(itemIdMale);
var fItem = _items.GetRow(itemIdFemale);
if (mItem == null || fItem == null)
{
PluginLog.Warning($"Could not add item {itemIdMale} or {itemIdFemale} to restricted items.");
return;
}
if (mItem.EquipRestriction != 2 && addMale)
{
PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} is not restricted anymore.");
return;
}
if (fItem.EquipRestriction != 3 && addFemale)
{
PluginLog.Warning($"{fItem.Name.ToDalamudString().TextValue} is not restricted anymore.");
return;
}
var mSlot = ((EquipSlot)mItem.EquipSlotCategory.Row).ToSlot();
var fSlot = ((EquipSlot)fItem.EquipSlotCategory.Row).ToSlot();
if (!mSlot.IsAccessory() && !mSlot.IsEquipment())
{
PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} is not equippable to a known slot.");
return;
}
if (mSlot != fSlot)
{
PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} and {fItem.Name.ToDalamudString().TextValue} are not compatible.");
return;
}
var mModelIdSlot = (uint)mItem.ModelMain | ((uint)mSlot << 24);
var fModelIdSlot = (uint)fItem.ModelMain | ((uint)fSlot << 24);
if (addMale)
_maleToFemale.TryAdd(mModelIdSlot, fModelIdSlot);
if (addFemale)
_femaleToMale.TryAdd(fModelIdSlot, mModelIdSlot);
}
// Add all currently existing and known gender restricted items.
private void AddKnown()
{
AddItem(02967, 02970); // Lord's Yukata (Blue) <-> Lady's Yukata (Red)
AddItem(02968, 02971); // Lord's Yukata (Green) <-> Lady's Yukata (Blue)
AddItem(02969, 02972); // Lord's Yukata (Grey) <-> Lady's Yukata (Black)
AddItem(02973, 02978); // Red Summer Top <-> Red Summer Halter
AddItem(02974, 02979); // Green Summer Top <-> Green Summer Halter
AddItem(02975, 02980); // Blue Summer Top <-> Blue Summer Halter
AddItem(02976, 02981); // Solar Summer Top <-> Solar Summer Halter
AddItem(02977, 02982); // Lunar Summer Top <-> Lunar Summer Halter
AddItem(02996, 02997); // Hempen Undershirt <-> Hempen Camise
AddItem(03280, 03283); // Lord's Drawers (Black) <-> Lady's Knickers (Black)
AddItem(03281, 03284); // Lord's Drawers (White) <-> Lady's Knickers (White)
AddItem(03282, 03285); // Lord's Drawers (Gold) <-> Lady's Knickers (Gold)
AddItem(03286, 03291); // Red Summer Trunks <-> Red Summer Tanga
AddItem(03287, 03292); // Green Summer Trunks <-> Green Summer Tanga
AddItem(03288, 03293); // Blue Summer Trunks <-> Blue Summer Tanga
AddItem(03289, 03294); // Solar Summer Trunks <-> Solar Summer Tanga
AddItem(03290, 03295); // Lunar Summer Trunks <-> Lunar Summer Tanga
AddItem(03307, 03308); // Hempen Underpants <-> Hempen Pantalettes
AddItem(03748, 03749); // Lord's Clogs <-> Lady's Clogs
AddItem(06045, 06041); // Bohemian's Coat <-> Guardian Corps Coat
AddItem(06046, 06042); // Bohemian's Gloves <-> Guardian Corps Gauntlets
AddItem(06047, 06043); // Bohemian's Trousers <-> Guardian Corps Skirt
AddItem(06048, 06044); // Bohemian's Boots <-> Guardian Corps Boots
AddItem(06094, 06098); // Summer Evening Top <-> Summer Morning Halter
AddItem(06095, 06099); // Summer Evening Trunks <-> Summer Morning Tanga
AddItem(06096, 06100); // Striped Summer Top <-> Striped Summer Halter
AddItem(06097, 06101); // Striped Summer Trunks <-> Striped Summer Tanga
AddItem(06102, 06104); // Black Summer Top <-> Black Summer Halter
AddItem(06103, 06105); // Black Summer Trunks <-> Black Summer Tanga
AddItem(06972, 06973); // Valentione Apron <-> Valentione Apron Dress
AddItem(06975, 06976); // Valentione Trousers <-> Valentione Skirt
AddItem(08532, 08535); // Lord's Yukata (Blackflame) <-> Lady's Yukata (Redfly)
AddItem(08533, 08536); // Lord's Yukata (Whiteflame) <-> Lady's Yukata (Bluefly)
AddItem(08534, 08537); // Lord's Yukata (Blueflame) <-> Lady's Yukata (Pinkfly)
AddItem(08542, 08549); // Ti Leaf Lei <-> Coronal Summer Halter
AddItem(08543, 08550); // Red Summer Maro <-> Red Summer Pareo
AddItem(08544, 08551); // South Seas Talisman <-> Sea Breeze Summer Halter
AddItem(08545, 08552); // Blue Summer Maro <-> Sea Breeze Summer Pareo
AddItem(08546, 08553); // Coeurl Talisman <-> Coeurl Beach Halter
AddItem(08547, 08554); // Coeurl Beach Maro <-> Coeurl Beach Pareo
AddItem(08548, 08555); // Coeurl Beach Briefs <-> Coeurl Beach Tanga
AddItem(10316, 10317); // Southern Seas Vest <-> Southern Seas Swimsuit
AddItem(10318, 10319); // Southern Seas Trunks <-> Southern Seas Tanga
AddItem(10320, 10321); // Striped Southern Seas Vest <-> Striped Southern Seas Swimsuit
AddItem(13298, 13567); // Black-feathered Flat Hat <-> Red-feathered Flat Hat
AddItem(13300, 13639); // Lord's Suikan <-> Lady's Suikan
AddItem(13724, 13725); // Little Lord's Clogs <-> Little Lady's Clogs
AddItem(14854, 14857); // Eastern Lord's Togi <-> Eastern Lady's Togi
AddItem(14855, 14858); // Eastern Lord's Trousers <-> Eastern Lady's Loincloth
AddItem(14856, 14859); // Eastern Lord's Crakows <-> Eastern Lady's Crakows
AddItem(15639, 15642); // Far Eastern Patriarch's Hat <-> Far Eastern Matriarch's Sun Hat
AddItem(15640, 15643); // Far Eastern Patriarch's Tunic <-> Far Eastern Matriarch's Dress
AddItem(15641, 15644); // Far Eastern Patriarch's Longboots <-> Far Eastern Matriarch's Boots
AddItem(15922, 15925); // Moonfire Vest <-> Moonfire Halter
AddItem(15923, 15926); // Moonfire Trunks <-> Moonfire Tanga
AddItem(15924, 15927); // Moonfire Caligae <-> Moonfire Sandals
AddItem(16106, 16111); // Makai Mauler's Facemask <-> Makai Manhandler's Facemask
AddItem(16107, 16112); // Makai Mauler's Oilskin <-> Makai Manhandler's Jerkin
AddItem(16108, 16113); // Makai Mauler's Fingerless Gloves <-> Makai Manhandler's Fingerless Gloves
AddItem(16109, 16114); // Makai Mauler's Leggings <-> Makai Manhandler's Quartertights
AddItem(16110, 16115); // Makai Mauler's Boots <-> Makai Manhandler's Longboots
AddItem(16116, 16121); // Makai Marksman's Eyepatch <-> Makai Markswoman's Ribbon
AddItem(16117, 16122); // Makai Marksman's Battlegarb <-> Makai Markswoman's Battledress
AddItem(16118, 16123); // Makai Marksman's Fingerless Gloves <-> Makai Markswoman's Fingerless Gloves
AddItem(16119, 16124); // Makai Marksman's Slops <-> Makai Markswoman's Quartertights
AddItem(16120, 16125); // Makai Marksman's Boots <-> Makai Markswoman's Longboots
AddItem(16126, 16131); // Makai Sun Guide's Circlet <-> Makai Moon Guide's Circlet
AddItem(16127, 16132); // Makai Sun Guide's Oilskin <-> Makai Moon Guide's Gown
AddItem(16128, 16133); // Makai Sun Guide's Fingerless Gloves <-> Makai Moon Guide's Fingerless Gloves
AddItem(16129, 16134); // Makai Sun Guide's Slops <-> Makai Moon Guide's Quartertights
AddItem(16130, 16135); // Makai Sun Guide's Boots <-> Makai Moon Guide's Longboots
AddItem(16136, 16141); // Makai Priest's Coronet <-> Makai Priestess's Headdress
AddItem(16137, 16142); // Makai Priest's Doublet Robe <-> Makai Priestess's Jerkin
AddItem(16138, 16143); // Makai Priest's Fingerless Gloves <-> Makai Priestess's Fingerless Gloves
AddItem(16139, 16144); // Makai Priest's Slops <-> Makai Priestess's Skirt
AddItem(16140, 16145); // Makai Priest's Boots <-> Makai Priestess's Longboots
AddItem(16588, 16592); // Far Eastern Gentleman's Hat <-> Far Eastern Beauty's Hairpin
AddItem(16589, 16593); // Far Eastern Gentleman's Robe <-> Far Eastern Beauty's Robe
AddItem(16590, 16594); // Far Eastern Gentleman's Haidate <-> Far Eastern Beauty's Koshita
AddItem(16591, 16595); // Far Eastern Gentleman's Boots <-> Far Eastern Beauty's Boots
AddItem(17204, 17209); // Common Makai Mauler's Facemask <-> Common Makai Manhandler's Facemask
AddItem(17205, 17210); // Common Makai Mauler's Oilskin <-> Common Makai Manhandler's Jerkin
AddItem(17206, 17211); // Common Makai Mauler's Fingerless Gloves <-> Common Makai Manhandler's Fingerless Glove
AddItem(17207, 17212); // Common Makai Mauler's Leggings <-> Common Makai Manhandler's Quartertights
AddItem(17208, 17213); // Common Makai Mauler's Boots <-> Common Makai Manhandler's Longboots
AddItem(17214, 17219); // Common Makai Marksman's Eyepatch <-> Common Makai Markswoman's Ribbon
AddItem(17215, 17220); // Common Makai Marksman's Battlegarb <-> Common Makai Markswoman's Battledress
AddItem(17216, 17221); // Common Makai Marksman's Fingerless Gloves <-> Common Makai Markswoman's Fingerless Glove
AddItem(17217, 17222); // Common Makai Marksman's Slops <-> Common Makai Markswoman's Quartertights
AddItem(17218, 17223); // Common Makai Marksman's Boots <-> Common Makai Markswoman's Longboots
AddItem(17224, 17229); // Common Makai Sun Guide's Circlet <-> Common Makai Moon Guide's Circlet
AddItem(17225, 17230); // Common Makai Sun Guide's Oilskin <-> Common Makai Moon Guide's Gown
AddItem(17226, 17231); // Common Makai Sun Guide's Fingerless Gloves <-> Common Makai Moon Guide's Fingerless Glove
AddItem(17227, 17232); // Common Makai Sun Guide's Slops <-> Common Makai Moon Guide's Quartertights
AddItem(17228, 17233); // Common Makai Sun Guide's Boots <-> Common Makai Moon Guide's Longboots
AddItem(17234, 17239); // Common Makai Priest's Coronet <-> Common Makai Priestess's Headdress
AddItem(17235, 17240); // Common Makai Priest's Doublet Robe <-> Common Makai Priestess's Jerkin
AddItem(17236, 17241); // Common Makai Priest's Fingerless Gloves <-> Common Makai Priestess's Fingerless Gloves
AddItem(17237, 17242); // Common Makai Priest's Slops <-> Common Makai Priestess's Skirt
AddItem(17238, 17243); // Common Makai Priest's Boots <-> Common Makai Priestess's Longboots
AddItem(17481, 17476); // Royal Seneschal's Chapeau <-> Songbird Hat
AddItem(17482, 17477); // Royal Seneschal's Coat <-> Songbird Jacket
AddItem(17483, 17478); // Royal Seneschal's Fingerless Gloves <-> Songbird Gloves
AddItem(17484, 17479); // Royal Seneschal's Breeches <-> Songbird Skirt
AddItem(17485, 17480); // Royal Seneschal's Boots <-> Songbird Boots
AddItem(20479, 20484); // Star of the Nezha Lord <-> Star of the Nezha Lady
AddItem(20480, 20485); // Nezha Lord's Togi <-> Nezha Lady's Togi
AddItem(20481, 20486); // Nezha Lord's Gloves <-> Nezha Lady's Gloves
AddItem(20482, 20487); // Nezha Lord's Slops <-> Nezha Lady's Slops
AddItem(20483, 20488); // Nezha Lord's Boots <-> Nezha Lady's Kneeboots
AddItem(22367, 22372); // Faerie Tale Prince's Circlet <-> Faerie Tale Princess's Tiara
AddItem(22368, 22373); // Faerie Tale Prince's Vest <-> Faerie Tale Princess's Dress
AddItem(22369, 22374); // Faerie Tale Prince's Gloves <-> Faerie Tale Princess's Gloves
AddItem(22370, 22375); // Faerie Tale Prince's Slops <-> Faerie Tale Princess's Long Skirt
AddItem(22371, 22376); // Faerie Tale Prince's Boots <-> Faerie Tale Princess's Heels
AddItem(24599, 24602); // Far Eastern Schoolboy's Hat <-> Far Eastern Schoolgirl's Hair Ribbon
AddItem(24600, 24603); // Far Eastern Schoolboy's Hakama <-> Far Eastern Schoolgirl's Hakama
AddItem(24601, 24604); // Far Eastern Schoolboy's Zori <-> Far Eastern Schoolgirl's Boots
AddItem(28558, 28573); // Valentione Rose Hat <-> Valentione Rose Ribboned Hat
AddItem(28559, 28574); // Valentione Rose Waistcoat <-> Valentione Rose Dress
AddItem(28560, 28575); // Valentione Rose Gloves <-> Valentione Rose Ribboned Gloves
AddItem(28561, 28576); // Valentione Rose Slacks <-> Valentione Rose Tights
AddItem(28562, 28577); // Valentione Rose Shoes <-> Valentione Rose Heels
AddItem(28563, 28578); // Valentione Forget-me-not Hat <-> Valentione Forget-me-not Ribboned Hat
AddItem(28564, 28579); // Valentione Forget-me-not Waistcoat <-> Valentione Forget-me-not Dress
AddItem(28565, 28580); // Valentione Forget-me-not Gloves <-> Valentione Forget-me-not Ribboned Gloves
AddItem(28566, 28581); // Valentione Forget-me-not Slacks <-> Valentione Forget-me-not Tights
AddItem(28567, 28582); // Valentione Forget-me-not Shoes <-> Valentione Forget-me-not Heels
AddItem(28568, 28583); // Valentione Acacia Hat <-> Valentione Acacia Ribboned Hat
AddItem(28569, 28584); // Valentione Acacia Waistcoat <-> Valentione Acacia Dress
AddItem(28570, 28585); // Valentione Acacia Gloves <-> Valentione Acacia Ribboned Gloves
AddItem(28571, 28586); // Valentione Acacia Slacks <-> Valentione Acacia Tights
AddItem(28572, 28587); // Valentione Acacia Shoes <-> Valentione Acacia Heels
AddItem(28600, 28605); // Eastern Lord Errant's Hat <-> Eastern Lady Errant's Hat
AddItem(28601, 28606); // Eastern Lord Errant's Jacket <-> Eastern Lady Errant's Coat
AddItem(28602, 28607); // Eastern Lord Errant's Wristbands <-> Eastern Lady Errant's Gloves
AddItem(28603, 28608); // Eastern Lord Errant's Trousers <-> Eastern Lady Errant's Skirt
AddItem(28604, 28609); // Eastern Lord Errant's Shoes <-> Eastern Lady Errant's Boots
AddItem(31408, 31413); // Bergsteiger's Hat <-> Dirndl's Hat
AddItem(31409, 31414); // Bergsteiger's Jacket <-> Dirndl's Bodice
AddItem(31410, 31415); // Bergsteiger's Halfgloves <-> Dirndl's Wrist Torque
AddItem(31411, 31416); // Bergsteiger's Halfslops <-> Dirndl's Long Skirt
AddItem(31412, 31417); // Bergsteiger's Boots <-> Dirndl's Pumps
AddItem(36336, 36337); // Omega-M Attire <-> Omega-F Attire
AddItem(36338, 36339); // Omega-M Ear Cuffs <-> Omega-F Earrings
AddItem(37442, 37447); // Makai Vanguard's Monocle <-> Makai Vanbreaker's Ribbon
AddItem(37443, 37448); // Makai Vanguard's Battlegarb <-> Makai Vanbreaker's Battledress
AddItem(37444, 37449); // Makai Vanguard's Fingerless Gloves <-> Makai Vanbreaker's Fingerless Gloves
AddItem(37445, 37450); // Makai Vanguard's Leggings <-> Makai Vanbreaker's Quartertights
AddItem(37446, 37451); // Makai Vanguard's Boots <-> Makai Vanbreaker's Longboots
AddItem(37452, 37457); // Makai Harbinger's Facemask <-> Makai Harrower's Facemask
AddItem(37453, 37458); // Makai Harbinger's Battlegarb <-> Makai Harrower's Jerkin
AddItem(37454, 37459); // Makai Harbinger's Fingerless Gloves <-> Makai Harrower's Fingerless Gloves
AddItem(37455, 37460); // Makai Harbinger's Leggings <-> Makai Harrower's Quartertights
AddItem(37456, 37461); // Makai Harbinger's Boots <-> Makai Harrower's Longboots
AddItem(37462, 37467); // Common Makai Vanguard's Monocle <-> Common Makai Vanbreaker's Ribbon
AddItem(37463, 37468); // Common Makai Vanguard's Battlegarb <-> Common Makai Vanbreaker's Battledress
AddItem(37464, 37469); // Common Makai Vanguard's Fingerless Gloves <-> Common Makai Vanbreaker's Fingerless Gloves
AddItem(37465, 37470); // Common Makai Vanguard's Leggings <-> Common Makai Vanbreaker's Quartertights
AddItem(37466, 37471); // Common Makai Vanguard's Boots <-> Common Makai Vanbreaker's Longboots
AddItem(37472, 37477); // Common Makai Harbinger's Facemask <-> Common Makai Harrower's Facemask
AddItem(37473, 37478); // Common Makai Harbinger's Battlegarb <-> Common Makai Harrower's Jerkin
AddItem(37474, 37479); // Common Makai Harbinger's Fingerless Gloves <-> Common Makai Harrower's Fingerless Gloves
AddItem(37475, 37480); // Common Makai Harbinger's Leggings <-> Common Makai Harrower's Quartertights
AddItem(37476, 37481); // Common Makai Harbinger's Boots <-> Common Makai Harrower's Longboots
AddItem(23003, 23008); // Mun'gaek Hat <-> Eastern Socialite's Hat
AddItem(23004, 23009); // Mun'gaek Uibok <-> Eastern Socialite's Cheongsam
AddItem(23005, 23010); // Mun'gaek Cuffs <-> Eastern Socialite's Gloves
AddItem(23006, 23011); // Mun'gaek Trousers <-> Eastern Socialite's Skirt
AddItem(23007, 23012); // Mun'gaek Boots <-> Eastern Socialite's Boots
AddItem(24148, 24153); // Far Eastern Officer's Hat <-> Far Eastern Maiden's Hat
AddItem(24149, 24154); // Far Eastern Officer's Robe <-> Far Eastern Maiden's Tunic
AddItem(24150, 24155); // Far Eastern Officer's Armband <-> Far Eastern Maiden's Armband
AddItem(24151, 24156); // Far Eastern Officer's Bottoms <-> Far Eastern Maiden's Bottoms
AddItem(24152, 24157); // Far Eastern Officer's Boots <-> Far Eastern Maiden's Boots
AddItem(13323, 13322); // Scion Thief's Tunic <-> Scion Conjurer's Dalmatica
AddItem(13693, 10034, true, false); // Scion Thief's Halfgloves -> The Emperor's New Gloves
AddItem(13694, 13691); // Scion Thief's Gaskins <-> Scion Conjurer's Chausses
AddItem(13695, 13692); // Scion Thief's Armored Caligae <-> Scion Conjurer's Pattens
AddItem(13326, 30063); // Scion Thaumaturge's Robe <-> Scion Sorceress's Headdress
AddItem(13696, 30062); // Scion Thaumaturge's Monocle <-> Scion Sorceress's Robe
AddItem(13697, 30064); // Scion Thaumaturge's Gauntlets <-> Scion Sorceress's Shadowtalons
AddItem(13698, 10035, true, false); // Scion Thaumaturge's Gaskins -> The Emperor's New Breeches
AddItem(13699, 30065); // Scion Thaumaturge's Moccasins <-> Scion Sorceress's High Boots
AddItem(13327, 15942); // Scion Chronocler's Cowl <-> Scion Healer's Robe
AddItem(13700, 10034, true, false); // Scion Chronocler's Ringbands -> The Emperor's New Gloves
AddItem(13701, 15943); // Scion Chronocler's Tights <-> Scion Healer's Halftights
AddItem(13702, 15944); // Scion Chronocler's Caligae <-> Scion Healer's Highboots
AddItem(14861, 13324); // Head Engineer's Goggles <-> Scion Striker's Visor
AddItem(14862, 13325); // Head Engineer's Attire <-> Scion Striker's Attire
AddItem(15938, 33751); // Scion Rogue's Jacket <-> Oracle Top
AddItem(15939, 10034, true, false); // Scion Rogue's Armguards -> The Emperor's New Gloves
AddItem(15940, 33752); // Scion Rogue's Gaskins <-> Oracle Leggings
AddItem(15941, 33753); // Scion Rogue's Boots <-> Oracle Pantalettes
AddItem(16042, 16046); // Abes Jacket <-> High Summoner's Dress
AddItem(16043, 16047); // Abes Gloves <-> High Summoner's Armlets
AddItem(16044, 10035, true, false); // Abes Halfslops -> The Emperor's New Breeches
AddItem(16045, 16048); // Abes Boots <-> High Summoner's Boots
AddItem(17473, 28553); // Lord Commander's Coat <-> Majestic Dress
AddItem(17474, 28554); // Lord Commander's Gloves <-> Majestic Wristdresses
AddItem(10036, 28555, false); // Emperor's New Boots <- Majestic Boots
AddItem(21021, 21026); // Werewolf Feet <-> Werewolf Legs
AddItem(22452, 20633); // Cracked Manderville Monocle <-> Blackbosom Hat
AddItem(22453, 20634); // Torn Manderville Coatee <-> Blackbosom Dress
AddItem(22454, 20635); // Singed Manderville Gloves <-> Blackbosom Dress Gloves
AddItem(22455, 10035, true, false); // Stained Manderville Bottoms -> The Emperor's New Breeches
AddItem(22456, 20636); // Scuffed Manderville Gaiters <-> lackbosom Boots
AddItem(23013, 21302); // Doman Liege's Dogi <-> Scion Liberator's Jacket
AddItem(23014, 21303); // Doman Liege's Kote <-> Scion Liberator's Fingerless Gloves
AddItem(23015, 21304); // Doman Liege's Kyakui <-> Scion Liberator's Pantalettes
AddItem(23016, 21305); // Doman Liege's Kyahan <-> Scion Liberator's Sabatons
AddItem(09293, 21306, false); // The Emperor's New Earrings <- Scion Liberator's Earrings
AddItem(24158, 23008); // Leal Samurai's Kasa <-> Eastern Socialite's Hat
AddItem(24159, 23009); // Leal Samurai's Dogi <-> Eastern Socialite's Cheongsam
AddItem(24160, 23010); // Leal Samurai's Tekko <-> Eastern Socialite's Gloves
AddItem(24161, 23011); // Leal Samurai's Tsutsu-hakama <-> Eastern Socialite's Skirt
AddItem(24162, 23012); // Leal Samurai's Geta <-> Eastern Socialite's Boots
AddItem(02966, 13321, false); // Reindeer Suit <- Antecedent's Attire
AddItem(15479, 36843, false); // Swine Body <- Lyse's Leadership Attire
AddItem(21941, 24999, false); // Ala Mhigan Gown <- Gown of Light
AddItem(30757, 25000, false); // Southern Seas Skirt <- Skirt of Light
AddItem(36821, 27933, false); // Archfiend Helm <- Scion Hearer's Hood
AddItem(36822, 27934, false); // Archfiend Armor <- Scion Hearer's Coat
AddItem(36825, 27935, false); // Archfiend Sabatons <- Scion Hearer's Shoes
}
// The racial starter sets are available for all 4 slots each,
// but have no associated accessories or hats.
private static readonly uint[] RaceGenderGroup =
{
0x020054,
0x020055,
0x020056,
0x020057,
0x02005C,
0x02005D,
0x020058,
0x020059,
0x02005A,
0x02005B,
0x020101,
0x020102,
0x010255,
uint.MaxValue, // TODO: Female Hrothgar
0x0102E8,
0x010245,
};
}

View file

@ -3,23 +3,25 @@ using Penumbra.GameData.Enums;
namespace Glamourer.Structs;
// Turn EquipSlot into a bitfield flag enum.
[Flags]
public enum CharacterEquipMask : ushort
{
None = 0,
None = 0,
MainHand = 0b000000000001,
OffHand = 0b000000000010,
Head = 0b000000000100,
Body = 0b000000001000,
Hands = 0b000000010000,
Legs = 0b000000100000,
Feet = 0b000001000000,
Ears = 0b000010000000,
Neck = 0b000100000000,
Wrists = 0b001000000000,
RFinger = 0b010000000000,
LFinger = 0b100000000000,
All = 0b111111111111,
OffHand = 0b000000000010,
Head = 0b000000000100,
Body = 0b000000001000,
Hands = 0b000000010000,
Legs = 0b000000100000,
Feet = 0b000001000000,
Ears = 0b000010000000,
Neck = 0b000100000000,
Wrists = 0b001000000000,
RFinger = 0b010000000000,
LFinger = 0b100000000000,
All = 0b111111111111,
}
public static class CharacterEquipMaskExtensions
@ -28,16 +30,16 @@ public static class CharacterEquipMaskExtensions
=> slot switch
{
EquipSlot.Unknown => false,
EquipSlot.Head => mask.HasFlag(CharacterEquipMask.Head),
EquipSlot.Body => mask.HasFlag(CharacterEquipMask.Body),
EquipSlot.Hands => mask.HasFlag(CharacterEquipMask.Hands),
EquipSlot.Legs => mask.HasFlag(CharacterEquipMask.Legs),
EquipSlot.Feet => mask.HasFlag(CharacterEquipMask.Feet),
EquipSlot.Ears => mask.HasFlag(CharacterEquipMask.Ears),
EquipSlot.Neck => mask.HasFlag(CharacterEquipMask.Neck),
EquipSlot.Wrists => mask.HasFlag(CharacterEquipMask.Wrists),
EquipSlot.Head => mask.HasFlag(CharacterEquipMask.Head),
EquipSlot.Body => mask.HasFlag(CharacterEquipMask.Body),
EquipSlot.Hands => mask.HasFlag(CharacterEquipMask.Hands),
EquipSlot.Legs => mask.HasFlag(CharacterEquipMask.Legs),
EquipSlot.Feet => mask.HasFlag(CharacterEquipMask.Feet),
EquipSlot.Ears => mask.HasFlag(CharacterEquipMask.Ears),
EquipSlot.Neck => mask.HasFlag(CharacterEquipMask.Neck),
EquipSlot.Wrists => mask.HasFlag(CharacterEquipMask.Wrists),
EquipSlot.RFinger => mask.HasFlag(CharacterEquipMask.RFinger),
EquipSlot.LFinger => mask.HasFlag(CharacterEquipMask.LFinger),
_ => false,
_ => false,
};
}

View file

@ -3,8 +3,37 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Structs;
// An Item wrapper struct that contains the item table, a precomputed name and the associated equip slot.
public readonly struct Item
{
public readonly Lumina.Excel.GeneratedSheets.Item Base;
public readonly string Name;
public readonly EquipSlot EquippableTo;
// Obtain the main model info used by the item.
public (SetId id, WeaponType type, ushort variant) MainModel
=> ParseModel(EquippableTo, Base.ModelMain);
// Obtain the sub model info used by the item. Will be 0 if the item has no sub model.
public (SetId id, WeaponType type, ushort variant) SubModel
=> ParseModel(EquippableTo, Base.ModelSub);
public bool HasSubModel
=> Base.ModelSub != 0;
// Create a new item from its sheet list with the given name and either the inferred equip slot or the given one.
public Item(Lumina.Excel.GeneratedSheets.Item item, string name, EquipSlot slot = EquipSlot.Unknown)
{
Base = item;
Name = name;
EquippableTo = slot == EquipSlot.Unknown ? ((EquipSlot)item.EquipSlotCategory.Row).ToSlot() : slot;
}
// Create empty Nothing items.
public static Item Nothing(EquipSlot slot)
=> new("Nothing", slot);
// Produce the relevant model information for a given item and equip slot.
private static (SetId id, WeaponType type, ushort variant) ParseModel(EquipSlot slot, ulong data)
{
if (slot is EquipSlot.MainHand or EquipSlot.OffHand)
@ -22,33 +51,14 @@ public readonly struct Item
}
}
public readonly Lumina.Excel.GeneratedSheets.Item Base;
public readonly string Name;
public readonly EquipSlot EquippableTo;
public (SetId id, WeaponType type, ushort variant) MainModel
=> ParseModel(EquippableTo, Base.ModelMain);
public bool HasSubModel
=> Base.ModelSub != 0;
public (SetId id, WeaponType type, ushort variant) SubModel
=> ParseModel(EquippableTo, Base.ModelSub);
public Item(Lumina.Excel.GeneratedSheets.Item item, string name, EquipSlot slot = EquipSlot.Unknown)
{
Base = item;
Name = name;
EquippableTo = slot == EquipSlot.Unknown ? ((EquipSlot)item.EquipSlotCategory.Row).ToSlot() : slot;
}
public static Item Nothing(EquipSlot slot)
=> new("Nothing", slot);
// Used for 'Nothing' items.
private Item(string name, EquipSlot slot)
{
Name = name;
Base = new Lumina.Excel.GeneratedSheets.Item();
EquippableTo = slot;
}
public override string ToString()
=> Name;
}

View file

@ -1,7 +1,10 @@
using Lumina.Excel.GeneratedSheets;
using Dalamud.Utility;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.Structs;
// A struct containing the different jobs the game supports.
// Also contains the jobs Name and Abbreviation as strings.
public readonly struct Job
{
public readonly string Name;
@ -14,7 +17,10 @@ public readonly struct Job
public Job(ClassJob job)
{
Base = job;
Name = job.Name.ToString();
Abbreviation = job.Abbreviation.ToString();
Name = job.Name.ToDalamudString().ToString();
Abbreviation = job.Abbreviation.ToDalamudString().ToString();
}
public override string ToString()
=> Name;
}

View file

@ -1,29 +1,31 @@
using System.Diagnostics;
using System.Linq;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.Structs;
// The game specifies different job groups that can contain specific jobs or not.
public readonly struct JobGroup
{
public readonly string Name;
private readonly ulong _flags;
public readonly int Count;
public readonly uint Id;
public readonly string Name;
public readonly int Count;
public readonly uint Id;
private readonly ulong _flags;
// Create a job group from a given category and the ClassJob sheet.
// It looks up the different jobs contained in the category and sets the flags appropriately.
public JobGroup(ClassJobCategory group, ExcelSheet<ClassJob> jobs)
{
Count = 0;
Count = 0;
_flags = 0ul;
Id = group.RowId;
Name = group.Name.ToString();
Id = group.RowId;
Name = group.Name.ToString();
Debug.Assert(jobs.RowCount < 64);
foreach (var job in jobs)
{
var abbr = job.Abbreviation.ToString();
if (!abbr.Any())
if (abbr.Length == 0)
continue;
var prop = group.GetType().GetProperty(abbr);
@ -37,9 +39,11 @@ public readonly struct JobGroup
}
}
// Check if a job is contained inside this group.
public bool Fits(Job job)
=> Fits(job.Id);
// Check if a job is contained inside this group.
public bool Fits(uint jobId)
{
var flag = 1ul << (int)jobId;

View file

@ -1,22 +1,28 @@
using Penumbra.GameData.Structs;
using Dalamud.Utility;
using Penumbra.GameData.Structs;
namespace Glamourer.Structs;
// A wrapper for the clothing dyes the game provides with their RGBA color value, game ID, unmodified color value and name.
public readonly struct Stain
{
public readonly string Name;
public readonly uint RgbaColor;
// An empty stain with transparent color.
public static readonly Stain None = new("None");
public readonly string Name;
public readonly uint RgbaColor;
// Combine the Id byte with the 3 bytes of color values.
private readonly uint _seColorId;
public byte R
=> (byte)(RgbaColor & 0xFF);
public byte G
=> (byte)(RgbaColor >> 8 & 0xFF);
=> (byte)((RgbaColor >> 8) & 0xFF);
public byte B
=> (byte)(RgbaColor >> 16 & 0xFF);
=> (byte)((RgbaColor >> 16) & 0xFF);
public byte Intensity
=> (byte)((1 + R + G + B) / 3);
@ -27,23 +33,22 @@ public readonly struct Stain
public StainId RowIndex
=> (StainId)(_seColorId >> 24);
// R and B need to be shuffled and Alpha set to max.
public static uint SeColorToRgba(uint color)
=> (color & 0xFF) << 16 | color >> 16 & 0xFF | color & 0xFF00 | 0xFF000000;
=> ((color & 0xFF) << 16) | ((color >> 16) & 0xFF) | (color & 0xFF00) | 0xFF000000;
public Stain(byte index, Lumina.Excel.GeneratedSheets.Stain stain)
{
Name = stain.Name.ToString();
_seColorId = stain.Color | (uint)index << 24;
RgbaColor = SeColorToRgba(stain.Color);
Name = stain.Name.ToDalamudString().ToString();
_seColorId = stain.Color | ((uint)index << 24);
RgbaColor = SeColorToRgba(stain.Color);
}
public static readonly Stain None = new("None");
// Only used by None.
private Stain(string name)
{
Name = name;
Name = name;
_seColorId = 0;
RgbaColor = 0;
RgbaColor = 0;
}
}