diff --git a/Glamourer.GameData/Customization/ActorCustomization.cs b/Glamourer.GameData/Customization/ActorCustomization.cs new file mode 100644 index 0000000..7408e72 --- /dev/null +++ b/Glamourer.GameData/Customization/ActorCustomization.cs @@ -0,0 +1,264 @@ +using System; +using System.Runtime.InteropServices; +using Penumbra.GameData.Enums; + +namespace Glamourer.Customization +{ + public unsafe struct LazyCustomization + { + public ActorCustomization* Address; + + public LazyCustomization(IntPtr actorPtr) + => Address = (ActorCustomization*) (actorPtr + ActorCustomization.CustomizationOffset); + + public ref ActorCustomization Value + => ref *Address; + } + + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ActorCustomization + { + public const int CustomizationOffset = 0x1898; + public const int CustomizationBytes = 26; + + private byte _race; + private byte _gender; + public byte BodyType; + public byte Height; + public SubRace Clan; + public byte Face; + public byte Hairstyle; + private byte _highlightsOn; + public byte SkinColor; + public byte EyeColorRight; + public byte HairColor; + public byte HighlightsColor; + public byte FacialFeatures; + public byte TattooColor; + public byte Eyebrow; + public byte EyeColorLeft; + private byte _eyeShape; + public byte Nose; + public byte Jaw; + private byte _mouth; + public byte LipColor; + public byte MuscleMass; + public byte TailShape; + public byte BustSize; + private byte _facePaint; + public byte FacePaintColor; + + public Race Race + { + get => (Race) (_race > (byte) Race.Midlander ? _race + 1 : _race); + set => _race = (byte) (value > Race.Highlander ? value - 1 : value); + } + + public Gender Gender + { + get => (Gender) (_gender + 1); + set => _gender = (byte) (value - 1); + } + + public bool HighlightsOn + { + get => (_highlightsOn & 128) == 128; + set => _highlightsOn = (byte) (value ? _highlightsOn | 128 : _highlightsOn & 127); + } + + public bool FacialFeature(int idx) + => (FacialFeatures & (1 << idx)) != 0; + + public void FacialFeature(int idx, bool set) + { + if (set) + FacialFeatures |= (byte) (1 << idx); + else + FacialFeatures &= (byte) ~(1 << idx); + } + + public byte EyeShape + { + get => (byte) (_eyeShape & 127); + set => _eyeShape = (byte) ((value & 127) | (_eyeShape & 128)); + } + + public bool SmallIris + { + get => (_eyeShape & 128) == 128; + set => _eyeShape = (byte) (value ? _eyeShape | 128 : _eyeShape & 127); + } + + + public byte Mouth + { + get => (byte) (_mouth & 127); + set => _mouth = (byte) ((value & 127) | (_mouth & 128)); + } + + public bool Lipstick + { + get => (_mouth & 128) == 128; + set => _mouth = (byte) (value ? _mouth | 128 : _mouth & 127); + } + + public byte FacePaint + { + get => (byte) (_facePaint & 127); + set => _facePaint = (byte) ((value & 127) | (_facePaint & 128)); + } + + public bool FacePaintReversed + { + get => (_facePaint & 128) == 128; + set => _facePaint = (byte) (value ? _facePaint | 128 : _facePaint & 127); + } + + public unsafe void Read(IntPtr customizeAddress) + { + fixed (byte* ptr = &_race) + { + Buffer.MemoryCopy(customizeAddress.ToPointer(), ptr, CustomizationBytes, CustomizationBytes); + } + } + + public byte this[CustomizationId id] + { + get => id switch + { + CustomizationId.Race => (byte) Race, + CustomizationId.Gender => (byte) Gender, + CustomizationId.BodyType => BodyType, + CustomizationId.Height => Height, + CustomizationId.Clan => (byte) Clan, + CustomizationId.Face => Face, + CustomizationId.Hairstyle => Hairstyle, + CustomizationId.HighlightsOnFlag => _highlightsOn, + CustomizationId.SkinColor => SkinColor, + CustomizationId.EyeColorR => EyeColorRight, + CustomizationId.HairColor => HairColor, + CustomizationId.HighlightColor => HighlightsColor, + CustomizationId.FacialFeaturesTattoos => FacialFeatures, + CustomizationId.TattooColor => TattooColor, + CustomizationId.Eyebrows => Eyebrow, + CustomizationId.EyeColorL => EyeColorLeft, + CustomizationId.EyeShape => EyeShape, + CustomizationId.Nose => Nose, + CustomizationId.Jaw => Jaw, + CustomizationId.Mouth => Mouth, + CustomizationId.LipColor => LipColor, + CustomizationId.MuscleToneOrTailEarLength => MuscleMass, + CustomizationId.TailEarShape => TailShape, + CustomizationId.BustSize => BustSize, + CustomizationId.FacePaint => FacePaint, + CustomizationId.FacePaintColor => FacePaintColor, + _ => throw new ArgumentOutOfRangeException(nameof(id), id, null), + }; + set + { + 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); + } + } + } + + public unsafe void Write(IntPtr actorAddress) + { + fixed (byte* ptr = &_race) + { + Buffer.MemoryCopy(ptr, (byte*) actorAddress + CustomizationOffset, CustomizationBytes, CustomizationBytes); + } + } + + public unsafe byte[] ToBytes() + { + var ret = new byte[CustomizationBytes]; + fixed (byte* ptr = &_race) + { + Marshal.Copy(new IntPtr(ptr), ret, 0, CustomizationBytes); + } + + return ret; + } + } +} diff --git a/Glamourer.GameData/Customization/CustomName.cs b/Glamourer.GameData/Customization/CustomName.cs new file mode 100644 index 0000000..6476b85 --- /dev/null +++ b/Glamourer.GameData/Customization/CustomName.cs @@ -0,0 +1,46 @@ +namespace Glamourer.Customization +{ + public enum CustomName + { + Clan = 0, + Gender, + Reverse, + OddEyes, + IrisSmall, + IrisLarge, + 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, + + Num, + } +} diff --git a/Glamourer.GameData/Customization/CustomizationId.cs b/Glamourer.GameData/Customization/CustomizationId.cs index c38ff8a..19dc004 100644 --- a/Glamourer.GameData/Customization/CustomizationId.cs +++ b/Glamourer.GameData/Customization/CustomizationId.cs @@ -1,4 +1,5 @@ using System; +using Penumbra.GameData.Enums; namespace Glamourer.Customization { @@ -34,7 +35,40 @@ namespace Glamourer.Customization public static class CustomizationExtensions { - public static CharaMakeParams.MenuType ToType(this CustomizationId customizationId, bool isHrothgar = false) + 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.Midlander) => customizationId switch { CustomizationId.Race => CharaMakeParams.MenuType.IconSelector, @@ -58,13 +92,17 @@ namespace Glamourer.Customization 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), + 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), }; } } diff --git a/Glamourer.GameData/Customization/CustomizationManager.cs b/Glamourer.GameData/Customization/CustomizationManager.cs index 9212d17..086903c 100644 --- a/Glamourer.GameData/Customization/CustomizationManager.cs +++ b/Glamourer.GameData/Customization/CustomizationManager.cs @@ -31,5 +31,8 @@ namespace Glamourer.Customization public ImGuiScene.TextureWrap GetIcon(uint iconId) => _options!.GetIcon(iconId); + + public string GetName(CustomName name) + => _options!.GetName(name); } } diff --git a/Glamourer.GameData/Customization/CustomizationOptions.cs b/Glamourer.GameData/Customization/CustomizationOptions.cs index b3f497f..7e2fe03 100644 --- a/Glamourer.GameData/Customization/CustomizationOptions.cs +++ b/Glamourer.GameData/Customization/CustomizationOptions.cs @@ -13,7 +13,7 @@ using Race = Penumbra.GameData.Enums.Race; namespace Glamourer.Customization { - public class CustomizationOptions + public partial 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(); @@ -82,7 +82,7 @@ namespace Glamourer.Customization 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.Duskwight => 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), @@ -91,7 +91,7 @@ namespace Glamourer.Customization 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.Helion => 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), @@ -106,11 +106,11 @@ namespace Glamourer.Customization { 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); + ? new Customization(id, (byte) (index + 1), value, 0) + : new Customization(id, row.FeatureID, row.Icon, (ushort) row.RowId); } - private int GetListSize(CharaMakeParams row, CustomizationId id) + private static int GetListSize(CharaMakeParams row, CustomizationId id) { var menu = row.Menus.Cast().FirstOrDefault(m => m!.Value.Customization == id); return menu?.Size ?? 0; @@ -187,6 +187,7 @@ namespace Glamourer.Customization set.SetAvailable(CustomizationId.FacePaint); set.SetAvailable(CustomizationId.FacePaintColor); } + if (set.TailEarShapes.Count > 0) set.SetAvailable(CustomizationId.TailEarShape); if (set.Faces.Count > 0) @@ -204,12 +205,48 @@ namespace Glamourer.Customization set.FeaturesTattoos = featureDict; + set.OptionName = ((CustomizationId[]) Enum.GetValues(typeof(CustomizationId))).Select(c => + { + var menu = row.Menus + .Cast() + .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(); + + set._types = ((CustomizationId[]) Enum.GetValues(typeof(CustomizationId))).Select(c => + { + if (c == CustomizationId.HighlightColor) + return CharaMakeParams.MenuType.ColorPicker; + + if (c == CustomizationId.EyeColorL) + return CharaMakeParams.MenuType.ColorPicker; + + var menu = row.Menus + .Cast() + .FirstOrDefault(m => m!.Value.Customization == c); + return menu?.Type ?? CharaMakeParams.MenuType.ListSelector; + }).ToArray(); + return set; } private readonly ExcelSheet _customizeSheet; private readonly ExcelSheet _listSheet; private readonly ExcelSheet _hairSheet; + private readonly ExcelSheet _lobby; private readonly CmpFile _cmpFile; private readonly Customization[] _highlightPicker; private readonly Customization[] _eyeColorPicker; @@ -218,6 +255,10 @@ namespace Glamourer.Customization private readonly Customization[] _lipColorPickerDark; private readonly Customization[] _lipColorPickerLight; private readonly Customization[] _tattooColorPicker; + private readonly string[] _names = new string[(int) CustomName.Num]; + + public string GetName(CustomName name) + => _names[(int) name]; private static Language FromClientLanguage(ClientLanguage language) => language switch @@ -233,6 +274,7 @@ namespace Glamourer.Customization { _cmpFile = new CmpFile(pi); _customizeSheet = pi.Data.GetExcelSheet(); + _lobby = pi.Data.GetExcelSheet(); var tmp = pi.Data.Excel.GetType()!.GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)! .MakeGenericMethod(typeof(CharaMakeParams))!.Invoke(pi.Data.Excel, new object?[] { @@ -242,6 +284,7 @@ namespace Glamourer.Customization }) as ExcelSheet; _listSheet = tmp!; _hairSheet = pi.Data.GetExcelSheet(); + SetNames(pi); _highlightPicker = CreateColorPicker(CustomizationId.HighlightColor, 256, 192); _lipColorPickerDark = CreateColorPicker(CustomizationId.LipColor, 512, 96); @@ -258,5 +301,55 @@ namespace Glamourer.Customization _list[ToIndex(race, gender)] = GetSet(race, gender); } } + + public void SetNames(DalamudPluginInterface pi) + { + var subRace = pi.Data.GetExcelSheet(); + _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.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(); + } } } diff --git a/Glamourer.GameData/Customization/CustomizationSet.cs b/Glamourer.GameData/Customization/CustomizationSet.cs index a744710..035c956 100644 --- a/Glamourer.GameData/Customization/CustomizationSet.cs +++ b/Glamourer.GameData/Customization/CustomizationSet.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using ImGuiScene; using Penumbra.GameData.Enums; namespace Glamourer.Customization @@ -11,7 +10,6 @@ namespace Glamourer.Customization 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) @@ -54,6 +52,7 @@ namespace Glamourer.Customization public int NumMouthShapes { get; internal set; } + public IReadOnlyList OptionName { get; internal set; } = null!; public IReadOnlyList Faces { get; internal set; } = null!; public IReadOnlyList HairStyles { get; internal set; } = null!; public IReadOnlyList TailEarShapes { get; internal set; } = null!; @@ -70,7 +69,10 @@ namespace Glamourer.Customization public IReadOnlyList LipColorsLight { get; internal set; } = null!; public IReadOnlyList LipColorsDark { get; internal set; } = null!; - public IReadOnlyDictionary OptionName { get; internal set; } = null!; + public IReadOnlyList _types { get; internal set; } = null!; + + public string Option(CustomizationId id) + => OptionName[(int) id]; public Customization FacialFeature(int faceIdx, int idx) => FeaturesTattoos[faceIdx - 1][idx]; @@ -151,6 +153,10 @@ namespace Glamourer.Customization }; } + public CharaMakeParams.MenuType Type(CustomizationId id) + => _types[(int) id]; + + public int Count(CustomizationId id) { if (!IsAvailable(id)) diff --git a/Glamourer.GameData/Customization/CustomizationStruct.cs b/Glamourer.GameData/Customization/CustomizationStruct.cs deleted file mode 100644 index 941ea66..0000000 --- a/Glamourer.GameData/Customization/CustomizationStruct.cs +++ /dev/null @@ -1,247 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Penumbra.GameData.Enums; - -namespace Glamourer.Customization -{ - [StructLayout(LayoutKind.Sequential)] - public unsafe struct CustomizationStruct - { - public CustomizationStruct(IntPtr ptr) - => _ptr = (byte*) ptr; - - private readonly byte* _ptr; - - public byte this[CustomizationId id] - { - get => id switch - { - // Needs to handle the Highlander Race in enum. - CustomizationId.Race => (byte) (_ptr[(int) CustomizationId.Race] > 1 ? _ptr[(int) CustomizationId.Race] + 1 : 1), - // Needs to handle Gender.Unknown = 0. - CustomizationId.Gender => (byte) (_ptr[(int) id] + 1), - // Just a flag. - CustomizationId.HighlightsOnFlag => (byte) ((_ptr[(int) id] & 128) == 128 ? 1 : 0), - // Eye also includes iris flag at bit 128. - CustomizationId.EyeShape => (byte) (_ptr[(int) CustomizationId.EyeShape] & 127), - // Mouth also includes Lipstick flag at bit 128. - CustomizationId.Mouth => (byte) (_ptr[(int) CustomizationId.Mouth] & 127), - // FacePaint also includes Reverse bit at 128. - CustomizationId.FacePaint => (byte) (_ptr[(int) CustomizationId.FacePaint] & 127), - _ => _ptr[(int) id], - }; - set - { - _ptr[(int) id] = id switch - { - CustomizationId.Race => (byte) (value > (byte) Race.Midlander ? value - 1 : value), - CustomizationId.Gender => (byte) (value - 1), - CustomizationId.HighlightsOnFlag => (byte) (value != 0 ? 128 : 0), - CustomizationId.EyeShape => (byte) ((_ptr[(int) CustomizationId.EyeShape] & 128) | (value & 127)), - CustomizationId.Mouth => (byte) ((_ptr[(int) CustomizationId.Mouth] & 128) | (value & 127)), - CustomizationId.FacePaint => (byte) ((_ptr[(int) CustomizationId.FacePaint] & 128) | (value & 127)), - _ => value, - }; - - if (Race != Race.Hrothgar) - return; - - // Handle Hrothgar Face being dependent on hairstyle, 2 faces per hairstyle. - switch (id) - { - case CustomizationId.Hairstyle: - _ptr[(int) CustomizationId.Face] = (byte) ((value + 1) / 2); - break; - case CustomizationId.Face: - _ptr[(int) CustomizationId.Hairstyle] = (byte) (value * 2 - 1); - break; - } - } - } - - public Race Race - { - get => (Race) this[CustomizationId.Race]; - set => this[CustomizationId.Race] = (byte) value; - } - - public Gender Gender - { - get => (Gender) this[CustomizationId.Gender]; - set => this[CustomizationId.Gender] = (byte) value; - } - - public byte BodyType - { - get => this[CustomizationId.BodyType]; - set => this[CustomizationId.BodyType] = value; - } - - public byte Height - { - get => _ptr[(int) CustomizationId.Height]; - set => _ptr[(int) CustomizationId.Height] = value; - } - - public SubRace Clan - { - get => (SubRace) this[CustomizationId.Clan]; - set => this[CustomizationId.Clan] = (byte) value; - } - - public byte Face - { - get => this[CustomizationId.Face]; - set => this[CustomizationId.Face] = value; - } - - public byte Hairstyle - { - get => this[CustomizationId.Hairstyle]; - set => this[CustomizationId.Hairstyle] = value; - } - - public bool HighlightsOn - { - get => this[CustomizationId.HighlightsOnFlag] == 1; - set => this[CustomizationId.HighlightsOnFlag] = (byte) (value ? 1 : 0); - } - - public byte SkinColor - { - get => this[CustomizationId.SkinColor]; - set => this[CustomizationId.SkinColor] = value; - } - - public byte EyeColorRight - { - get => this[CustomizationId.EyeColorR]; - set => this[CustomizationId.EyeColorR] = value; - } - - public byte HairColor - { - get => this[CustomizationId.HairColor]; - set => this[CustomizationId.HairColor] = value; - } - - public byte HighlightsColor - { - get => this[CustomizationId.HighlightColor]; - set => this[CustomizationId.HighlightColor] = value; - } - - public bool FacialFeature(int idx) - => (this[CustomizationId.FacialFeaturesTattoos] & (1 << idx)) != 0; - - public void FacialFeature(int idx, bool set) - { - if (set) - this[CustomizationId.FacialFeaturesTattoos] |= (byte) (1 << idx); - else - this[CustomizationId.FacialFeaturesTattoos] &= (byte) ~(1 << idx); - } - - public byte FacialFeatures - { - get => this[CustomizationId.FacialFeaturesTattoos]; - set => this[CustomizationId.FacialFeaturesTattoos] = value; - } - - public byte TattooColor - { - get => this[CustomizationId.TattooColor]; - set => this[CustomizationId.TattooColor] = value; - } - - public byte Eyebrow - { - get => this[CustomizationId.Eyebrows]; - set => this[CustomizationId.Eyebrows] = value; - } - - public byte EyeColorLeft - { - get => this[CustomizationId.EyeColorL]; - set => this[CustomizationId.EyeColorL] = value; - } - - public byte Eye - { - get => this[CustomizationId.EyeShape]; - set => this[CustomizationId.EyeShape] = value; - } - - public bool SmallIris - { - get => (_ptr[(int) CustomizationId.EyeShape] & 128) == 128; - set => _ptr[(int) CustomizationId.EyeShape] = (byte) (this[CustomizationId.EyeShape] | (value ? 128u : 0u)); - } - - public byte Nose - { - get => _ptr[(int) CustomizationId.Nose]; - set => _ptr[(int) CustomizationId.Nose] = value; - } - - public byte Jaw - { - get => _ptr[(int) CustomizationId.Jaw]; - set => _ptr[(int) CustomizationId.Jaw] = value; - } - - public byte Mouth - { - get => this[CustomizationId.Mouth]; - set => this[CustomizationId.Mouth] = value; - } - - public bool LipstickSet - { - get => (_ptr[(int) CustomizationId.Mouth] & 128) == 128; - set => _ptr[(int) CustomizationId.Mouth] = (byte) (this[CustomizationId.Mouth] | (value ? 128u : 0u)); - } - - public byte LipColor - { - get => _ptr[(int) CustomizationId.LipColor]; - set => _ptr[(int) CustomizationId.LipColor] = value; - } - - public byte MuscleMass - { - get => _ptr[(int) CustomizationId.MuscleToneOrTailEarLength]; - set => _ptr[(int) CustomizationId.MuscleToneOrTailEarLength] = value; - } - - public byte TailShape - { - get => _ptr[(int) CustomizationId.TailEarShape]; - set => _ptr[(int) CustomizationId.TailEarShape] = value; - } - - public byte BustSize - { - get => _ptr[(int) CustomizationId.BustSize]; - set => _ptr[(int) CustomizationId.BustSize] = value; - } - - public byte FacePaint - { - get => this[CustomizationId.FacePaint]; - set => this[CustomizationId.FacePaint] = value; - } - - public bool FacePaintReversed - { - get => (_ptr[(int) CustomizationId.FacePaint] & 128) == 128; - set => _ptr[(int) CustomizationId.FacePaint] = (byte) (this[CustomizationId.FacePaint] | (value ? 128u : 0u)); - } - - public byte FacePaintColor - { - get => _ptr[(int) CustomizationId.FacePaintColor]; - set => _ptr[(int) CustomizationId.FacePaintColor] = value; - } - } -} diff --git a/Glamourer.GameData/Customization/ICustomizationManager.cs b/Glamourer.GameData/Customization/ICustomizationManager.cs index 8e03285..f0486ed 100644 --- a/Glamourer.GameData/Customization/ICustomizationManager.cs +++ b/Glamourer.GameData/Customization/ICustomizationManager.cs @@ -12,5 +12,6 @@ namespace Glamourer.Customization public CustomizationSet GetList(SubRace race, Gender gender); public ImGuiScene.TextureWrap GetIcon(uint iconId); + public string GetName(CustomName name); } } diff --git a/Glamourer/CharacterFlag.cs b/Glamourer/CharacterFlag.cs new file mode 100644 index 0000000..51ae8b4 --- /dev/null +++ b/Glamourer/CharacterFlag.cs @@ -0,0 +1,68 @@ +using System; + +namespace Glamourer +{ + [Flags] + public enum CharacterFlag : ulong + { + MainHand = 1ul << 0, + OffHand = 1ul << 1, + Head = 1ul << 2, + Body = 1ul << 3, + Hands = 1ul << 4, + Legs = 1ul << 5, + Feet = 1ul << 6, + Ears = 1ul << 7, + Neck = 1ul << 8, + Wrists = 1ul << 9, + RFinger = 1ul << 10, + LFinger = 1ul << 11, + ModelMask = (1ul << 12) - 1, + + StainMainHand = MainHand << 16, + StainOffHand = OffHand << 16, + StainHead = Head << 16, + StainBody = Body << 16, + StainHands = Hands << 16, + StainLegs = Legs << 16, + StainFeet = Feet << 16, + StainEars = Ears << 16, + StainNeck = Neck << 16, + StainWrists = Wrists << 16, + StainRFinger = RFinger << 16, + StainLFinger = LFinger << 16, + StainMask = ModelMask << 16, + EquipMask = ModelMask | StainMask, + + Race = 1ul << 32, + Gender = 1ul << 33, + BodyType = 1ul << 34, + Height = 1ul << 35, + Clan = 1ul << 36, + Face = 1ul << 37, + Hairstyle = 1ul << 38, + Highlights = 1ul << 39, + SkinColor = 1ul << 40, + EyeColorRight = 1ul << 41, + HairColor = 1ul << 42, + HighlightsColor = 1ul << 43, + FacialFeatures = 1ul << 44, + TattooColor = 1ul << 45, + Eyebrows = 1ul << 46, + EyeColorLeft = 1ul << 47, + EyeShape = 1ul << 48, + IrisSize = 1ul << 49, + NoseShape = 1ul << 50, + JawShape = 1ul << 51, + MouthShape = 1ul << 52, + Lipstick = 1ul << 53, + LipColor = 1ul << 54, + MuscleMass = 1ul << 55, + TailShape = 1ul << 56, + BustSize = 1ul << 57, + FacePaint = 1ul << 58, + FacePaintReversed = 1ul << 59, + FacePaintColor = 1ul << 60, + CustomizeMask = ((1ul << 61) - 1) & ~EquipMask, + } +} diff --git a/Glamourer/Glamourer.csproj b/Glamourer/Glamourer.csproj index 536d05f..943b075 100644 --- a/Glamourer/Glamourer.csproj +++ b/Glamourer/Glamourer.csproj @@ -35,6 +35,14 @@ OnOutputUpdated + + + + + + + + $(appdata)\XIVLauncher\addon\Hooks\dev\Dalamud.dll diff --git a/Glamourer/Gui/ComboWithFilter.cs b/Glamourer/Gui/ComboWithFilter.cs index 85b2dfc..69f4fdd 100644 --- a/Glamourer/Gui/ComboWithFilter.cs +++ b/Glamourer/Gui/ComboWithFilter.cs @@ -15,7 +15,7 @@ namespace Glamourer.Gui private string _currentFilterLower = string.Empty; private bool _focus; private readonly float _size; - private readonly float _previewSize; + private float _previewSize; private readonly IReadOnlyList _items; private readonly IReadOnlyList _itemNamesLower; private readonly Func _itemToName; @@ -130,8 +130,11 @@ namespace Glamourer.Gui return ret; } - public bool Draw(string currentName, out T? value) + public bool Draw(string currentName, out T? value, float? size = null) { + if (size.HasValue) + _previewSize = size.Value; + value = default; ImGui.SetNextItemWidth(_previewSize); PrePreview?.Invoke(); diff --git a/Glamourer/Gui/Interface.cs b/Glamourer/Gui/Interface.cs index fa4a009..a4d461c 100644 --- a/Glamourer/Gui/Interface.cs +++ b/Glamourer/Gui/Interface.cs @@ -2,61 +2,126 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Reflection; using Dalamud.Game.ClientState.Actors; using Dalamud.Game.ClientState.Actors.Types; +using Dalamud.Interface; using Dalamud.Plugin; using Glamourer.Customization; using ImGuiNET; +using Lumina.Excel.GeneratedSheets; using Penumbra.Api; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.PlayerWatch; +using Race = Penumbra.GameData.Enums.Race; namespace Glamourer.Gui { - internal partial class Interface : IDisposable + internal partial class Interface { - public const int GPoseActorId = 201; - private const string PluginName = "Glamourer"; - private readonly string _glamourerHeader; + // Push the stain color to type and if it is too bright, turn the text color black. + // Return number of pushed styles. + private static int PushColor(Stain stain, ImGuiCol type = ImGuiCol.Button) + { + ImGui.PushStyleColor(type, stain.RgbaColor); + if (stain.Intensity > 127) + { + ImGui.PushStyleColor(ImGuiCol.Text, 0xFF101010); + return 2; + } + return 1; + } + + // Update actors without triggering PlayerWatcher Events, + // then manually redraw using Penumbra. + public void UpdateActors(Actor actor) + { + var newEquip = _playerWatcher.UpdateActorWithoutEvent(actor); + GlamourerPlugin.Penumbra?.RedrawActor(actor, RedrawType.WithSettings); + + // Special case for carrying over changes to the gPose actor to the regular player actor, too. + var gPose = _actors[GPoseActorId]; + var player = _actors[0]; + if (gPose != null && actor.Address == gPose.Address && player != null) + newEquip.Write(player.Address); + } + + // Go through a whole customization struct and fix up all settings that need fixing. + private static void FixUpAttributes(LazyCustomization customization) + { + var set = GlamourerPlugin.Customization.GetList(customization.Value.Clan, customization.Value.Gender); + foreach (CustomizationId id in Enum.GetValues(typeof(CustomizationId))) + { + switch (id) + { + case CustomizationId.Race: break; + case CustomizationId.Clan: break; + case CustomizationId.BodyType: break; + case CustomizationId.Gender: break; + case CustomizationId.FacialFeaturesTattoos: break; + case CustomizationId.HighlightsOnFlag: break; + case CustomizationId.Face: + if (customization.Value.Race != Race.Hrothgar) + goto default; + break; + default: + var count = set.Count(id); + if (customization.Value[id] >= count) + if (count == 0) + customization.Value[id] = 0; + else + customization.Value[id] = set.Data(id, 0).Value; + break; + } + } + } + + // Change a race and fix up all required customizations afterwards. + private static bool ChangeRace(LazyCustomization customization, SubRace clan) + { + if (clan == customization.Value.Clan) + return false; + + var race = clan.ToRace(); + customization.Value.Race = race; + customization.Value.Clan = clan; + + customization.Value.Gender = race switch + { + Race.Hrothgar => Gender.Male, + Race.Viera => Gender.Female, + _ => customization.Value.Gender, + }; + + FixUpAttributes(customization); + + return true; + } + + // Change a gender and fix up all required customizations afterwards. + private static bool ChangeGender(LazyCustomization customization, Gender gender) + { + if (gender == customization.Value.Gender) + return false; + + customization.Value.Gender = gender; + FixUpAttributes(customization); + + return true; + } + } + + internal partial class Interface + { private const float ColorButtonWidth = 22.5f; private const float ColorComboWidth = 140f; + private const float ItemComboWidth = 300f; - private readonly IReadOnlyDictionary _stains; - private readonly IReadOnlyDictionary> _equip; - private readonly ActorTable _actors; - private readonly IObjectIdentifier _identifier; - private readonly Dictionary, ComboWithFilter)> _combos; - private readonly IPlayerWatcher _playerWatcher; - - private bool _visible = false; - - private Actor? _player; - - private static readonly Vector2 FeatureIconSizeIntern = - Vector2.One * ImGui.GetTextLineHeightWithSpacing() * 2 / ImGui.GetIO().FontGlobalScale; - - public static Vector2 FeatureIconSize - => FeatureIconSizeIntern * ImGui.GetIO().FontGlobalScale; - - - public Interface() - { - _glamourerHeader = GlamourerPlugin.Version.Length > 0 - ? $"{PluginName} v{GlamourerPlugin.Version}###{PluginName}Main" - : $"{PluginName}###{PluginName}Main"; - GlamourerPlugin.PluginInterface.UiBuilder.OnBuildUi += Draw; - GlamourerPlugin.PluginInterface.UiBuilder.OnOpenConfigUi += ToggleVisibility; - - _stains = GameData.Stains(GlamourerPlugin.PluginInterface); - _equip = GameData.ItemsBySlot(GlamourerPlugin.PluginInterface); - _identifier = Penumbra.GameData.GameData.GetIdentifier(GlamourerPlugin.PluginInterface); - _actors = GlamourerPlugin.PluginInterface.ClientState.Actors; - _playerWatcher = PlayerWatchFactory.Create(GlamourerPlugin.PluginInterface); - - var stainCombo = new ComboWithFilter("##StainCombo", ColorComboWidth, ColorButtonWidth, _stains.Values.ToArray(), + private ComboWithFilter CreateDefaultStainCombo(IReadOnlyList stains) + => new("##StainCombo", ColorComboWidth, ColorButtonWidth, stains, s => s.Name.ToString()) { Flags = ImGuiComboFlags.NoArrowButton | ImGuiComboFlags.HeightLarge, @@ -78,48 +143,30 @@ namespace Glamourer.Gui ItemsAtOnce = 12, }; - _combos = _equip.ToDictionary(kvp => kvp.Key, - kvp => (new ComboWithFilter($"{kvp.Key}##Equip", 300, 300, kvp.Value, i => i.Name) { Flags = ImGuiComboFlags.HeightLarge } - , new ComboWithFilter($"##{kvp.Key}Stain", stainCombo)) - ); - } - public void ToggleVisibility(object _, object _2) - => _visible = !_visible; - - public void Dispose() - { - _playerWatcher?.Dispose(); - GlamourerPlugin.PluginInterface.UiBuilder.OnBuildUi -= Draw; - GlamourerPlugin.PluginInterface.UiBuilder.OnOpenConfigUi -= ToggleVisibility; - } - - private string _currentActorName = ""; - - private static int PushColor(Stain stain, ImGuiCol type = ImGuiCol.Button) - { - ImGui.PushStyleColor(type, stain.RgbaColor); - if (stain.Intensity > 127) + private ComboWithFilter CreateItemCombo(EquipSlot slot, IReadOnlyList items) + => new($"{_equipSlotNames[slot]}##Equip", ItemComboWidth, ItemComboWidth, items, i => i.Name) { - ImGui.PushStyleColor(ImGuiCol.Text, 0xFF101010); - return 2; - } + Flags = ImGuiComboFlags.HeightLarge, + }; - return 1; - } + private (ComboWithFilter, ComboWithFilter) CreateCombos(EquipSlot slot, IReadOnlyList items, + ComboWithFilter defaultStain) + => (CreateItemCombo(slot, items), new ComboWithFilter($"##{slot}Stain", defaultStain)); + } - private bool DrawColorSelector(ComboWithFilter stainCombo, EquipSlot slot, StainId stainIdx) + internal partial class Interface + { + private bool DrawStainSelector(ComboWithFilter stainCombo, EquipSlot slot, StainId stainIdx) { - var name = string.Empty; stainCombo.PostPreview = null; if (_stains.TryGetValue((byte) stainIdx, out var stain)) { - name = stain.Name; var previewPush = PushColor(stain, ImGuiCol.FrameBg); stainCombo.PostPreview = () => ImGui.PopStyleColor(previewPush); } - if (stainCombo.Draw(string.Empty, out var newStain) && _player != null) + if (stainCombo.Draw(string.Empty, out var newStain) && _player != null && !newStain.RowIndex.Equals(stainIdx)) { newStain.Write(_player.Address, slot); return true; @@ -131,7 +178,7 @@ namespace Glamourer.Gui private bool DrawItemSelector(ComboWithFilter equipCombo, Lumina.Excel.GeneratedSheets.Item? item) { var currentName = item?.Name.ToString() ?? "Nothing"; - if (equipCombo.Draw(currentName, out var newItem) && _player != null) + if (equipCombo.Draw(currentName, out var newItem, _itemComboWidth) && _player != null && newItem.Base.RowId != item?.RowId) { newItem.Write(_player.Address); return true; @@ -144,8 +191,7 @@ namespace Glamourer.Gui { var (equipCombo, stainCombo) = _combos[slot]; - var ret = false; - ret = DrawColorSelector(stainCombo, slot, equip.Stain); + var ret = DrawStainSelector(stainCombo, slot, equip.Stain); ImGui.SameLine(); var item = _identifier.Identify(equip.Set, new WeaponType(), equip.Variant, slot); ret |= DrawItemSelector(equipCombo, item); @@ -157,33 +203,17 @@ namespace Glamourer.Gui { var (equipCombo, stainCombo) = _combos[slot]; - var ret = DrawColorSelector(stainCombo, slot, weapon.Stain); + var ret = DrawStainSelector(stainCombo, slot, weapon.Stain); ImGui.SameLine(); - - var item = _identifier.Identify(weapon.Set, weapon.Type, weapon.Variant, slot); ret |= DrawItemSelector(equipCombo, item); return ret; } + } - public void UpdateActors(Actor actor) - { - var newEquip = _playerWatcher.UpdateActorWithoutEvent(actor); - GlamourerPlugin.Penumbra?.RedrawActor(actor, RedrawType.WithSettings); - - var gPose = _actors[GPoseActorId]; - var player = _actors[0]; - if (gPose != null && actor.Address == gPose.Address && player != null) - newEquip.Write(player.Address); - } - - private SubRace _currentSubRace = SubRace.Midlander; - private Gender _currentGender = Gender.Male; - - private static readonly string[] - SubRaceNames = ((SubRace[]) Enum.GetValues(typeof(SubRace))).Skip(1).Select(s => s.ToName()).ToArray(); - + internal partial class Interface + { private static bool DrawColorPickerPopup(string label, CustomizationSet set, CustomizationId id, out Customization.Customization value) { value = default; @@ -197,7 +227,7 @@ namespace Glamourer.Gui for (var i = 0; i < count; ++i) { var custom = set.Data(id, i); - if (ImGui.ColorButton($"{i}", ImGui.ColorConvertU32ToFloat4(custom.Color))) + if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color))) { value = custom; ret = true; @@ -212,122 +242,192 @@ namespace Glamourer.Gui return ret; } - private static void FixUpAttributes(CustomizationStruct customization) + private Vector2 _iconSize = Vector2.Zero; + private Vector2 _actualIconSize = Vector2.Zero; + private float _raceSelectorWidth = 0; + private float _inputIntSize = 0; + private float _comboSelectorSize = 0; + private float _percentageSize = 0; + private float _itemComboWidth = 0; + + private bool InputInt(string label, ref int value, int minValue, int maxValue) { - var set = GlamourerPlugin.Customization.GetList(customization.Clan, customization.Gender); - foreach (CustomizationId id in Enum.GetValues(typeof(CustomizationId))) + var ret = false; + var tmp = value + 1; + ImGui.SetNextItemWidth(_inputIntSize); + if (ImGui.InputInt(label, ref tmp, 1) && tmp != value + 1 && tmp >= minValue && tmp <= maxValue) { - switch (id) - { - case CustomizationId.Race: break; - case CustomizationId.Clan: break; - case CustomizationId.BodyType: break; - case CustomizationId.Gender: break; - case CustomizationId.FacialFeaturesTattoos: break; - case CustomizationId.Face: - if (customization.Race != Race.Hrothgar) - goto default; - break; - default: - var count = set.Count(id); - if (customization[id] >= count) - customization[id] = set.Data(id, 0).Value; - break; - } + value = tmp - 1; + ret = true; } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Input Range: [{minValue}, {maxValue}]"); + + return ret; } - private static bool ChangeRace(CustomizationStruct customization, SubRace clan) + private static (int, Customization.Customization) GetCurrentCustomization(LazyCustomization customization, CustomizationId id, + CustomizationSet set) { - if (clan == customization.Clan) - return false; - - var race = clan.ToRace(); - customization.Race = race; - customization.Clan = clan; - - customization.Gender = race switch + var current = set.DataByValue(id, customization.Value[id], out var custom); + if (current < 0) { - Race.Hrothgar => Gender.Male, - Race.Viera => Gender.Female, - _ => customization.Gender, - }; + PluginLog.Warning($"Read invalid customization value {customization.Value[id]} for {id}."); + current = 0; + custom = set.Data(id, 0); + } - FixUpAttributes(customization); - - return true; + return (current, custom!.Value); } - private static bool ChangeGender(CustomizationStruct customization, Gender gender) - { - if (gender == customization.Gender) - return false; - - customization.Gender = gender; - FixUpAttributes(customization); - - return true; - } - - private static bool DrawColorPicker(string label, string tooltip, CustomizationStruct customization, CustomizationId id, + private bool DrawColorPicker(string label, string tooltip, LazyCustomization customization, CustomizationId id, CustomizationSet set) { var ret = false; var count = set.Count(id); - var current = set.DataByValue(id, customization[id], out var custom); - if (current < 0) - { - PluginLog.Warning($"Read invalid customization value {customization[id]} for {id}."); - current = 0; - custom = set.Data(id, 0); - } + var (current, custom) = GetCurrentCustomization(customization, id, set); var popupName = $"Color Picker##{id}"; - if (ImGui.ColorButton($"{current}##color_{id}", ImGui.ColorConvertU32ToFloat4(custom!.Value.Color))) + if (ImGui.ColorButton($"{current + 1}##color_{id}", ImGui.ColorConvertU32ToFloat4(custom.Color), ImGuiColorEditFlags.None, + _actualIconSize)) ImGui.OpenPopup(popupName); ImGui.SameLine(); - ImGui.SetNextItemWidth(50 + 2 * 22.5f * ImGui.GetIO().FontGlobalScale); - if (ImGui.InputInt($"##text_{id}", ref current, 1) && current != customization[id] && current >= 0 && current < count) + + using (var group = ImGuiRaii.NewGroup()) { - customization[id] = set.Data(id, current).Value; - ret = true; + if (InputInt($"##text_{id}", ref current, 1, count)) + { + customization.Value[id] = set.Data(id, current - 1).Value; + ret = true; + } + + + ImGui.Text(label); + if (tooltip.Any() && ImGui.IsItemHovered()) + ImGui.SetTooltip(tooltip); } - if (ImGui.IsItemHovered()) - ImGui.SetTooltip($"Input Range: [0, {count - 1}]"); - - ImGui.SameLine(); - ImGui.Text(label); - if (tooltip.Any() && ImGui.IsItemHovered()) - ImGui.SetTooltip(tooltip); - if (!DrawColorPickerPopup(popupName, set, id, out var newCustom)) return ret; - customization[id] = newCustom.Value; - ret = true; + customization.Value[id] = newCustom.Value; + ret = true; return ret; } + } - private static bool DrawListSelector(string label, string tooltip, CustomizationStruct customization, CustomizationId id, + internal partial class Interface : IDisposable + { + public const int GPoseActorId = 201; + private const string PluginName = "Glamourer"; + private readonly string _glamourerHeader; + + private readonly IReadOnlyDictionary _stains; + private readonly IReadOnlyDictionary> _equip; + private readonly ActorTable _actors; + private readonly IObjectIdentifier _identifier; + private readonly Dictionary, ComboWithFilter)> _combos; + private readonly IPlayerWatcher _playerWatcher; + private readonly ImGuiScene.TextureWrap? _legacyTattooIcon; + private readonly Dictionary _equipSlotNames; + + private bool _visible = false; + + private Actor? _player; + + private static ImGuiScene.TextureWrap? GetLegacyTattooIcon() + { + using var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("Glamourer.LegacyTattoo.raw"); + if (resource != null) + { + var rawImage = new byte[resource.Length]; + resource.Read(rawImage, 0, (int) resource.Length); + return GlamourerPlugin.PluginInterface.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4); + } + + return null; + } + + private static Dictionary GetEquipSlotNames() + { + var sheet = GlamourerPlugin.PluginInterface.Data.GetExcelSheet(); + var ret = new Dictionary(12) + { + [EquipSlot.MainHand] = sheet.GetRow(738)?.Text.ToString() ?? "Main Hand", + [EquipSlot.OffHand] = sheet.GetRow(739)?.Text.ToString() ?? "Off Hand", + [EquipSlot.Head] = sheet.GetRow(740)?.Text.ToString() ?? "Head", + [EquipSlot.Body] = sheet.GetRow(741)?.Text.ToString() ?? "Body", + [EquipSlot.Hands] = sheet.GetRow(750)?.Text.ToString() ?? "Hands", + [EquipSlot.Legs] = sheet.GetRow(742)?.Text.ToString() ?? "Legs", + [EquipSlot.Feet] = sheet.GetRow(744)?.Text.ToString() ?? "Feet", + [EquipSlot.Ears] = sheet.GetRow(745)?.Text.ToString() ?? "Ears", + [EquipSlot.Neck] = sheet.GetRow(746)?.Text.ToString() ?? "Neck", + [EquipSlot.Wrists] = sheet.GetRow(747)?.Text.ToString() ?? "Wrists", + [EquipSlot.RFinger] = sheet.GetRow(748)?.Text.ToString() ?? "Right Ring", + [EquipSlot.LFinger] = sheet.GetRow(749)?.Text.ToString() ?? "Left Ring", + }; + return ret; + } + + public Interface() + { + _glamourerHeader = GlamourerPlugin.Version.Length > 0 + ? $"{PluginName} v{GlamourerPlugin.Version}###{PluginName}Main" + : $"{PluginName}###{PluginName}Main"; + GlamourerPlugin.PluginInterface.UiBuilder.OnBuildUi += Draw; + GlamourerPlugin.PluginInterface.UiBuilder.OnOpenConfigUi += ToggleVisibility; + + _equipSlotNames = GetEquipSlotNames(); + + _stains = GameData.Stains(GlamourerPlugin.PluginInterface); + _equip = GameData.ItemsBySlot(GlamourerPlugin.PluginInterface); + _identifier = Penumbra.GameData.GameData.GetIdentifier(GlamourerPlugin.PluginInterface); + _actors = GlamourerPlugin.PluginInterface.ClientState.Actors; + _playerWatcher = PlayerWatchFactory.Create(GlamourerPlugin.PluginInterface); + + var stainCombo = CreateDefaultStainCombo(_stains.Values.ToArray()); + + _combos = _equip.ToDictionary(kvp => kvp.Key, kvp => CreateCombos(kvp.Key, kvp.Value, stainCombo)); + _legacyTattooIcon = GetLegacyTattooIcon(); + } + + public void ToggleVisibility(object _, object _2) + => _visible = !_visible; + + public void Dispose() + { + _legacyTattooIcon?.Dispose(); + _playerWatcher?.Dispose(); + GlamourerPlugin.PluginInterface.UiBuilder.OnBuildUi -= Draw; + GlamourerPlugin.PluginInterface.UiBuilder.OnOpenConfigUi -= ToggleVisibility; + } + + private string _currentActorName = ""; + + private SubRace _currentSubRace = SubRace.Midlander; + private Gender _currentGender = Gender.Male; + + private bool DrawListSelector(string label, string tooltip, LazyCustomization customization, CustomizationId id, CustomizationSet set) { - var ret = false; - int current = customization[id]; - var count = set.Count(id); + using var bigGroup = ImGuiRaii.NewGroup(); + var ret = false; + int current = customization.Value[id]; + var count = set.Count(id); - ImGui.SetNextItemWidth(150 * ImGui.GetIO().FontGlobalScale); - if (ImGui.BeginCombo($"##combo_{id}", $"{id} #{current + 1}")) + ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale); + if (ImGui.BeginCombo($"##combo_{id}", $"{set.Option(id)} #{current + 1}")) { for (var i = 0; i < count; ++i) { - if (ImGui.Selectable($"{id} #{i + 1}##combo", i == current) && i != current) + if (ImGui.Selectable($"{set.Option(id)} #{i + 1}##combo", i == current) && i != current) { - customization[id] = (byte) i; - ret = true; + customization.Value[id] = (byte) i; + ret = true; } } @@ -335,11 +435,10 @@ namespace Glamourer.Gui } ImGui.SameLine(); - ImGui.SetNextItemWidth(50 + 2 * 22.5f * ImGui.GetIO().FontGlobalScale); - if (ImGui.InputInt($"##text_{id}", ref current, 1) && current != customization[id] && current >= 0 && current < count) + if (InputInt($"##text_{id}", ref current, 1, count)) { - customization[id] = set.Data(id, current).Value; - ret = true; + customization.Value[id] = set.Data(id, current).Value; + ret = true; } ImGui.SameLine(); @@ -353,53 +452,55 @@ namespace Glamourer.Gui private static readonly Vector4 NoColor = new(1f, 1f, 1f, 1f); private static readonly Vector4 RedColor = new(0.6f, 0.3f, 0.3f, 1f); - private static bool DrawMultiSelector(CustomizationStruct customization, CustomizationSet set) + private bool DrawMultiSelector(LazyCustomization customization, CustomizationSet set) { - var ret = false; - var count = set.Count(CustomizationId.FacialFeaturesTattoos); - using var raii = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2); - for (var i = 0; i < count; ++i) + using var bigGroup = ImGuiRaii.NewGroup(); + var ret = false; + var count = set.Count(CustomizationId.FacialFeaturesTattoos); + using (var raii = ImGuiRaii.NewGroup()) { - var enabled = customization.FacialFeature(i); - var feature = set.FacialFeature(set.Race == Race.Hrothgar ? customization.Hairstyle : customization.Face, i); - var icon = GlamourerPlugin.Customization.GetIcon(feature.IconId); - if (ImGui.ImageButton(icon.ImGuiHandle, FeatureIconSize, Vector2.Zero, Vector2.One, (int) ImGui.GetStyle().FramePadding.X, - Vector4.Zero, - enabled ? NoColor : RedColor)) + for (var i = 0; i < count; ++i) { - ret = true; - customization.FacialFeature(i, !enabled); - } + var enabled = customization.Value.FacialFeature(i); + var feature = set.FacialFeature(set.Race == Race.Hrothgar ? customization.Value.Hairstyle : customization.Value.Face, i); + var icon = i == count - 1 + ? _legacyTattooIcon ?? GlamourerPlugin.Customization.GetIcon(feature.IconId) + : GlamourerPlugin.Customization.GetIcon(feature.IconId); + if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int) ImGui.GetStyle().FramePadding.X, + Vector4.Zero, + enabled ? NoColor : RedColor)) + { + ret = true; + customization.Value.FacialFeature(i, !enabled); + } - if (ImGui.IsItemHovered()) - { - using var tt = ImGuiRaii.NewTooltip(); - ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height)); - } + if (ImGui.IsItemHovered()) + { + using var tt = ImGuiRaii.NewTooltip(); + ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height)); + } - ImGui.SameLine(); + if (i % 4 != 3) + ImGui.SameLine(); + } } - raii.PopStyles(); - raii.Group(); - int value = customization[CustomizationId.FacialFeaturesTattoos]; - ImGui.SetNextItemWidth(50 + 2 * 22.5f * ImGui.GetIO().FontGlobalScale); - if (ImGui.InputInt($"##{CustomizationId.FacialFeaturesTattoos}", ref value, 1) - && value != customization[CustomizationId.FacialFeaturesTattoos] - && value > 0 - && value < 256) + ImGui.SameLine(); + using var group = ImGuiRaii.NewGroup(); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() + 3 * ImGui.GetStyle().ItemSpacing.Y / 2); + int value = customization.Value[CustomizationId.FacialFeaturesTattoos]; + if (InputInt($"##{CustomizationId.FacialFeaturesTattoos}", ref value, 1, 256)) { - customization[CustomizationId.FacialFeaturesTattoos] = (byte) value; - ret = true; + customization.Value[CustomizationId.FacialFeaturesTattoos] = (byte) value; + ret = true; } - ImGui.Text("Facial Features & Tattoos"); - + ImGui.Text(set.Option(CustomizationId.FacialFeaturesTattoos)); return ret; } - private static bool DrawIconPickerPopup(string label, CustomizationSet set, CustomizationId id, out Customization.Customization value) + private bool DrawIconPickerPopup(string label, CustomizationSet set, CustomizationId id, out Customization.Customization value) { value = default; if (!ImGui.BeginPopup(label, ImGuiWindowFlags.AlwaysAutoResize)) @@ -413,7 +514,7 @@ namespace Glamourer.Gui { var custom = set.Data(id, i); var icon = GlamourerPlugin.Customization.GetIcon(custom.IconId); - if (ImGui.ImageButton(icon.ImGuiHandle, FeatureIconSize)) + if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) { value = custom; ret = true; @@ -434,23 +535,24 @@ namespace Glamourer.Gui return ret; } - private static bool DrawIconSelector(string label, string tooltip, CustomizationStruct customization, CustomizationId id, + private bool DrawIconSelector(string label, string tooltip, LazyCustomization customization, CustomizationId id, CustomizationSet set) { - var ret = false; - var count = set.Count(id); + using var bigGroup = ImGuiRaii.NewGroup(); + var ret = false; + var count = set.Count(id); - var current = set.DataByValue(id, customization[id], out var custom); + var current = set.DataByValue(id, customization.Value[id], out var custom); if (current < 0) { - PluginLog.Warning($"Read invalid customization value {customization[id]} for {id}."); + PluginLog.Warning($"Read invalid customization value {customization.Value[id]} for {id}."); current = 0; custom = set.Data(id, 0); } var popupName = $"Style Picker##{id}"; var icon = GlamourerPlugin.Customization.GetIcon(custom!.Value.IconId); - if (ImGui.ImageButton(icon.ImGuiHandle, FeatureIconSize)) + if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) ImGui.OpenPopup(popupName); if (ImGui.IsItemHovered()) @@ -461,21 +563,16 @@ namespace Glamourer.Gui ImGui.SameLine(); using var group = ImGuiRaii.NewGroup(); - ImGui.SetNextItemWidth(50 + 2 * 22.5f * ImGui.GetIO().FontGlobalScale); - var oldIdx = current; - if (ImGui.InputInt($"##text_{id}", ref current, 1) && current != oldIdx && current >= 0 && current < count) + if (InputInt($"##text_{id}", ref current, 1, count)) { - customization[id] = set.Data(id, current).Value; - ret = true; + customization.Value[id] = set.Data(id, current).Value; + ret = true; } - if (ImGui.IsItemHovered()) - ImGui.SetTooltip($"Input Range: [0, {count - 1}]"); - if (DrawIconPickerPopup(popupName, set, id, out var newCustom)) { - customization[id] = newCustom.Value; - ret = true; + customization.Value[id] = newCustom.Value; + ret = true; } ImGui.Text(label); @@ -486,30 +583,28 @@ namespace Glamourer.Gui } - private static bool DrawPercentageSelector(string label, string tooltip, CustomizationStruct customization, CustomizationId id, + private bool DrawPercentageSelector(string label, string tooltip, LazyCustomization customization, CustomizationId id, CustomizationSet set) { - var ret = false; - int value = customization[id]; - var count = set.Count(id); - ImGui.SetNextItemWidth(150 * ImGui.GetIO().FontGlobalScale); - if (ImGui.SliderInt($"##slider_{id}", ref value, 0, count - 1, "") && value != customization[id]) + using var bigGroup = ImGuiRaii.NewGroup(); + var ret = false; + int value = customization.Value[id]; + var count = set.Count(id); + ImGui.SetNextItemWidth(_percentageSize * ImGui.GetIO().FontGlobalScale); + if (ImGui.SliderInt($"##slider_{id}", ref value, 0, count - 1, "") && value != customization.Value[id]) { - customization[id] = (byte) value; - ret = true; + customization.Value[id] = (byte) value; + ret = true; } ImGui.SameLine(); - ImGui.SetNextItemWidth(50 + 2 * 22.5f * ImGui.GetIO().FontGlobalScale); - if (ImGui.InputInt($"##input_{id}", ref value, 1) && value != customization[id] && value >= 0 && value < count) + --value; + if (InputInt($"##input_{id}", ref value, 0, count - 1)) { - customization[id] = (byte) value; - ret = true; + customization.Value[id] = (byte) (value + 1); + ret = true; } - if (ImGui.IsItemHovered()) - ImGui.SetTooltip($"Input Range: [0, {count - 1}]"); - ImGui.SameLine(); ImGui.Text(label); if (tooltip.Any() && ImGui.IsItemHovered()) @@ -518,103 +613,245 @@ namespace Glamourer.Gui return ret; } - private bool DrawStuff() + private string ClanName(SubRace race, Gender gender) { - var ret = false; - var x = new CustomizationStruct(_player!.Address + 0x1898); - _currentSubRace = x.Clan; - ImGui.SetNextItemWidth(150 * ImGui.GetIO().FontGlobalScale); - if (ImGui.BeginCombo("SubRace", SubRaceNames[(int) _currentSubRace - 1])) - { - for (var i = 0; i < SubRaceNames.Length; ++i) + if (gender == Gender.Female) + return race switch { - if (ImGui.Selectable(SubRaceNames[i], (int) _currentSubRace == i + 1)) + SubRace.Midlander => GlamourerPlugin.Customization.GetName(CustomName.MidlanderM), + SubRace.Highlander => GlamourerPlugin.Customization.GetName(CustomName.HighlanderM), + SubRace.Wildwood => GlamourerPlugin.Customization.GetName(CustomName.WildwoodM), + SubRace.Duskwight => GlamourerPlugin.Customization.GetName(CustomName.DuskwightM), + SubRace.Plainsfolk => GlamourerPlugin.Customization.GetName(CustomName.PlainsfolkM), + SubRace.Dunesfolk => GlamourerPlugin.Customization.GetName(CustomName.DunesfolkM), + SubRace.SeekerOfTheSun => GlamourerPlugin.Customization.GetName(CustomName.SeekerOfTheSunM), + SubRace.KeeperOfTheMoon => GlamourerPlugin.Customization.GetName(CustomName.KeeperOfTheMoonM), + SubRace.Seawolf => GlamourerPlugin.Customization.GetName(CustomName.SeawolfM), + SubRace.Hellsguard => GlamourerPlugin.Customization.GetName(CustomName.HellsguardM), + SubRace.Raen => GlamourerPlugin.Customization.GetName(CustomName.RaenM), + SubRace.Xaela => GlamourerPlugin.Customization.GetName(CustomName.XaelaM), + SubRace.Helion => GlamourerPlugin.Customization.GetName(CustomName.HelionM), + SubRace.Lost => GlamourerPlugin.Customization.GetName(CustomName.LostM), + SubRace.Rava => GlamourerPlugin.Customization.GetName(CustomName.RavaF), + SubRace.Veena => GlamourerPlugin.Customization.GetName(CustomName.VeenaF), + _ => throw new ArgumentOutOfRangeException(nameof(race), race, null), + }; + + return race switch + { + SubRace.Midlander => GlamourerPlugin.Customization.GetName(CustomName.MidlanderF), + SubRace.Highlander => GlamourerPlugin.Customization.GetName(CustomName.HighlanderF), + SubRace.Wildwood => GlamourerPlugin.Customization.GetName(CustomName.WildwoodF), + SubRace.Duskwight => GlamourerPlugin.Customization.GetName(CustomName.DuskwightF), + SubRace.Plainsfolk => GlamourerPlugin.Customization.GetName(CustomName.PlainsfolkF), + SubRace.Dunesfolk => GlamourerPlugin.Customization.GetName(CustomName.DunesfolkF), + SubRace.SeekerOfTheSun => GlamourerPlugin.Customization.GetName(CustomName.SeekerOfTheSunF), + SubRace.KeeperOfTheMoon => GlamourerPlugin.Customization.GetName(CustomName.KeeperOfTheMoonF), + SubRace.Seawolf => GlamourerPlugin.Customization.GetName(CustomName.SeawolfF), + SubRace.Hellsguard => GlamourerPlugin.Customization.GetName(CustomName.HellsguardF), + SubRace.Raen => GlamourerPlugin.Customization.GetName(CustomName.RaenF), + SubRace.Xaela => GlamourerPlugin.Customization.GetName(CustomName.XaelaF), + SubRace.Helion => GlamourerPlugin.Customization.GetName(CustomName.HelionM), + SubRace.Lost => GlamourerPlugin.Customization.GetName(CustomName.LostM), + SubRace.Rava => GlamourerPlugin.Customization.GetName(CustomName.RavaF), + SubRace.Veena => GlamourerPlugin.Customization.GetName(CustomName.VeenaF), + _ => throw new ArgumentOutOfRangeException(nameof(race), race, null), + }; + } + + private bool DrawRaceSelector(LazyCustomization customization) + { + using var group = ImGuiRaii.NewGroup(); + var ret = false; + _currentSubRace = customization.Value.Clan; + ImGui.SetNextItemWidth(_raceSelectorWidth); + if (ImGui.BeginCombo("##subRaceCombo", ClanName(_currentSubRace, customization.Value.Gender))) + { + for (var i = 0; i < (int) SubRace.Veena; ++i) + { + if (ImGui.Selectable(ClanName((SubRace) i + 1, customization.Value.Gender), (int) _currentSubRace == i + 1)) { _currentSubRace = (SubRace) i + 1; - ret |= ChangeRace(x, _currentSubRace); + ret |= ChangeRace(customization, _currentSubRace); } } ImGui.EndCombo(); } - _currentGender = x.Gender; - ImGui.SetNextItemWidth(150 * ImGui.GetIO().FontGlobalScale); - if (ImGui.BeginCombo("Gender", _currentGender.ToName())) + ImGui.Text( + $"{GlamourerPlugin.Customization.GetName(CustomName.Gender)} & {GlamourerPlugin.Customization.GetName(CustomName.Clan)}"); + + return ret; + } + + private bool DrawGenderSelector(LazyCustomization customization) + { + var ret = false; + ImGui.PushFont(UiBuilder.IconFont); + var icon = _currentGender == Gender.Male ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus; + var restricted = false; + if (customization.Value.Race == Race.Viera) { - if (_currentSubRace.ToRace() != Race.Viera - && ImGui.Selectable(Gender.Male.ToName(), _currentGender == Gender.Male) - && _currentGender != Gender.Male) - { - _currentGender = Gender.Male; - ret = ChangeGender(x, _currentGender); - } - - if (_currentSubRace.ToRace() != Race.Hrothgar - && ImGui.Selectable(Gender.Female.ToName(), _currentGender == Gender.Female) - && _currentGender != Gender.Female) - { - _currentGender = Gender.Female; - ret = ChangeGender(x, _currentGender); - } - - ImGui.EndCombo(); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.25f); + icon = FontAwesomeIcon.VenusDouble; + restricted = true; } + else if (customization.Value.Race == Race.Hrothgar) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.25f); + icon = FontAwesomeIcon.MarsDouble; + restricted = true; + } + + if (ImGui.Button(icon.ToIconString(), _actualIconSize) && !restricted) + { + _currentGender = _currentGender == Gender.Male ? Gender.Female : Gender.Male; + ret = ChangeGender(customization, _currentGender); + } + + if (restricted) + ImGui.PopStyleVar(); + ImGui.PopFont(); + return ret; + } + + private bool DrawPicker(CustomizationSet set, CustomizationId id, LazyCustomization customization) + { + if (!set.IsAvailable(id)) + return false; + + switch (set.Type(id)) + { + case CharaMakeParams.MenuType.ColorPicker: return DrawColorPicker(set.OptionName[(int) id], "", customization, id, set); + case CharaMakeParams.MenuType.ListSelector: return DrawListSelector(set.OptionName[(int) id], "", customization, id, set); + case CharaMakeParams.MenuType.IconSelector: return DrawIconSelector(set.OptionName[(int) id], "", customization, id, set); + case CharaMakeParams.MenuType.MultiIconSelector: return DrawMultiSelector(customization, set); + case CharaMakeParams.MenuType.Percentage: return DrawPercentageSelector(set.OptionName[(int) id], "", customization, id, set); + } + + return false; + } + + private static readonly CustomizationId[] AllCustomizations = (CustomizationId[]) Enum.GetValues(typeof(CustomizationId)); + + private bool DrawStuff() + { + var x = new LazyCustomization(_player!.Address); + _currentSubRace = x.Value.Clan; + _currentGender = x.Value.Gender; + var ret = DrawGenderSelector(x); + ImGui.SameLine(); + ret |= DrawRaceSelector(x); var set = GlamourerPlugin.Customization.GetList(_currentSubRace, _currentGender); - foreach (CustomizationId customizationId in Enum.GetValues(typeof(CustomizationId))) - { - if (!set.IsAvailable(customizationId)) - continue; + foreach (var id in AllCustomizations.Where(c => set.Type(c) == CharaMakeParams.MenuType.Percentage)) + ret |= DrawPicker(set, id, x); - switch (customizationId.ToType(_currentSubRace.ToRace() == Race.Hrothgar)) + var odd = true; + foreach (var id in AllCustomizations.Where((c, i) => set.Type(c) == CharaMakeParams.MenuType.IconSelector)) + { + ret |= DrawPicker(set, id, x); + if (odd) + ImGui.SameLine(); + odd = !odd; + } + + if (!odd) + ImGui.NewLine(); + + ret |= DrawPicker(set, CustomizationId.FacialFeaturesTattoos, x); + + foreach (var id in AllCustomizations.Where(c => set.Type(c) == CharaMakeParams.MenuType.ListSelector)) + ret |= DrawPicker(set, id, x); + + odd = true; + foreach (var id in AllCustomizations.Where(c => set.Type(c) == CharaMakeParams.MenuType.ColorPicker)) + { + ret |= DrawPicker(set, id, x); + if (odd) + ImGui.SameLine(); + odd = !odd; + } + + if (!odd) + ImGui.NewLine(); + + var tmp = x.Value.HighlightsOn; + if (ImGui.Checkbox(set.Option(CustomizationId.HighlightsOnFlag), ref tmp) && tmp != x.Value.HighlightsOn) + { + x.Value.HighlightsOn = tmp; + ret = true; + } + + var xPos = _inputIntSize + _actualIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X; + ImGui.SameLine(xPos); + tmp = x.Value.FacePaintReversed; + if (ImGui.Checkbox($"{GlamourerPlugin.Customization.GetName(CustomName.Reverse)} {set.Option(CustomizationId.FacePaint)}", ref tmp) + && tmp != x.Value.FacePaintReversed) + { + x.Value.FacePaintReversed = tmp; + ret = true; + } + + tmp = x.Value.SmallIris; + if (ImGui.Checkbox($"{GlamourerPlugin.Customization.GetName(CustomName.IrisSmall)} {set.Option(CustomizationId.EyeColorL)}", + ref tmp) + && tmp != x.Value.SmallIris) + { + x.Value.SmallIris = tmp; + ret = true; + } + + if (x.Value.Race != Race.Hrothgar) + { + tmp = x.Value.Lipstick; + ImGui.SameLine(xPos); + if (ImGui.Checkbox(set.Option(CustomizationId.LipColor), ref tmp) && tmp != x.Value.Lipstick) { - case CharaMakeParams.MenuType.ColorPicker: - ret |= DrawColorPicker(customizationId.ToString(), "", x, - customizationId, set); - break; - case CharaMakeParams.MenuType.ListSelector: - ret |= DrawListSelector(customizationId.ToString(), "", x, - customizationId, set); - break; - case CharaMakeParams.MenuType.IconSelector: - ret |= DrawIconSelector(customizationId.ToString(), "", x, customizationId, set); - break; - case CharaMakeParams.MenuType.MultiIconSelector: - ret |= DrawMultiSelector(x, set); - break; - case CharaMakeParams.MenuType.Percentage: - ret |= DrawPercentageSelector(customizationId.ToString(), "", x, customizationId, set); - break; + x.Value.Lipstick = tmp; + ret = true; } } return ret; } + private void Draw() { - ImGui.SetNextWindowSizeConstraints(Vector2.One * 600, Vector2.One * 5000); + ImGui.SetNextWindowSizeConstraints(Vector2.One * 450 * ImGui.GetIO().FontGlobalScale, + Vector2.One * 5000 * ImGui.GetIO().FontGlobalScale); if (!_visible || !ImGui.Begin(_glamourerHeader, ref _visible)) return; try { - if (ImGui.BeginCombo("Actor", _currentActorName)) + var inCombo = ImGui.BeginCombo("Actor", _currentActorName); + var idx = 0; + _player = null; + foreach (var actor in _actors.Where(a => a.ObjectKind == ObjectKind.Player)) { - var idx = 0; - foreach (var actor in GlamourerPlugin.PluginInterface.ClientState.Actors.Where(a => a.ObjectKind == ObjectKind.Player)) - { - if (ImGui.Selectable($"{actor.Name}##{idx++}")) - _currentActorName = actor.Name; - } + if (_currentActorName == actor.Name) + _player = actor; - ImGui.EndCombo(); + if (inCombo && ImGui.Selectable($"{actor.Name}##{idx++}")) + _currentActorName = actor.Name; } - _player = _actors[GPoseActorId] ?? _actors[0]; + if (_player == null) + { + _player = _actors[0]; + _currentActorName = _player?.Name ?? string.Empty; + } + + if (inCombo) + ImGui.EndCombo(); + + if (_player == _actors[0] && _actors[GPoseActorId] != null) + _player = _actors[GPoseActorId]; if (_player == null || !GlamourerPlugin.PluginInterface.ClientState.Condition.Any()) { ImGui.TextColored(new Vector4(0.4f, 0.1f, 0.1f, 1f), @@ -623,22 +860,33 @@ namespace Glamourer.Gui else { var equip = new ActorEquipment(_player); - + _iconSize = Vector2.One * ImGui.GetTextLineHeightWithSpacing() * 2; + _actualIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding; + _comboSelectorSize = 4 * _actualIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X; + _percentageSize = _comboSelectorSize; + _inputIntSize = 2 * _actualIconSize.X + ImGui.GetStyle().ItemSpacing.X; + _raceSelectorWidth = _inputIntSize + _percentageSize - _actualIconSize.X; + _itemComboWidth = 6 * _actualIconSize.X + 4 * ImGui.GetStyle().ItemSpacing.X - ColorButtonWidth + 1; var changes = false; - changes |= DrawWeapon(EquipSlot.MainHand, equip.MainHand); - changes |= DrawWeapon(EquipSlot.OffHand, equip.OffHand); - changes |= DrawEquip(EquipSlot.Head, equip.Head); - changes |= DrawEquip(EquipSlot.Body, equip.Body); - changes |= DrawEquip(EquipSlot.Hands, equip.Hands); - changes |= DrawEquip(EquipSlot.Legs, equip.Legs); - changes |= DrawEquip(EquipSlot.Feet, equip.Feet); - changes |= DrawEquip(EquipSlot.Ears, equip.Ears); - changes |= DrawEquip(EquipSlot.Neck, equip.Neck); - changes |= DrawEquip(EquipSlot.Wrists, equip.Wrists); - changes |= DrawEquip(EquipSlot.RFinger, equip.RFinger); - changes |= DrawEquip(EquipSlot.LFinger, equip.LFinger); - changes |= DrawStuff(); + if (ImGui.CollapsingHeader("Character Equipment")) + { + changes |= DrawWeapon(EquipSlot.MainHand, equip.MainHand); + changes |= DrawWeapon(EquipSlot.OffHand, equip.OffHand); + changes |= DrawEquip(EquipSlot.Head, equip.Head); + changes |= DrawEquip(EquipSlot.Body, equip.Body); + changes |= DrawEquip(EquipSlot.Hands, equip.Hands); + changes |= DrawEquip(EquipSlot.Legs, equip.Legs); + changes |= DrawEquip(EquipSlot.Feet, equip.Feet); + changes |= DrawEquip(EquipSlot.Ears, equip.Ears); + changes |= DrawEquip(EquipSlot.Neck, equip.Neck); + changes |= DrawEquip(EquipSlot.Wrists, equip.Wrists); + changes |= DrawEquip(EquipSlot.RFinger, equip.RFinger); + changes |= DrawEquip(EquipSlot.LFinger, equip.LFinger); + } + + if (ImGui.CollapsingHeader("Character Customization")) + changes |= DrawStuff(); if (changes) UpdateActors(_player); diff --git a/Glamourer/LegacyTattoo.raw b/Glamourer/LegacyTattoo.raw new file mode 100644 index 0000000..c6a347b Binary files /dev/null and b/Glamourer/LegacyTattoo.raw differ diff --git a/Glamourer/Main.cs b/Glamourer/Main.cs index d7e9fdb..de7ca13 100644 --- a/Glamourer/Main.cs +++ b/Glamourer/Main.cs @@ -1,16 +1,28 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Runtime.CompilerServices; using Dalamud.Game.Command; using Dalamud.Plugin; using Glamourer.Customization; using Glamourer.Gui; using ImGuiNET; using Penumbra.Api; +using Penumbra.GameData.Structs; using CommandManager = Glamourer.Managers.CommandManager; namespace Glamourer { + public class CharacterSave + { + public string Name { get; set; } = string.Empty; + public ActorCustomization Customizations { get; private set; } + public ActorEquipment Equipment { get; private set; } = null!; + + public ActorEquipMask WriteEquipment { get; set; } = ActorEquipMask.None; + public bool WriteCustomizations { get; set; } = false; + } + internal class Glamourer { private readonly DalamudPluginInterface _pluginInterface;