Initial Commit

This commit is contained in:
Ottermandias 2021-07-30 17:23:15 +02:00
commit 164f304cf6
38 changed files with 2796 additions and 0 deletions

View file

@ -0,0 +1,168 @@
using System;
using System.ComponentModel;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer
{
public static class WriteExtensions
{
private static unsafe void Write(IntPtr actorPtr, EquipSlot slot, SetId? id, WeaponType? type, ushort? variant, StainId? stain)
{
void WriteWeapon(int offset)
{
var address = (byte*) actorPtr + offset;
if (id.HasValue)
*(ushort*) address = (ushort) id.Value;
if (type.HasValue)
*(ushort*) (address + 2) = (ushort) type.Value;
if (variant.HasValue)
*(ushort*) (address + 4) = variant.Value;
if (stain.HasValue)
*(address + 6) = (byte) stain.Value;
}
void WriteEquip(int offset)
{
var address = (byte*) actorPtr + offset;
if (id.HasValue)
*(ushort*) address = (ushort) id.Value;
if (variant < byte.MaxValue)
*(address + 2) = (byte) variant.Value;
if (stain.HasValue)
*(address + 3) = (byte) stain.Value;
}
switch (slot)
{
case EquipSlot.MainHand:
WriteWeapon(ActorEquipment.MainWeaponOffset);
break;
case EquipSlot.OffHand:
WriteWeapon(ActorEquipment.OffWeaponOffset);
break;
case EquipSlot.Head:
WriteEquip(ActorEquipment.EquipmentOffset);
break;
case EquipSlot.Body:
WriteEquip(ActorEquipment.EquipmentOffset + 4);
break;
case EquipSlot.Hands:
WriteEquip(ActorEquipment.EquipmentOffset + 8);
break;
case EquipSlot.Legs:
WriteEquip(ActorEquipment.EquipmentOffset + 12);
break;
case EquipSlot.Feet:
WriteEquip(ActorEquipment.EquipmentOffset + 16);
break;
case EquipSlot.Ears:
WriteEquip(ActorEquipment.EquipmentOffset + 20);
break;
case EquipSlot.Neck:
WriteEquip(ActorEquipment.EquipmentOffset + 24);
break;
case EquipSlot.Wrists:
WriteEquip(ActorEquipment.EquipmentOffset + 28);
break;
case EquipSlot.RFinger:
WriteEquip(ActorEquipment.EquipmentOffset + 32);
break;
case EquipSlot.LFinger:
WriteEquip(ActorEquipment.EquipmentOffset + 36);
break;
default: throw new InvalidEnumArgumentException();
}
}
public static void Write(this Stain stain, IntPtr actorPtr, EquipSlot slot)
=> Write(actorPtr, slot, null, null, null, stain.RowIndex);
public static void Write(this Item item, IntPtr actorAddress)
{
var (id, type, variant) = item.MainModel;
Write(actorAddress, item.EquippableTo, id, type, variant, null);
if (item.EquippableTo == EquipSlot.MainHand && item.HasSubModel)
{
var (subId, subType, subVariant) = item.SubModel;
Write(actorAddress, EquipSlot.OffHand, subId, subType, subVariant, null);
}
}
public static void Write(this ActorArmor armor, IntPtr actorAddress, EquipSlot slot)
=> Write(actorAddress, slot, armor.Set, null, armor.Variant, armor.Stain);
public static void Write(this ActorWeapon weapon, IntPtr actorAddress, EquipSlot slot)
=> Write(actorAddress, slot, weapon.Set, weapon.Type, weapon.Variant, weapon.Stain);
public static unsafe void Write(this ActorEquipment equip, IntPtr actorAddress)
{
if (equip.IsSet == 0)
return;
Write(actorAddress, EquipSlot.MainHand, equip.MainHand.Set, equip.MainHand.Type, equip.MainHand.Variant, equip.MainHand.Stain);
Write(actorAddress, EquipSlot.OffHand, equip.OffHand.Set, equip.OffHand.Type, equip.OffHand.Variant, equip.OffHand.Stain);
fixed (ActorArmor* equipment = &equip.Head)
{
Buffer.MemoryCopy(equipment, (byte*) actorAddress + ActorEquipment.EquipmentOffset,
ActorEquipment.EquipmentSlots * sizeof(ActorArmor), ActorEquipment.EquipmentSlots * sizeof(ActorArmor));
}
}
public static void Write(this ActorEquipment equip, IntPtr actorAddress, ActorEquipMask models, ActorEquipMask stains)
{
if (models == ActorEquipMask.All && stains == ActorEquipMask.All)
{
equip.Write(actorAddress);
return;
}
if (models.HasFlag(ActorEquipMask.MainHand))
Write(actorAddress, EquipSlot.MainHand, equip.MainHand.Set, equip.MainHand.Type, equip.MainHand.Variant, null);
if (stains.HasFlag(ActorEquipMask.MainHand))
Write(actorAddress, EquipSlot.MainHand, null, null, null, equip.MainHand.Stain);
if (models.HasFlag(ActorEquipMask.OffHand))
Write(actorAddress, EquipSlot.OffHand, equip.OffHand.Set, equip.OffHand.Type, equip.OffHand.Variant, null);
if (stains.HasFlag(ActorEquipMask.OffHand))
Write(actorAddress, EquipSlot.OffHand, null, null, null, equip.OffHand.Stain);
if (models.HasFlag(ActorEquipMask.Head))
Write(actorAddress, EquipSlot.Head, equip.Head.Set, null, equip.Head.Variant, null);
if (stains.HasFlag(ActorEquipMask.Head))
Write(actorAddress, EquipSlot.Head, null, null, null, equip.Head.Stain);
if (models.HasFlag(ActorEquipMask.Body))
Write(actorAddress, EquipSlot.Body, equip.Body.Set, null, equip.Body.Variant, null);
if (stains.HasFlag(ActorEquipMask.Body))
Write(actorAddress, EquipSlot.Body, null, null, null, equip.Body.Stain);
if (models.HasFlag(ActorEquipMask.Hands))
Write(actorAddress, EquipSlot.Hands, equip.Hands.Set, null, equip.Hands.Variant, null);
if (stains.HasFlag(ActorEquipMask.Hands))
Write(actorAddress, EquipSlot.Hands, null, null, null, equip.Hands.Stain);
if (models.HasFlag(ActorEquipMask.Legs))
Write(actorAddress, EquipSlot.Legs, equip.Legs.Set, null, equip.Legs.Variant, null);
if (stains.HasFlag(ActorEquipMask.Legs))
Write(actorAddress, EquipSlot.Legs, null, null, null, equip.Legs.Stain);
if (models.HasFlag(ActorEquipMask.Feet))
Write(actorAddress, EquipSlot.Feet, equip.Feet.Set, null, equip.Feet.Variant, null);
if (stains.HasFlag(ActorEquipMask.Feet))
Write(actorAddress, EquipSlot.Feet, null, null, null, equip.Feet.Stain);
if (models.HasFlag(ActorEquipMask.Ears))
Write(actorAddress, EquipSlot.Ears, equip.Ears.Set, null, equip.Ears.Variant, null);
if (models.HasFlag(ActorEquipMask.Neck))
Write(actorAddress, EquipSlot.Neck, equip.Neck.Set, null, equip.Neck.Variant, null);
if (models.HasFlag(ActorEquipMask.Wrists))
Write(actorAddress, EquipSlot.Wrists, equip.Wrists.Set, null, equip.Wrists.Variant, null);
if (models.HasFlag(ActorEquipMask.LFinger))
Write(actorAddress, EquipSlot.LFinger, equip.LFinger.Set, null, equip.LFinger.Variant, null);
if (models.HasFlag(ActorEquipMask.RFinger))
Write(actorAddress, EquipSlot.RFinger, equip.RFinger.Set, null, equip.RFinger.Variant, null);
}
}
}

View file

@ -0,0 +1,23 @@
using System;
namespace Glamourer
{
[Flags]
public enum ActorEquipMask : ushort
{
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,
}
}

View file

@ -0,0 +1,124 @@
using Lumina.Data;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.Customization
{
[Sheet("CharaMakeParams")]
public class CharaMakeParams : ExcelRow
{
public const int NumMenus = 28;
public const int NumVoices = 12;
public const int NumGraphics = 10;
public const int MaxNumValues = 100;
public const int NumFaces = 8;
public const int NumFeatures = 7;
public const int NumEquip = 3;
public enum MenuType
{
ListSelector = 0,
IconSelector = 1,
ColorPicker = 2,
DoubleColorPicker = 3,
MultiIconSelector = 4,
Percentage = 5,
}
public struct Menu
{
public uint Id;
public byte InitVal;
public MenuType Type;
public byte Size;
public byte LookAt;
public uint Mask;
public CustomizationId Customization;
public uint[] Values;
public byte[] Graphic;
}
public struct FacialFeatures
{
public uint[] Icons;
}
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];
public FacialFeatures[] FacialFeatureByFace { get; set; } = new FacialFeatures[NumFaces];
public CharaMakeType.UnkStruct3347Struct[] Equip { get; set; } = new CharaMakeType.UnkStruct3347Struct[NumEquip];
public override void PopulateData(RowParser parser, Lumina.GameData gameData, Language language)
{
RowId = parser.Row;
SubRowId = parser.SubRow;
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);
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);
Menus[i].Values = new uint[Menus[i].Size];
switch (Menus[i].Type)
{
case MenuType.ColorPicker:
case MenuType.DoubleColorPicker:
case MenuType.Percentage:
break;
default:
for (var j = 0; j < Menus[i].Size; ++j)
Menus[i].Values[j] = parser.ReadColumn<uint>(3 + (7 + j) * NumMenus + i);
break;
}
Menus[i].Graphic = new byte[NumGraphics];
for (var j = 0; j < NumGraphics; ++j)
Menus[i].Graphic[j] = parser.ReadColumn<byte>(3 + (MaxNumValues + 7 + j) * NumMenus + i);
}
for (var i = 0; i < NumVoices; ++i)
Voices[i] = parser.ReadColumn<byte>(3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + i);
for (var i = 0; i < NumFaces; ++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);
}
for (var i = 0; i < NumEquip; ++i)
{
Equip[i] = new CharaMakeType.UnkStruct3347Struct
{
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),
};
}
}
}
}

View file

@ -0,0 +1,23 @@
using Dalamud.Plugin;
namespace Glamourer
{
public class CmpFile
{
public readonly Lumina.Data.FileResource File;
public readonly uint[] RgbaColors;
public CmpFile(DalamudPluginInterface pi)
{
File = pi.Data.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);
}
}
}
}

View file

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

View file

@ -0,0 +1,70 @@
using System;
namespace Glamourer.Customization
{
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 CharaMakeParams.MenuType ToType(this CustomizationId customizationId, bool isHrothgar = false)
=> 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.TailEarShape => CharaMakeParams.MenuType.IconSelector,
CustomizationId.BustSize => CharaMakeParams.MenuType.Percentage,
CustomizationId.FacePaint => CharaMakeParams.MenuType.IconSelector,
CustomizationId.FacePaintColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.LipColor => isHrothgar ? CharaMakeParams.MenuType.IconSelector : CharaMakeParams.MenuType.ColorPicker,
_ => throw new ArgumentOutOfRangeException(nameof(customizationId), customizationId, null),
};
}
}

View file

@ -0,0 +1,35 @@
using System.Collections.Generic;
using Dalamud.Plugin;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization
{
public class CustomizationManager : ICustomizationManager
{
private static CustomizationOptions? _options;
private CustomizationManager()
{ }
public static ICustomizationManager Create(DalamudPluginInterface pi)
{
_options ??= new CustomizationOptions(pi);
return new CustomizationManager();
}
public IReadOnlyList<Race> Races
=> CustomizationOptions.Races;
public IReadOnlyList<SubRace> Clans
=> CustomizationOptions.Clans;
public IReadOnlyList<Gender> Genders
=> CustomizationOptions.Genders;
public CustomizationSet GetList(SubRace clan, Gender gender)
=> _options!.GetList(clan, gender);
public ImGuiScene.TextureWrap GetIcon(uint iconId)
=> _options!.GetIcon(iconId);
}
}

View file

@ -0,0 +1,264 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Dalamud;
using Dalamud.Plugin;
using Glamourer.Util;
using Lumina.Data;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.Customization
{
public class CustomizationOptions
{
internal static readonly Race[] Races = ((Race[]) Enum.GetValues(typeof(Race))).Skip(1).ToArray();
internal static readonly SubRace[] Clans = ((SubRace[]) Enum.GetValues(typeof(SubRace))).Skip(1).ToArray();
internal static readonly Gender[] Genders =
{
Gender.Male,
Gender.Female,
};
internal CustomizationSet GetList(SubRace race, Gender gender)
=> _list[ToIndex(race, gender)];
internal ImGuiScene.TextureWrap GetIcon(uint id)
=> _icons.LoadIcon(id);
private static readonly int ListSize = Clans.Length * Genders.Length;
private readonly CustomizationSet[] _list = new CustomizationSet[ListSize];
private readonly IconStorage _icons;
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)
{
var (skinOffset, hairOffset) = race switch
{
SubRace.Midlander => gender == Gender.Male ? (0x1200, 0x1300) : (0x0D00, 0x0E00),
SubRace.Highlander => gender == Gender.Male ? (0x1C00, 0x1D00) : (0x1700, 0x1800),
SubRace.Wildwood => gender == Gender.Male ? (0x2600, 0x2700) : (0x2100, 0x2200),
SubRace.Duskwright => gender == Gender.Male ? (0x3000, 0x3100) : (0x2B00, 0x2C00),
SubRace.Plainsfolk => gender == Gender.Male ? (0x3A00, 0x3B00) : (0x3500, 0x3600),
SubRace.Dunesfolk => gender == Gender.Male ? (0x4400, 0x4500) : (0x3F00, 0x4000),
SubRace.SeekerOfTheSun => gender == Gender.Male ? (0x4E00, 0x4F00) : (0x4900, 0x4A00),
SubRace.KeeperOfTheMoon => gender == Gender.Male ? (0x5800, 0x5900) : (0x5300, 0x5400),
SubRace.Seawolf => gender == Gender.Male ? (0x6200, 0x6300) : (0x5D00, 0x5E00),
SubRace.Hellsguard => gender == Gender.Male ? (0x6C00, 0x6D00) : (0x6700, 0x6800),
SubRace.Raen => gender == Gender.Male ? (0x7100, 0x7700) : (0x7600, 0x7200),
SubRace.Xaela => gender == Gender.Male ? (0x7B00, 0x8100) : (0x8000, 0x7C00),
SubRace.Hellion => gender == Gender.Male ? (0x8500, 0x8600) : (0x0000, 0x0000),
SubRace.Lost => gender == Gender.Male ? (0x8C00, 0x8F00) : (0x0000, 0x0000),
SubRace.Rava => gender == Gender.Male ? (0x0000, 0x0000) : (0x9E00, 0x9F00),
SubRace.Veena => gender == Gender.Male ? (0x0000, 0x0000) : (0xA800, 0xA900),
_ => throw new ArgumentOutOfRangeException(nameof(race), race, null),
};
return (CreateColorPicker(CustomizationId.SkinColor, skinOffset, 192),
CreateColorPicker(CustomizationId.HairColor, hairOffset, 192));
}
private Customization FromValueAndIndex(CustomizationId id, uint value, int index)
{
var row = _customizeSheet.GetRow(value);
return row == null
? new Customization(id, (byte) index, value, 0)
: new Customization(id, row.FeatureID, row.Icon, (ushort) row.RowId);
}
private 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(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.FacePaint)?.Values
.Select((v, i) => FromValueAndIndex(CustomizationId.FacePaint, v, i)).ToArray()
?? Array.Empty<Customization>();
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 = race.ToRace() == Race.Hrothgar ? HrothgarFaces(row) : 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(row),
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 = race.ToRace() == Race.Hrothgar ? set.HairStyles.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;
return set;
}
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet;
private readonly ExcelSheet<CharaMakeParams> _listSheet;
private readonly ExcelSheet<HairMakeType> _hairSheet;
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 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)
{
_cmpFile = new CmpFile(pi);
_customizeSheet = pi.Data.GetExcelSheet<CharaMakeCustomize>();
var tmp = pi.Data.Excel.GetType()!.GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)!
.MakeGenericMethod(typeof(CharaMakeParams))!.Invoke(pi.Data.Excel, new object?[]
{
"charamaketype",
FromClientLanguage(pi.ClientState.ClientLanguage),
null,
}) as ExcelSheet<CharaMakeParams>;
_listSheet = tmp!;
_hairSheet = pi.Data.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);
_icons = new IconStorage(pi, _list.Length * 50);
foreach (var race in Clans)
{
foreach (var gender in Genders)
_list[ToIndex(race, gender)] = GetSet(race, gender);
}
}
}
}

View file

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization
{
public class CustomizationSet
{
public const int DefaultAvailable =
(1 << (int) CustomizationId.Height)
| (1 << (int) CustomizationId.Hairstyle)
| (1 << (int) CustomizationId.HighlightsOnFlag)
| (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.Viera && gender == Gender.Male
|| clan.ToRace() == Race.Hrothgar && gender == Gender.Female
? 0
: DefaultAvailable;
}
public Gender Gender { get; }
public SubRace Clan { get; }
public Race Race
=> Clan.ToRace();
private int _settingAvailable = DefaultAvailable;
internal void SetAvailable(CustomizationId id)
=> _settingAvailable |= 1 << (int) id;
public bool IsAvailable(CustomizationId id)
=> (_settingAvailable & (1 << (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 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<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 IReadOnlyDictionary<CustomizationId, string> OptionName { get; internal set; } = null!;
public Customization FacialFeature(int faceIdx, int idx)
=> FeaturesTattoos[faceIdx][idx];
public Customization Data(CustomizationId id, int idx)
{
if (idx > Count(id))
throw new IndexOutOfRangeException();
switch (id.ToType())
{
case CharaMakeParams.MenuType.Percentage: return new Customization(id, (byte) idx, 0, (ushort) idx);
case CharaMakeParams.MenuType.ListSelector: return new Customization(id, (byte) idx, 0, (ushort) idx);
}
return id switch
{
CustomizationId.Face => Faces[idx],
CustomizationId.Hairstyle => HairStyles[idx],
CustomizationId.TailEarShape => TailEarShapes[idx],
CustomizationId.FacePaint => FacePaints[idx],
CustomizationId.FacialFeaturesTattoos => FeaturesTattoos[0][idx],
CustomizationId.SkinColor => SkinColors[idx],
CustomizationId.EyeColorL => EyeColors[idx],
CustomizationId.EyeColorR => EyeColors[idx],
CustomizationId.HairColor => HairColors[idx],
CustomizationId.HighlightColor => HighlightColors[idx],
CustomizationId.TattooColor => TattooColors[idx],
CustomizationId.LipColor => idx < 96 ? LipColorsDark[idx] : LipColorsLight[idx - 96],
CustomizationId.FacePaintColor => idx < 96 ? FacePaintColorsDark[idx] : FacePaintColorsLight[idx - 96],
_ => new Customization(0, 0),
};
}
public int Count(CustomizationId id)
{
if (!IsAvailable(id))
return 0;
if (id.ToType() == CharaMakeParams.MenuType.Percentage)
return 101;
return id switch
{
CustomizationId.Face => Faces.Count,
CustomizationId.Hairstyle => HairStyles.Count,
CustomizationId.HighlightsOnFlag => 2,
CustomizationId.SkinColor => SkinColors.Count,
CustomizationId.EyeColorR => EyeColors.Count,
CustomizationId.HairColor => HairColors.Count,
CustomizationId.HighlightColor => HighlightColors.Count,
CustomizationId.FacialFeaturesTattoos => 8,
CustomizationId.TattooColor => TattooColors.Count,
CustomizationId.Eyebrows => NumEyebrows,
CustomizationId.EyeColorL => EyeColors.Count,
CustomizationId.EyeShape => NumEyeShapes,
CustomizationId.Nose => NumNoseShapes,
CustomizationId.Jaw => NumJawShapes,
CustomizationId.Mouth => NumMouthShapes,
CustomizationId.LipColor => LipColorsLight.Count + LipColorsDark.Count,
CustomizationId.TailEarShape => TailEarShapes.Count,
CustomizationId.FacePaint => FacePaints.Count,
CustomizationId.FacePaintColor => FacePaintColorsLight.Count + FacePaintColorsDark.Count,
_ => throw new ArgumentOutOfRangeException(nameof(id), id, null),
};
}
}
}

View file

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

View file

@ -0,0 +1,61 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<LangVersion>preview</LangVersion>
<RootNamespace>Glamourer</RootNamespace>
<AssemblyName>Glamourer.GameData</AssemblyName>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Company>SoftOtter</Company>
<Product>Glamourer</Product>
<Copyright>Copyright © 2020</Copyright>
<Deterministic>true</Deterministic>
<OutputType>Library</OutputType>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<OutputPath>bin\$(Configuration)\</OutputPath>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>full</DebugType>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
</PropertyGroup>
<PropertyGroup>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DALAMUD_ROOT)\Dalamud.dll</HintPath>
<HintPath>..\libs\Dalamud.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DALAMUD_ROOT)\Lumina.dll</HintPath>
<HintPath>..\libs\Lumina.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DALAMUD_ROOT)\Lumina.Excel.dll</HintPath>
<HintPath>..\libs\Lumina.Excel.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Penumbra.GameData">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.GameData.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Folder Include="Util\" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,61 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<LangVersion>preview</LangVersion>
<RootNamespace>Glamourer</RootNamespace>
<AssemblyName>Glamourer.GameData</AssemblyName>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Company>SoftOtter</Company>
<Product>Glamourer</Product>
<Copyright>Copyright © 2020</Copyright>
<Deterministic>true</Deterministic>
<OutputType>Library</OutputType>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<OutputPath>bin\$(Configuration)\</OutputPath>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>full</DebugType>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
</PropertyGroup>
<PropertyGroup>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DALAMUD_ROOT)\Dalamud.dll</HintPath>
<HintPath>..\libs\Dalamud.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DALAMUD_ROOT)\Lumina.dll</HintPath>
<HintPath>..\libs\Lumina.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DALAMUD_ROOT)\Lumina.Excel.dll</HintPath>
<HintPath>..\libs\Lumina.Excel.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Penumbra.GameData">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.GameData.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,45 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer
{
public readonly struct Item
{
private static (SetId id, WeaponType type, ushort variant) ParseModel(EquipSlot slot, ulong data)
{
if (slot == EquipSlot.MainHand || slot == EquipSlot.OffHand)
{
var id = (SetId) (data & 0xFFFF);
var type = (WeaponType) ((data >> 16) & 0xFFFF);
var variant = (ushort) ((data >> 32) & 0xFFFF);
return (id, type, variant);
}
else
{
var id = (SetId) (data & 0xFFFF);
var variant = (byte) ((data >> 16) & 0xFF);
return (id, new WeaponType(), variant);
}
}
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;
}
}
}

View file

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin;
using Penumbra.GameData.Enums;
namespace Glamourer
{
public static class GameData
{
private static Dictionary<byte, Stain>? _stains;
private static Dictionary<EquipSlot, List<Item>>? _itemsBySlot;
public static IReadOnlyDictionary<byte, Stain> Stains(DalamudPluginInterface pi)
{
if (_stains != null)
return _stains;
var sheet = pi.Data.GetExcelSheet<Lumina.Excel.GeneratedSheets.Stain>();
_stains = sheet.Where(s => s.Color != 0).ToDictionary(s => (byte) s.RowId, s => new Stain((byte) s.RowId, s));
return _stains;
}
public static IReadOnlyDictionary<EquipSlot, List<Item>> ItemsBySlot(DalamudPluginInterface pi)
{
if (_itemsBySlot != null)
return _itemsBySlot;
var sheet = pi.Data.GetExcelSheet<Lumina.Excel.GeneratedSheets.Item>();
Item EmptySlot(EquipSlot slot)
=> new(sheet.First(), "Nothing", slot);
_itemsBySlot = new Dictionary<EquipSlot, List<Item>>()
{
[EquipSlot.Head] = new(200) { EmptySlot(EquipSlot.Head) },
[EquipSlot.Body] = new(200) { EmptySlot(EquipSlot.Body) },
[EquipSlot.Hands] = new(200) { EmptySlot(EquipSlot.Hands) },
[EquipSlot.Legs] = new(200) { EmptySlot(EquipSlot.Legs) },
[EquipSlot.Feet] = new(200) { EmptySlot(EquipSlot.Feet) },
[EquipSlot.RFinger] = new(200) { EmptySlot(EquipSlot.RFinger) },
[EquipSlot.Neck] = new(200) { EmptySlot(EquipSlot.Neck) },
[EquipSlot.MainHand] = new(200) { EmptySlot(EquipSlot.MainHand) },
[EquipSlot.OffHand] = new(200) { EmptySlot(EquipSlot.OffHand) },
[EquipSlot.Wrists] = new(200) { EmptySlot(EquipSlot.Wrists) },
[EquipSlot.Ears] = new(200) { EmptySlot(EquipSlot.Ears) },
};
foreach (var item in sheet)
{
var name = item.Name.ToString();
if (!name.Any())
continue;
var slot = (EquipSlot) item.EquipSlotCategory.Row;
if (slot == EquipSlot.Unknown)
continue;
slot = slot.ToSlot();
if (!_itemsBySlot.TryGetValue(slot, out var list))
continue;
list.Add(new Item(item, name, slot));
}
foreach (var list in _itemsBySlot.Values)
list.Sort((i1, i2) => string.Compare(i1.Name, i2.Name, StringComparison.InvariantCulture));
_itemsBySlot[EquipSlot.LFinger] = _itemsBySlot[EquipSlot.RFinger];
return _itemsBySlot;
}
}
}

View file

@ -0,0 +1,41 @@
using Penumbra.GameData.Structs;
namespace Glamourer
{
public readonly struct Stain
{
public readonly string Name;
public readonly uint RgbaColor;
private readonly uint _seColorId;
public byte R
=> (byte) (RgbaColor & 0xFF);
public byte G
=> (byte) ((RgbaColor >> 8) & 0xFF);
public byte B
=> (byte) ((RgbaColor >> 16) & 0xFF);
public byte Intensity
=> (byte) ((1 + R + G + B) / 3);
public uint SeColor
=> _seColorId & 0x00FFFFFF;
public StainId RowIndex
=> (StainId) (_seColorId >> 24);
public static uint SeColorToRgba(uint color)
=> ((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);
}
}
}

View file

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using Dalamud.Data.LuminaExtensions;
using Dalamud.Plugin;
using ImGuiScene;
using Lumina.Data.Files;
namespace Glamourer.Util
{
public class IconStorage : IDisposable
{
private readonly DalamudPluginInterface _pi;
private readonly Dictionary<int, TextureWrap> _icons;
public IconStorage(DalamudPluginInterface pi, int size = 0)
{
_pi = pi;
_icons = new Dictionary<int, TextureWrap>(size);
}
public TextureWrap this[int id]
=> LoadIcon(id);
private TexFile? LoadIconHq(int id)
{
var path = $"ui/icon/{id / 1000 * 1000:000000}/{id:000000}_hr1.tex";
return _pi.Data.GetFile<TexFile>(path);
}
public TextureWrap LoadIcon(uint id)
=> LoadIcon((int) id);
public TextureWrap LoadIcon(int id)
{
if (_icons.TryGetValue(id, out var ret))
return ret;
var icon = LoadIconHq(id) ?? _pi.Data.GetIcon(id);
var iconData = icon.GetRgbaImageData();
ret = _pi.UiBuilder.LoadImageRaw(iconData, icon.Header.Width, icon.Header.Height, 4);
_icons[id] = ret;
return ret;
}
public void Dispose()
{
foreach (var icon in _icons.Values)
icon.Dispose();
_icons.Clear();
}
~IconStorage()
=> Dispose();
}
}