From 052a2e7719f7c9aa608b9103ae5d50bc2854871e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Aug 2021 23:49:15 +0200 Subject: [PATCH] So. Much. Stuff. Glamourer now works with all player actors, can change all customization, gear and stains. Also has a cool Legacy Tattoo icon. --- .../Customization/ActorCustomization.cs | 264 ++++++ .../Customization/CustomName.cs | 46 + .../Customization/CustomizationId.cs | 46 +- .../Customization/CustomizationManager.cs | 3 + .../Customization/CustomizationOptions.cs | 105 ++- .../Customization/CustomizationSet.cs | 12 +- .../Customization/CustomizationStruct.cs | 247 ----- .../Customization/ICustomizationManager.cs | 1 + Glamourer/CharacterFlag.cs | 68 ++ Glamourer/Glamourer.csproj | 8 + Glamourer/Gui/ComboWithFilter.cs | 7 +- Glamourer/Gui/Interface.cs | 878 +++++++++++------- Glamourer/LegacyTattoo.raw | Bin 0 -> 147456 bytes Glamourer/Main.cs | 12 + 14 files changed, 1120 insertions(+), 577 deletions(-) create mode 100644 Glamourer.GameData/Customization/ActorCustomization.cs create mode 100644 Glamourer.GameData/Customization/CustomName.cs delete mode 100644 Glamourer.GameData/Customization/CustomizationStruct.cs create mode 100644 Glamourer/CharacterFlag.cs create mode 100644 Glamourer/LegacyTattoo.raw 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 0000000000000000000000000000000000000000..c6a347badf9e7dc3a51441e3d27baa70ebb199ae GIT binary patch literal 147456 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≪WJ3q})6tjXUf1Av@*U+q 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 B literal 0 HcmV?d00001 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;