diff --git a/Glamourer.GameData/Customization/CustomizationData.cs b/Glamourer.GameData/Customization/CustomizationData.cs index 7ded7a9..7135d65 100644 --- a/Glamourer.GameData/Customization/CustomizationData.cs +++ b/Glamourer.GameData/Customization/CustomizationData.cs @@ -6,59 +6,59 @@ namespace Glamourer.Customization; public unsafe struct Customize { - private readonly CustomizeData* _data; + public readonly CustomizeData* Data; public Customize(CustomizeData* data) - => _data = data; + => Data = data; public Race Race { - get => (Race)_data->Data[0]; - set => _data->Data[0] = (byte)value; + get => (Race)Data->Data[0]; + set => Data->Data[0] = (byte)value; } // Skip Unknown Gender public Gender Gender { - get => (Gender)(_data->Data[1] + 1); - set => _data->Data[1] = (byte)(value - 1); + get => (Gender)(Data->Data[1] + 1); + set => Data->Data[1] = (byte)(value - 1); } public ref byte BodyType - => ref _data->Data[2]; + => ref Data->Data[2]; public ref byte Height - => ref _data->Data[3]; + => ref Data->Data[3]; public SubRace Clan { - get => (SubRace)_data->Data[4]; - set => _data->Data[4] = (byte)value; + get => (SubRace)Data->Data[4]; + set => Data->Data[4] = (byte)value; } public ref byte Face - => ref _data->Data[5]; + => ref Data->Data[5]; public ref byte Hairstyle - => ref _data->Data[6]; + => ref Data->Data[6]; public bool HighlightsOn { - get => _data->Data[7] >> 7 == 1; - set => _data->Data[7] = (byte)(value ? _data->Data[7] | 0x80 : _data->Data[7] & 0x7F); + get => Data->Data[7] >> 7 == 1; + set => Data->Data[7] = (byte)(value ? Data->Data[7] | 0x80 : Data->Data[7] & 0x7F); } public ref byte SkinColor - => ref _data->Data[8]; + => ref Data->Data[8]; public ref byte EyeColorRight - => ref _data->Data[9]; + => ref Data->Data[9]; public ref byte HairColor - => ref _data->Data[10]; + => ref Data->Data[10]; public ref byte HighlightsColor - => ref _data->Data[11]; + => ref Data->Data[11]; public readonly ref struct FacialFeatureStruct { @@ -84,73 +84,73 @@ public unsafe struct Customize } public FacialFeatureStruct FacialFeatures - => new(_data->Data + 12); + => new(Data->Data + 12); public ref byte TattooColor - => ref _data->Data[13]; + => ref Data->Data[13]; public ref byte Eyebrows - => ref _data->Data[14]; + => ref Data->Data[14]; public ref byte EyeColorLeft - => ref _data->Data[15]; + => ref Data->Data[15]; public byte EyeShape { - get => (byte)(_data->Data[16] & 0x7F); - set => _data->Data[16] = (byte)((value & 0x7F) | (_data->Data[16] & 0x80)); + get => (byte)(Data->Data[16] & 0x7F); + set => Data->Data[16] = (byte)((value & 0x7F) | (Data->Data[16] & 0x80)); } public bool SmallIris { - get => _data->Data[16] >> 7 == 1; - set => _data->Data[16] = (byte)(value ? _data->Data[16] | 0x80 : _data->Data[16] & 0x7F); + get => Data->Data[16] >> 7 == 1; + set => Data->Data[16] = (byte)(value ? Data->Data[16] | 0x80 : Data->Data[16] & 0x7F); } public ref byte Nose - => ref _data->Data[17]; + => ref Data->Data[17]; public ref byte Jaw - => ref _data->Data[18]; + => ref Data->Data[18]; public byte Mouth { - get => (byte)(_data->Data[19] & 0x7F); - set => _data->Data[19] = (byte)((value & 0x7F) | (_data->Data[19] & 0x80)); + get => (byte)(Data->Data[19] & 0x7F); + set => Data->Data[19] = (byte)((value & 0x7F) | (Data->Data[19] & 0x80)); } public bool Lipstick { - get => _data->Data[19] >> 7 == 1; - set => _data->Data[19] = (byte)(value ? _data->Data[19] | 0x80 : _data->Data[19] & 0x7F); + get => Data->Data[19] >> 7 == 1; + set => Data->Data[19] = (byte)(value ? Data->Data[19] | 0x80 : Data->Data[19] & 0x7F); } public ref byte LipColor - => ref _data->Data[20]; + => ref Data->Data[20]; public ref byte MuscleMass - => ref _data->Data[21]; + => ref Data->Data[21]; public ref byte TailShape - => ref _data->Data[22]; + => ref Data->Data[22]; public ref byte BustSize - => ref _data->Data[23]; + => ref Data->Data[23]; public byte FacePaint { - get => (byte)(_data->Data[24] & 0x7F); - set => _data->Data[24] = (byte)((value & 0x7F) | (_data->Data[24] & 0x80)); + get => (byte)(Data->Data[24] & 0x7F); + set => Data->Data[24] = (byte)((value & 0x7F) | (Data->Data[24] & 0x80)); } public bool FacePaintReversed { - get => _data->Data[24] >> 7 == 1; - set => _data->Data[24] = (byte)(value ? _data->Data[24] | 0x80 : _data->Data[24] & 0x7F); + get => Data->Data[24] >> 7 == 1; + set => Data->Data[24] = (byte)(value ? Data->Data[24] | 0x80 : Data->Data[24] & 0x7F); } public ref byte FacePaintColor - => ref _data->Data[25]; + => ref Data->Data[25]; public static readonly CustomizeData Default = GenerateDefault(); public static readonly CustomizeData Empty = new(); @@ -165,12 +165,12 @@ public unsafe struct Customize CustomizationId.Clan => (byte)Clan, CustomizationId.Face => Face, CustomizationId.Hairstyle => Hairstyle, - CustomizationId.HighlightsOnFlag => _data->Data[7], + CustomizationId.HighlightsOnFlag => Data->Data[7], CustomizationId.SkinColor => SkinColor, CustomizationId.EyeColorR => EyeColorRight, CustomizationId.HairColor => HairColor, CustomizationId.HighlightColor => HighlightsColor, - CustomizationId.FacialFeaturesTattoos => _data->Data[12], + CustomizationId.FacialFeaturesTattoos => Data->Data[12], CustomizationId.TattooColor => TattooColor, CustomizationId.Eyebrows => Eyebrows, CustomizationId.EyeColorL => EyeColorLeft, @@ -204,7 +204,7 @@ public unsafe struct Customize case CustomizationId.EyeColorR: EyeColorRight = value; break; case CustomizationId.HairColor: HairColor = value; break; case CustomizationId.HighlightColor: HighlightsColor = value; break; - case CustomizationId.FacialFeaturesTattoos: _data->Data[12] = value; break; + case CustomizationId.FacialFeaturesTattoos: Data->Data[12] = value; break; case CustomizationId.TattooColor: TattooColor = value; break; case CustomizationId.Eyebrows: Eyebrows = value; break; case CustomizationId.EyeColorL: EyeColorLeft = value; break; @@ -224,7 +224,7 @@ public unsafe struct Customize } public bool Equals(Customize other) - => throw new NotImplementedException(); + => CustomizeData.Equals(Data, other.Data); public byte this[CustomizationId id] { @@ -268,5 +268,8 @@ public unsafe struct Customize } public void Load(Customize other) - => _data->Read(other._data); + => Data->Read(other.Data); + + public void Write(IntPtr target) + => Data->Write((void*)target); } diff --git a/Glamourer.GameData/Customization/CustomizationOptions.cs b/Glamourer.GameData/Customization/CustomizationOptions.cs index 053cf73..49980e7 100644 --- a/Glamourer.GameData/Customization/CustomizationOptions.cs +++ b/Glamourer.GameData/Customization/CustomizationOptions.cs @@ -137,8 +137,8 @@ public partial class CustomizationOptions public CustomizationSet GetSet(SubRace race, Gender gender) { var (skin, hair) = GetColors(race, gender); - var row = _listSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; - + var row = _listSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; + var hrothgar = race.ToRace() == Race.Hrothgar; // Create the initial set with all the easily accessible parameters available for anyone. var set = new CustomizationSet(race, gender) { @@ -148,8 +148,8 @@ public partial class CustomizationOptions EyeColors = _eyeColorPicker, HighlightColors = _highlightPicker, TattooColors = _tattooColorPicker, - LipColorsDark = race.ToRace() == Race.Hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark, - LipColorsLight = race.ToRace() == Race.Hrothgar ? Array.Empty() : _lipColorPickerLight, + LipColorsDark = hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark, + LipColorsLight = hrothgar ? Array.Empty() : _lipColorPickerLight, FacePaintColorsDark = _facePaintColorPickerDark, FacePaintColorsLight = _facePaintColorPickerLight, Faces = GetFaces(row), @@ -164,9 +164,9 @@ public partial class CustomizationOptions SetAvailability(set, row); SetFacialFeatures(set, row); + SetHairByFace(set); SetMenuTypes(set, row); SetNames(set, row); - return set; } @@ -218,6 +218,32 @@ public partial class CustomizationOptions .Select((c, i) => new Customization(id, (byte)(light ? 128 + i : 0 + i), c, (ushort)(offset + i))) .ToArray(); + + private void SetHairByFace(CustomizationSet set) + { + if (set.Race != Race.Hrothgar) + { + set.HairByFace = Enumerable.Repeat(set.HairStyles, set.Faces.Count + 1).ToArray(); + return; + } + + var tmp = new IReadOnlyList[set.Faces.Count + 1]; + tmp[0] = set.HairStyles; + + for (var i = 1; i <= set.Faces.Count; ++i) + { + bool Valid(Customization c) + { + var data = _customizeSheet.GetRow(c.CustomizeId)?.Unknown6 ?? 0; + return data == 0 || data == i + set.Faces.Count; + } + + tmp[i] = set.HairStyles.Where(Valid).ToArray(); + } + + set.HairByFace = tmp; + } + private static void SetMenuTypes(CustomizationSet set, CharaMakeParams row) { // Set up the menu types for all customizations. @@ -270,6 +296,7 @@ public partial class CustomizationOptions { var count = set.Faces.Count; var featureDict = new List>(count); + for (var i = 0; i < count; ++i) { var legacyTattoo = new Customization(CustomizationId.FacialFeaturesTattoos, 1 << 7, 137905, (ushort)((i + 1) * 8)); diff --git a/Glamourer.GameData/Customization/CustomizationSet.cs b/Glamourer.GameData/Customization/CustomizationSet.cs index fc5e111..843bce2 100644 --- a/Glamourer.GameData/Customization/CustomizationSet.cs +++ b/Glamourer.GameData/Customization/CustomizationSet.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using Microsoft.VisualBasic; using Penumbra.GameData.Enums; namespace Glamourer.Customization; @@ -52,6 +53,7 @@ public class CustomizationSet public IReadOnlyList OptionName { get; internal set; } = null!; public IReadOnlyList Faces { get; internal init; } = null!; public IReadOnlyList HairStyles { get; internal init; } = null!; + public IReadOnlyList> HairByFace { get; internal set; } = null!; public IReadOnlyList TailEarShapes { get; internal init; } = null!; public IReadOnlyList> FeaturesTattoos { get; internal set; } = null!; public IReadOnlyList FacePaints { get; internal init; } = null!; @@ -74,13 +76,22 @@ public class CustomizationSet => OptionName[(int)id]; public Customization FacialFeature(int faceIdx, int idx) - => FeaturesTattoos[faceIdx - 1][idx]; + { + faceIdx = HrothgarFaceHack((byte) faceIdx) - 1; + if (faceIdx < FeaturesTattoos.Count) + return FeaturesTattoos[HrothgarFaceHack((byte)faceIdx)][idx]; + + return FeaturesTattoos[0][idx]; + } + + private byte HrothgarFaceHack(byte value) + => value is > 4 and < 9 && Clan.ToRace() == Race.Hrothgar ? (byte)(value - 4) : value; public int DataByValue(CustomizationId id, byte value, out Customization? custom) { var type = id.ToType(); custom = null; - if (type == CharaMakeParams.MenuType.Percentage || type == CharaMakeParams.MenuType.ListSelector) + if (type is CharaMakeParams.MenuType.Percentage or CharaMakeParams.MenuType.ListSelector) { if (value < Count(id)) { @@ -91,9 +102,9 @@ public class CustomizationSet return -1; } - int Get(IEnumerable list, ref Customization? output) + int Get(IEnumerable list, byte v, ref Customization? output) { - var (val, idx) = list.Cast().Select((c, i) => (c, i)).FirstOrDefault(c => c.c!.Value.Value == value); + var (val, idx) = list.Cast().Select((c, i) => (c, i)).FirstOrDefault(c => c.c!.Value.Value == v); if (val == null) return -1; @@ -103,27 +114,27 @@ public class CustomizationSet return id switch { - CustomizationId.SkinColor => Get(SkinColors, ref custom), - CustomizationId.EyeColorL => Get(EyeColors, ref custom), - CustomizationId.EyeColorR => Get(EyeColors, ref custom), - CustomizationId.HairColor => Get(HairColors, ref custom), - CustomizationId.HighlightColor => Get(HighlightColors, ref custom), - CustomizationId.TattooColor => Get(TattooColors, ref custom), - CustomizationId.LipColor => Get(LipColorsDark.Concat(LipColorsLight), ref custom), - CustomizationId.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), ref custom), + CustomizationId.SkinColor => Get(SkinColors, value, ref custom), + CustomizationId.EyeColorL => Get(EyeColors, value, ref custom), + CustomizationId.EyeColorR => Get(EyeColors, value, ref custom), + CustomizationId.HairColor => Get(HairColors, value, ref custom), + CustomizationId.HighlightColor => Get(HighlightColors, value, ref custom), + CustomizationId.TattooColor => Get(TattooColors, value, ref custom), + CustomizationId.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, ref custom), + CustomizationId.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, ref custom), - CustomizationId.Face => Get(Faces, ref custom), - CustomizationId.Hairstyle => Get(HairStyles, ref custom), - CustomizationId.TailEarShape => Get(TailEarShapes, ref custom), - CustomizationId.FacePaint => Get(FacePaints, ref custom), - CustomizationId.FacialFeaturesTattoos => Get(FeaturesTattoos[0], ref custom), + CustomizationId.Face => Get(Faces, HrothgarFaceHack(value), ref custom), + CustomizationId.Hairstyle => Get(HairStyles, value, ref custom), + CustomizationId.TailEarShape => Get(TailEarShapes, value, ref custom), + CustomizationId.FacePaint => Get(FacePaints, value, ref custom), + CustomizationId.FacialFeaturesTattoos => Get(FeaturesTattoos[0], value, ref custom), _ => throw new ArgumentOutOfRangeException(nameof(id), id, null), }; } - public Customization Data(CustomizationId id, int idx) + public Customization Data(CustomizationId id, int idx, byte face = 0) { - if (idx > Count(id)) + if (idx > Count(id, face = HrothgarFaceHack(face))) throw new IndexOutOfRangeException(); switch (id.ToType()) @@ -135,7 +146,7 @@ public class CustomizationSet return id switch { CustomizationId.Face => Faces[idx], - CustomizationId.Hairstyle => HairStyles[idx], + CustomizationId.Hairstyle => face < HairByFace.Count ? HairByFace[face][idx] : HairStyles[idx], CustomizationId.TailEarShape => TailEarShapes[idx], CustomizationId.FacePaint => FacePaints[idx], CustomizationId.FacialFeaturesTattoos => FeaturesTattoos[0][idx], @@ -162,10 +173,13 @@ public class CustomizationSet ret[(int)CustomizationId.EyeColorL] = CustomizationId.EyeColorR; ret[(int)CustomizationId.EyeColorR] = CustomizationId.TattooColor; - return ret.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray()); + var dict = ret.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray()); + foreach (var type in Enum.GetValues()) + dict.TryAdd(type, Array.Empty()); + return dict; } - public int Count(CustomizationId id) + public int Count(CustomizationId id, byte face = 0) { if (!IsAvailable(id)) return 0; @@ -176,7 +190,7 @@ public class CustomizationSet return id switch { CustomizationId.Face => Faces.Count, - CustomizationId.Hairstyle => HairStyles.Count, + CustomizationId.Hairstyle => (face = HrothgarFaceHack(face)) < HairByFace.Count ? HairByFace[face].Count : 0, CustomizationId.HighlightsOnFlag => 2, CustomizationId.SkinColor => SkinColors.Count, CustomizationId.EyeColorR => EyeColors.Count, diff --git a/Glamourer.GameData/ModelData.cs b/Glamourer.GameData/ModelData.cs index 16420e5..7d8f9a1 100644 --- a/Glamourer.GameData/ModelData.cs +++ b/Glamourer.GameData/ModelData.cs @@ -1,12 +1,12 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using Dalamud.Data; -using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Utility; -using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Lumina.Excel.GeneratedSheets; +using OtterGui.Raii; using Companion = Lumina.Excel.GeneratedSheets.Companion; namespace Glamourer; @@ -26,16 +26,37 @@ public class ModelData FirstName = $"{name} #{model.RowId:D4}"; AllNames = $"#{model.RowId:D4}\n{name}"; } + + public uint Id + => Model.RowId; } - private readonly SortedList _models; + private readonly SortedList _models; + private readonly Dictionary _modelByData; public IReadOnlyDictionary Models => _models; + public unsafe ulong KeyFromCharacterBase(CharacterBase* drawObject) + { + var type = (*(delegate* unmanaged**)drawObject)[50](drawObject); + var unk = (ulong)*((byte*)drawObject + 0x8E8) << 8; + return type switch + { + 1 => type | unk, + 2 => type | unk | ((ulong)*(ushort*)((byte*)drawObject + 0x908) << 16), + 3 => type | unk | ((ulong)*(ushort*)((byte*)drawObject + 0x8F0) << 16) | ((ulong)**(ushort**)((byte*)drawObject + 0x910) << 32) | ((ulong)**(ushort**)((byte*)drawObject + 0x910) << 40), + _ => 0u, + }; + } + + public unsafe bool FromCharacterBase(CharacterBase* drawObject, out Data data) + => _modelByData.TryGetValue(KeyFromCharacterBase(drawObject), out data); + + public ModelData(DataManager dataManager) { - var modelSheet = dataManager.GetExcelSheet(); + var modelSheet = dataManager.GetExcelSheet()!; _models = new SortedList(NpcNames.ModelCharas.Count); @@ -71,5 +92,20 @@ public class ModelData UpdateData(model, name); } } + + _modelByData = new Dictionary((int)modelSheet.RowCount); + foreach (var mdl in modelSheet) + { + var unk5 = (ulong)mdl.Unknown5 << 8; + var key = mdl.Type switch + { + 1 => mdl.Type | unk5, + 2 => mdl.Type | unk5 | ((ulong)mdl.Model << 16), + 3 => mdl.Type | unk5 | ((ulong)mdl.Model << 16) | ((ulong)mdl.Base << 32) | ((ulong)mdl.Base << 40), + _ => 0u, + }; + if (key != 0) + _modelByData.TryAdd(key, _models.TryGetValue(mdl.RowId, out var d) ? d : new Data(mdl, string.Empty)); + } } } diff --git a/Glamourer.GameData/RestrictedGear.cs b/Glamourer.GameData/RestrictedGear.cs index b53411e..b67570e 100644 --- a/Glamourer.GameData/RestrictedGear.cs +++ b/Glamourer.GameData/RestrictedGear.cs @@ -5,7 +5,6 @@ using Dalamud.Logging; using Dalamud.Utility; using Lumina.Excel; using Lumina.Excel.GeneratedSheets; -using Newtonsoft.Json.Linq; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Race = Penumbra.GameData.Enums.Race; @@ -356,16 +355,6 @@ public class RestrictedGear AddItem(37474, 37479); // Common Makai Harbinger's Fingerless Gloves <-> Common Makai Harrower's Fingerless Gloves AddItem(37475, 37480); // Common Makai Harbinger's Leggings <-> Common Makai Harrower's Quartertights AddItem(37476, 37481); // Common Makai Harbinger's Boots <-> Common Makai Harrower's Longboots - AddItem(23003, 23008); // Mun'gaek Hat <-> Eastern Socialite's Hat - AddItem(23004, 23009); // Mun'gaek Uibok <-> Eastern Socialite's Cheongsam - AddItem(23005, 23010); // Mun'gaek Cuffs <-> Eastern Socialite's Gloves - AddItem(23006, 23011); // Mun'gaek Trousers <-> Eastern Socialite's Skirt - AddItem(23007, 23012); // Mun'gaek Boots <-> Eastern Socialite's Boots - AddItem(24148, 24153); // Far Eastern Officer's Hat <-> Far Eastern Maiden's Hat - AddItem(24149, 24154); // Far Eastern Officer's Robe <-> Far Eastern Maiden's Tunic - AddItem(24150, 24155); // Far Eastern Officer's Armband <-> Far Eastern Maiden's Armband - AddItem(24151, 24156); // Far Eastern Officer's Bottoms <-> Far Eastern Maiden's Bottoms - AddItem(24152, 24157); // Far Eastern Officer's Boots <-> Far Eastern Maiden's Boots AddItem(13323, 13322); // Scion Thief's Tunic <-> Scion Conjurer's Dalmatica AddItem(13693, 10034, true, false); // Scion Thief's Halfgloves -> The Emperor's New Gloves AddItem(13694, 13691); // Scion Thief's Gaskins <-> Scion Conjurer's Chausses diff --git a/Glamourer.GameData/Structs/CharacterEquipMask.cs b/Glamourer.GameData/Structs/CharacterEquipMask.cs deleted file mode 100644 index 8a38db3..0000000 --- a/Glamourer.GameData/Structs/CharacterEquipMask.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using Penumbra.GameData.Enums; - -namespace Glamourer.Structs; - - -// Turn EquipSlot into a bitfield flag enum. -[Flags] -public enum CharacterEquipMask : ushort -{ - None = 0, - MainHand = 0b000000000001, - OffHand = 0b000000000010, - Head = 0b000000000100, - Body = 0b000000001000, - Hands = 0b000000010000, - Legs = 0b000000100000, - Feet = 0b000001000000, - Ears = 0b000010000000, - Neck = 0b000100000000, - Wrists = 0b001000000000, - RFinger = 0b010000000000, - LFinger = 0b100000000000, - All = 0b111111111111, -} - -public static class CharacterEquipMaskExtensions -{ - public static bool Fits(this CharacterEquipMask mask, EquipSlot slot) - => slot switch - { - EquipSlot.Unknown => false, - EquipSlot.Head => mask.HasFlag(CharacterEquipMask.Head), - EquipSlot.Body => mask.HasFlag(CharacterEquipMask.Body), - EquipSlot.Hands => mask.HasFlag(CharacterEquipMask.Hands), - EquipSlot.Legs => mask.HasFlag(CharacterEquipMask.Legs), - EquipSlot.Feet => mask.HasFlag(CharacterEquipMask.Feet), - EquipSlot.Ears => mask.HasFlag(CharacterEquipMask.Ears), - EquipSlot.Neck => mask.HasFlag(CharacterEquipMask.Neck), - EquipSlot.Wrists => mask.HasFlag(CharacterEquipMask.Wrists), - EquipSlot.RFinger => mask.HasFlag(CharacterEquipMask.RFinger), - EquipSlot.LFinger => mask.HasFlag(CharacterEquipMask.LFinger), - _ => false, - }; -} diff --git a/Glamourer.GameData/Structs/Item.cs b/Glamourer.GameData/Structs/Item.cs index 0b6ef42..22fe0bb 100644 --- a/Glamourer.GameData/Structs/Item.cs +++ b/Glamourer.GameData/Structs/Item.cs @@ -21,6 +21,9 @@ public readonly struct Item public bool HasSubModel => Base.ModelSub != 0; + public bool IsBothHand + => (EquipSlot)Base.EquipSlotCategory.Row == EquipSlot.BothHand; + // Create a new item from its sheet list with the given name and either the inferred equip slot or the given one. public Item(Lumina.Excel.GeneratedSheets.Item item, string name, EquipSlot slot = EquipSlot.Unknown) { diff --git a/Glamourer/Actor.cs b/Glamourer/Actor.cs deleted file mode 100644 index 5ad4364..0000000 --- a/Glamourer/Actor.cs +++ /dev/null @@ -1,241 +0,0 @@ -using System; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.ClientState.Objects.Types; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Penumbra.GameData.ByteString; - -namespace Glamourer; - -public unsafe struct Actor : IEquatable -{ - public interface IIdentifier : IEquatable - { - Utf8String Name { get; } - - public IIdentifier CreatePermanent(); - } - - public class InvalidIdentifier : IIdentifier - { - public Utf8String Name - => Utf8String.Empty; - - public bool Equals(IIdentifier? other) - => false; - - public override int GetHashCode() - => 0; - - public override string ToString() - => "Invalid"; - - public IIdentifier CreatePermanent() - => this; - } - - public class PlayerIdentifier : IIdentifier - { - public Utf8String Name { get; } - public readonly ushort HomeWorld; - - public PlayerIdentifier(Utf8String name, ushort homeWorld) - { - Name = name; - HomeWorld = homeWorld; - } - - public bool Equals(IIdentifier? other) - => other is PlayerIdentifier p && p.HomeWorld == HomeWorld && p.Name.Equals(Name); - - public override int GetHashCode() - => HashCode.Combine(Name.Crc32, HomeWorld); - - public override string ToString() - => $"{Name} ({HomeWorld})"; - - public IIdentifier CreatePermanent() - => new PlayerIdentifier(Name.Clone(), HomeWorld); - } - - public class OwnedIdentifier : IIdentifier - { - public Utf8String Name { get; } - public readonly Utf8String OwnerName; - public readonly uint DataId; - public readonly ushort OwnerHomeWorld; - public readonly ObjectKind Kind; - - public OwnedIdentifier(Utf8String name, Utf8String ownerName, ushort ownerHomeWorld, uint dataId, ObjectKind kind) - { - Name = name; - OwnerName = ownerName; - OwnerHomeWorld = ownerHomeWorld; - DataId = dataId; - Kind = kind; - } - - public bool Equals(IIdentifier? other) - => other is OwnedIdentifier p - && p.DataId == DataId - && p.OwnerHomeWorld == OwnerHomeWorld - && p.Kind == Kind - && p.OwnerName.Equals(OwnerName); - - public override int GetHashCode() - => HashCode.Combine(OwnerName.Crc32, OwnerHomeWorld, DataId, Kind); - - public override string ToString() - => $"{OwnerName}s {Name}"; - - public IIdentifier CreatePermanent() - => new OwnedIdentifier(Name.Clone(), OwnerName.Clone(), OwnerHomeWorld, DataId, Kind); - } - - public class NpcIdentifier : IIdentifier - { - public Utf8String Name { get; } - public readonly uint DataId; - public readonly ushort ObjectIndex; - - public NpcIdentifier(Utf8String actorName, ushort objectIndex = ushort.MaxValue, uint dataId = uint.MaxValue) - { - Name = actorName; - ObjectIndex = objectIndex; - DataId = dataId; - } - - public bool Equals(IIdentifier? other) - => other is NpcIdentifier p - && p.Name.Equals(Name) - && (p.DataId == uint.MaxValue || DataId == uint.MaxValue || p.DataId == DataId) - && (p.ObjectIndex == ushort.MaxValue || ObjectIndex == ushort.MaxValue || p.ObjectIndex == ObjectIndex); - - public override int GetHashCode() - => Name.Crc32; - - public override string ToString() - => DataId == uint.MaxValue ? ObjectIndex == ushort.MaxValue ? Name.ToString() : $"{Name} at {ObjectIndex}" : - ObjectIndex == ushort.MaxValue ? $"{Name} ({DataId})" : $"{Name} ({DataId}) at {ObjectIndex}"; - - public IIdentifier CreatePermanent() - => new NpcIdentifier(Name.Clone(), ObjectIndex, DataId); - } - - - public static readonly Actor Null = new() { Pointer = null }; - - public FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Pointer; - - public IntPtr Address - => (IntPtr)Pointer; - - public static implicit operator Actor(IntPtr? pointer) - => new() { Pointer = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)pointer.GetValueOrDefault(IntPtr.Zero) }; - - public static implicit operator IntPtr(Actor actor) - => actor.Pointer == null ? IntPtr.Zero : (IntPtr)actor.Pointer; - - public IIdentifier GetIdentifier() - => CreateIdentifier(this); - - public Character? Character - => Pointer == null ? null : Dalamud.Objects[Pointer->GameObject.ObjectIndex] as Character; - - public bool IsAvailable - => Pointer->GameObject.GetIsTargetable(); - - public bool IsHuman - => Pointer != null && Pointer->ModelCharaId == 0; - - public ref int ModelId - => ref Pointer->ModelCharaId; - - public ObjectKind ObjectKind - { - get => (ObjectKind) Pointer->GameObject.ObjectKind; - set => Pointer->GameObject.ObjectKind = (byte)value; - } - - public Utf8String Utf8Name - => new(Pointer->GameObject.Name); - - public Human* DrawObject - => (Human*)Pointer->GameObject.DrawObject; - - public void SetModelId(int value) - { - if (Pointer != null) - Pointer->ModelCharaId = value; - } - - public static implicit operator bool(Actor actor) - => actor.Pointer != null; - - public static bool operator true(Actor actor) - => actor.Pointer != null; - - public static bool operator false(Actor actor) - => actor.Pointer == null; - - public static bool operator !(Actor actor) - => actor.Pointer == null; - - public bool Equals(Actor other) - => Pointer == other.Pointer; - - public override bool Equals(object? obj) - => obj is Actor other && Equals(other); - - public override int GetHashCode() - => ((ulong)Pointer).GetHashCode(); - - public static bool operator ==(Actor lhs, Actor rhs) - => lhs.Pointer == rhs.Pointer; - - public static bool operator !=(Actor lhs, Actor rhs) - => lhs.Pointer != rhs.Pointer; - - private static IIdentifier CreateIdentifier(Actor actor) - { - switch (actor.ObjectKind) - { - case ObjectKind.Player: return new PlayerIdentifier(actor.Utf8Name, actor.Pointer->HomeWorld); - - case ObjectKind.BattleNpc: - { - var ownerId = actor.Pointer->GameObject.OwnerID; - if (ownerId != 0xE0000000) - { - var owner = (Actor)Dalamud.Objects.SearchById(ownerId)?.Address; - if (!owner) - return new InvalidIdentifier(); - - return new OwnedIdentifier(actor.Utf8Name, owner.Utf8Name, owner.Pointer->HomeWorld, - actor.Pointer->GameObject.DataID, ObjectKind.BattleNpc); - } - - return new NpcIdentifier(actor.Utf8Name, actor.Pointer->GameObject.ObjectIndex, - actor.Pointer->GameObject.DataID); - } - case ObjectKind.Retainer: - case ObjectKind.EventNpc: - return new NpcIdentifier(actor.Utf8Name, actor.Pointer->GameObject.ObjectIndex, - actor.Pointer->GameObject.DataID); - case ObjectKind.MountType: - case ObjectKind.Companion: - { - var idx = actor.Pointer->GameObject.ObjectIndex; - if (idx % 2 == 0) - return new InvalidIdentifier(); - - var owner = (Actor)Dalamud.Objects[idx - 1]?.Address; - if (!owner) - return new InvalidIdentifier(); - - return new OwnedIdentifier(actor.Utf8Name, owner.Utf8Name, owner.Pointer->HomeWorld, - actor.Pointer->GameObject.DataID, actor.ObjectKind); - } - default: return new InvalidIdentifier(); - } - } -} diff --git a/Glamourer/Api/GlamourerIpc.cs b/Glamourer/Api/GlamourerIpc.cs index 0136d06..3ab12f8 100644 --- a/Glamourer/Api/GlamourerIpc.cs +++ b/Glamourer/Api/GlamourerIpc.cs @@ -27,17 +27,17 @@ public class GlamourerIpc : IDisposable private readonly ObjectTable _objectTable; private readonly DalamudPluginInterface _pluginInterface; - internal ICallGateProvider? ProviderGetAllCustomization; - internal ICallGateProvider? ProviderGetAllCustomizationFromCharacter; - internal ICallGateProvider? ProviderApplyAll; - internal ICallGateProvider? ProviderApplyAllToCharacter; - internal ICallGateProvider? ProviderApplyOnlyCustomization; - internal ICallGateProvider? ProviderApplyOnlyCustomizationToCharacter; - internal ICallGateProvider? ProviderApplyOnlyEquipment; - internal ICallGateProvider? ProviderApplyOnlyEquipmentToCharacter; - internal ICallGateProvider? ProviderRevert; - internal ICallGateProvider? ProviderRevertCharacter; - internal ICallGateProvider? ProviderGetApiVersion; + //internal ICallGateProvider? ProviderGetAllCustomization; + //internal ICallGateProvider? ProviderGetAllCustomizationFromCharacter; + //internal ICallGateProvider? ProviderApplyAll; + //internal ICallGateProvider? ProviderApplyAllToCharacter; + //internal ICallGateProvider? ProviderApplyOnlyCustomization; + //internal ICallGateProvider? ProviderApplyOnlyCustomizationToCharacter; + //internal ICallGateProvider? ProviderApplyOnlyEquipment; + //internal ICallGateProvider? ProviderApplyOnlyEquipmentToCharacter; + //internal ICallGateProvider? ProviderRevert; + //internal ICallGateProvider? ProviderRevertCharacter; + //internal ICallGateProvider? ProviderGetApiVersion; public GlamourerIpc(ClientState clientState, ObjectTable objectTable, DalamudPluginInterface pluginInterface) { diff --git a/Glamourer/Api/PenumbraAttach.cs b/Glamourer/Api/PenumbraAttach.cs index fbc51e2..5feb90a 100644 --- a/Glamourer/Api/PenumbraAttach.cs +++ b/Glamourer/Api/PenumbraAttach.cs @@ -2,15 +2,18 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Dalamud.Plugin.Ipc; +using Glamourer.Interop; +using Glamourer.Structs; using ImGuiNET; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; namespace Glamourer.Api; public class PenumbraAttach : IDisposable { public const int RequiredPenumbraBreakingVersion = 4; - public const int RequiredPenumbraFeatureVersion = 0; + public const int RequiredPenumbraFeatureVersion = 12; private ICallGateSubscriber? _tooltipSubscriber; private ICallGateSubscriber? _clickSubscriber; @@ -18,11 +21,14 @@ public class PenumbraAttach : IDisposable private ICallGateSubscriber? _redrawSubscriberObject; private ICallGateSubscriber? _drawObjectInfo; private ICallGateSubscriber? _creatingCharacterBase; + private ICallGateSubscriber? _createdCharacterBase; + private ICallGateSubscriber? _cutsceneParent; private readonly ICallGateSubscriber _initializedEvent; private readonly ICallGateSubscriber _disposedEvent; public event Action? CreatingCharacterBase; + public event Action? CreatedCharacterBase; public PenumbraAttach(bool attach) { @@ -51,6 +57,7 @@ public class PenumbraAttach : IDisposable _redrawSubscriberName = Dalamud.PluginInterface.GetIpcSubscriber("Penumbra.RedrawObjectByName"); _redrawSubscriberObject = Dalamud.PluginInterface.GetIpcSubscriber("Penumbra.RedrawObject"); _drawObjectInfo = Dalamud.PluginInterface.GetIpcSubscriber("Penumbra.GetDrawObjectInfo"); + _cutsceneParent = Dalamud.PluginInterface.GetIpcSubscriber("Penumbra.GetCutsceneParentIndex"); if (!attach) return; @@ -60,9 +67,12 @@ public class PenumbraAttach : IDisposable Dalamud.PluginInterface.GetIpcSubscriber("Penumbra.ChangedItemClick"); _creatingCharacterBase = Dalamud.PluginInterface.GetIpcSubscriber("Penumbra.CreatingCharacterBase"); + _createdCharacterBase = + Dalamud.PluginInterface.GetIpcSubscriber("Penumbra.CreatedCharacterBase"); _tooltipSubscriber.Subscribe(PenumbraTooltip); _clickSubscriber.Subscribe(PenumbraRightClick); - _creatingCharacterBase.Subscribe(SubscribeCharacterBase); + _creatingCharacterBase.Subscribe(SubscribeCreatingCharacterBase); + _createdCharacterBase.Subscribe(SubscribeCreatedCharacterBase); PluginLog.Debug("Glamourer attached to Penumbra."); } catch (Exception e) @@ -71,14 +81,18 @@ public class PenumbraAttach : IDisposable } } - private void SubscribeCharacterBase(IntPtr gameObject, string _, IntPtr modelId, IntPtr customize, IntPtr equipment) + private void SubscribeCreatingCharacterBase(IntPtr gameObject, string _, IntPtr modelId, IntPtr customize, IntPtr equipment) => CreatingCharacterBase?.Invoke(gameObject, modelId, customize, equipment); + private void SubscribeCreatedCharacterBase(IntPtr gameObject, string _, IntPtr drawObject) + => CreatedCharacterBase?.Invoke(gameObject, drawObject); + public void Unattach() { _tooltipSubscriber?.Unsubscribe(PenumbraTooltip); _clickSubscriber?.Unsubscribe(PenumbraRightClick); - _creatingCharacterBase?.Unsubscribe(SubscribeCharacterBase); + _creatingCharacterBase?.Unsubscribe(SubscribeCreatingCharacterBase); + _createdCharacterBase?.Unsubscribe(SubscribeCreatedCharacterBase); _tooltipSubscriber = null; _clickSubscriber = null; _creatingCharacterBase = null; @@ -109,25 +123,54 @@ public class PenumbraAttach : IDisposable if (button != MouseButton.Right || type != ChangedItemType.Item) return; - //var gPose = ObjectManager.GPosePlayer; - //var player = ObjectManager.Player; - //var item = (Lumina.Excel.GeneratedSheets.Item)type.GetObject(id)!; - //var writeItem = new Item(item, string.Empty); - //if (gPose != null) - //{ - // writeItem.Write(gPose.Address); - // UpdateCharacters(gPose, player); - //} - //else if (player != null) - //{ - // writeItem.Write(player.Address); - // UpdateCharacters(player); - //} + var item = (Lumina.Excel.GeneratedSheets.Item)type.GetObject(id)!; + var writeItem = new Item(item, string.Empty); + + UpdateItem(ObjectManager.GPosePlayer, writeItem); + UpdateItem(ObjectManager.Player, writeItem); + } + + private static void UpdateItem(Actor actor, Item item) + { + if (!actor || !actor.DrawObject) + return; + + switch (item.EquippableTo) + { + case EquipSlot.MainHand: + { + var off = item.HasSubModel + ? new CharacterWeapon(item.SubModel.id, item.SubModel.type, item.SubModel.variant, actor.DrawObject.OffHand.Stain) + : item.IsBothHand + ? CharacterWeapon.Empty + : actor.OffHand; + var main = new CharacterWeapon(item.MainModel.id, item.MainModel.type, item.MainModel.variant, actor.DrawObject.MainHand.Stain); + Glamourer.RedrawManager.LoadWeapon(actor, main, off); + return; + } + case EquipSlot.OffHand: + { + var off = new CharacterWeapon(item.MainModel.id, item.MainModel.type, item.MainModel.variant, actor.DrawObject.OffHand.Stain); + var main = actor.MainHand; + Glamourer.RedrawManager.LoadWeapon(actor, main, off); + return; + } + default: + { + var current = actor.DrawObject.Equip[item.EquippableTo]; + var armor = new CharacterArmor(item.MainModel.id, (byte)item.MainModel.variant, current.Stain); + Glamourer.RedrawManager.ChangeEquip(actor.DrawObject, item.EquippableTo, armor); + return; + } + } } public Actor GameObjectFromDrawObject(IntPtr drawObject) => _drawObjectInfo?.InvokeFunc(drawObject).Item1 ?? IntPtr.Zero; + public int CutsceneParent(int idx) + => _cutsceneParent?.InvokeFunc(idx) ?? -1; + public void RedrawObject(GameObject? actor, RedrawType settings, bool repeat) { if (actor == null) diff --git a/Glamourer/CharacterSave.cs b/Glamourer/CharacterSave.cs deleted file mode 100644 index 0fbcf93..0000000 --- a/Glamourer/CharacterSave.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System; -using System.Linq; -using System.Runtime.InteropServices; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Glamourer.Customization; -using Glamourer.Structs; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Penumbra.GameData.Structs; -using Functions = Penumbra.GameData.Util.Functions; - -namespace Glamourer; - -public class CharacterSaveConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, CharacterSave? value, JsonSerializer serializer) - { - var s = value?.ToBase64() ?? string.Empty; - serializer.Serialize(writer, s); - } - - public override CharacterSave ReadJson(JsonReader reader, Type objectType, CharacterSave? existingValue, bool hasExistingValue, - JsonSerializer serializer) - { - var token = JToken.Load(reader); - var s = token.ToObject(); - return CharacterSave.FromString(s!); - } -} - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public unsafe struct CharacterData -{ - [Flags] - public enum SaveFlags : byte - { - WriteCustomizations = 0x01, - IsWet = 0x02, - SetHatState = 0x04, - SetWeaponState = 0x08, - SetVisorState = 0x10, - HatState = 0x20, - WeaponState = 0x40, - VisorState = 0x80, - } - - public const byte TotalSizeVersion1 = 1 + 1 + 2 + 56 + CustomizeData.Size; - public const byte TotalSizeVersion2 = 1 + 1 + 2 + 56 + CustomizeData.Size + 4 + 1; - public const byte TotalSizeVersion3 = 1 + 1 + 2 + 7 + 7 + 2 + 40 + CustomizeData.Size + 4; - public const byte CurrentVersion = 3; - - public byte Version; - public SaveFlags Flags; - public CharacterEquipMask Equip; - public CharacterWeapon MainHand; - public CharacterWeapon OffHand; - public ushort Padding; - public CharacterArmor Head; - public CharacterArmor Body; - public CharacterArmor Hands; - public CharacterArmor Legs; - public CharacterArmor Feet; - public CharacterArmor Ears; - public CharacterArmor Neck; - public CharacterArmor Wrist; - public CharacterArmor RFinger; - public CharacterArmor LFinger; - private CustomizeData _customizeData; - public float Alpha; - - public Customize Customize - { - get - { - fixed (CustomizeData* ptr = &_customizeData) - { - return new Customize(ptr); - } - } - } - - public CharacterEquip Equipment - { - get - { - fixed (CharacterArmor* ptr = &Head) - { - return new CharacterEquip(ptr); - } - } - } - - public static readonly CharacterData Default - = new() - { - Version = CurrentVersion, - Flags = SaveFlags.WriteCustomizations, - Equip = CharacterEquipMask.All, - MainHand = CharacterWeapon.Empty, - OffHand = CharacterWeapon.Empty, - Padding = 0, - Head = CharacterArmor.Empty, - Body = CharacterArmor.Empty, - Hands = CharacterArmor.Empty, - Legs = CharacterArmor.Empty, - Feet = CharacterArmor.Empty, - Ears = CharacterArmor.Empty, - Neck = CharacterArmor.Empty, - Wrist = CharacterArmor.Empty, - RFinger = CharacterArmor.Empty, - LFinger = CharacterArmor.Empty, - _customizeData = Customize.Default, - Alpha = 1f, - }; - - public void Load(Actor actor) - { - if (!actor.IsHuman || actor.Pointer->GameObject.DrawObject == null) - return; - - var human = (Human*)actor.Pointer->GameObject.DrawObject; - _customizeData.Read(human->CustomizeData); - fixed (void* equip = &Head) - { - Functions.MemCpyUnchecked(equip, human->EquipSlotData, sizeof(CharacterArmor) * 10); - } - } - - public string ToBase64() - { - fixed (void* ptr = &this) - { - return Convert.ToBase64String(new ReadOnlySpan(ptr, sizeof(CharacterData))); - } - } - - private static void CheckSize(int length, int requiredLength) - { - if (length != requiredLength) - throw new Exception( - $"Can not parse Base64 string into CharacterSave:\n\tInvalid size {length} instead of {requiredLength}."); - } - - private static void CheckRange(int idx, byte value, byte min, byte max) - { - if (value < min || value > max) - throw new Exception( - $"Can not parse Base64 string into CharacterSave:\n\tInvalid value {value} in byte {idx}, should be in [{min},{max}]."); - } - - public static CharacterData FromString(string data) - { - var bytes = Convert.FromBase64String(data); - var ret = new CharacterData(); - fixed (byte* ptr = bytes) - { - switch (bytes[0]) - { - case 1: - CheckSize(bytes.Length, TotalSizeVersion1); - CheckRange(2, bytes[1], 0, 1); - Functions.MemCpyUnchecked(&ret, ptr, TotalSizeVersion1); - ret.Version = CurrentVersion; - ret.Alpha = 1f; - break; - case 2: - CheckSize(bytes.Length, TotalSizeVersion2); - CheckRange(2, bytes[1], 0, 0x3F); - Functions.MemCpyUnchecked(&ret, ptr, TotalSizeVersion2 - 1); - ret.Flags &= ~SaveFlags.HatState; - if ((bytes.Last() & 0x01) != 0) - ret.Flags |= SaveFlags.HatState; - if ((bytes.Last() & 0x02) != 0) - ret.Flags |= SaveFlags.WeaponState; - if ((bytes.Last() & 0x04) != 0) - ret.Flags |= SaveFlags.VisorState; - break; - case 3: - CheckSize(bytes.Length, TotalSizeVersion3); - Functions.MemCpyUnchecked(&ret, ptr, TotalSizeVersion3); - break; - default: throw new Exception($"Can not parse Base64 string into CharacterSave:\n\tInvalid Version {bytes[0]}."); - } - } - - return ret; - } -} - -[JsonConverter(typeof(CharacterSaveConverter))] -public class CharacterSave -{ - private CharacterData _data; - - public CharacterSave() - => _data = CharacterData.Default; - - public CharacterSave(Actor actor) - => _data.Load(actor); - - public void Load(Actor actor) - => _data.Load(actor); - - public string ToBase64() - => _data.ToBase64(); - - public Customize Customize - => _data.Customize; - - public CharacterEquip Equipment - => _data.Equipment; - - public ref CharacterWeapon MainHand - => ref _data.MainHand; - - public ref CharacterWeapon OffHand - => ref _data.OffHand; - - public static CharacterSave FromString(string data) - => new() { _data = CharacterData.FromString(data) }; -} diff --git a/Glamourer/CurrentManipulations.cs b/Glamourer/CurrentManipulations.cs deleted file mode 100644 index 800acfe..0000000 --- a/Glamourer/CurrentManipulations.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Glamourer.Customization; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Glamourer; - -public class CurrentManipulations -{ - private readonly RestrictedGear _restrictedGear = GameData.RestrictedGear(Dalamud.GameData); - private readonly Dictionary _characterSaves = new(); - - public CharacterSave CreateSave(Actor actor) - { - var id = actor.GetIdentifier(); - if (_characterSaves.TryGetValue(id, out var save)) - return save; - - save = new CharacterSave(actor); - _characterSaves.Add(id.CreatePermanent(), save); - return save; - } - - public bool TryGetDesign(Actor.IIdentifier identifier, [NotNullWhen(true)] out CharacterSave? save) - => _characterSaves.TryGetValue(identifier, out save); - - public CharacterArmor? ChangeEquip(Actor actor, EquipSlot slot, CharacterArmor data) - { - var save = CreateSave(actor); - (_, data) = _restrictedGear.ResolveRestricted(data, slot, save.Customize.Race, save.Customize.Gender); - if (save.Equipment[slot] == data) - return null; - - save.Equipment[slot] = data; - return data; - } - - public bool ChangeWeapon(Actor actor, CharacterWeapon main) - { - var save = CreateSave(actor); - if (save.MainHand == main) - return false; - - save.MainHand = main; - return true; - } - - public bool ChangeWeapon(Actor actor, CharacterWeapon main, CharacterWeapon off) - { - var save = CreateSave(actor); - if (main == save.MainHand && off == save.OffHand) - return false; - - save.MainHand = main; - save.OffHand = off; - return true; - } - - public void ChangeCustomization(Actor actor, Customize customize) - { - var save = CreateSave(actor); - FixRestrictedGear(save, customize.Gender, customize.Race); - save.Customize.Load(customize); - } - - public bool ChangeCustomization(Actor actor, CustomizationId id, byte value) - { - if (id == CustomizationId.Race) - return ChangeRace(actor, (SubRace)value); - if (id == CustomizationId.Gender) - return ChangeGender(actor, (Gender)value); - - var save = CreateSave(actor); - var customize = save.Customize; - if (customize[id] != value) - return false; - - customize[id] = value; - return true; - } - - // Change a gender and fix up all required customizations afterwards. - public bool ChangeGender(Actor actor, Gender gender) - { - var save = CreateSave(actor); - if (save.Customize.Gender == gender) - return false; - - var customize = save.Customize; - FixRestrictedGear(save, gender, customize.Race); - FixUpAttributes(customize); - return true; - } - - // Change a race and fix up all required customizations afterwards. - public bool ChangeRace(Actor actor, SubRace clan) - { - var save = CreateSave(actor); - if (save.Customize.Clan == clan) - return false; - - var customize = save.Customize; - var race = clan.ToRace(); - var gender = race == Race.Hrothgar ? Gender.Male : customize.Gender; // TODO Female Hrothgar - FixRestrictedGear(save, gender, race); - customize.Gender = gender; - customize.Race = race; - customize.Clan = clan; - - FixUpAttributes(customize); - return true; - } - - // Go through a whole customization struct and fix up all settings that need fixing. - private void FixUpAttributes(Customize customize) - { - var set = Glamourer.Customization.GetList(customize.Clan, customize.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: break; - default: - var count = set.Count(id); - if (set.DataByValue(id, customize[id], out _) < 0) - customize[id] = count == 0 ? (byte)0 : set.Data(id, 0).Value; - break; - } - } - } - - private void FixRestrictedGear(CharacterSave save, Gender gender, Race race) - { - if (race == save.Customize.Race && gender == save.Customize.Gender) - return; - - var equip = save.Equipment; - foreach (var slot in EquipSlotExtensions.EqdpSlots) - (_, equip[slot]) = _restrictedGear.ResolveRestricted(equip[slot], slot, race, gender); - } -} diff --git a/Glamourer/Dalamud.cs b/Glamourer/Dalamud.cs index c42b2dc..d105b8b 100644 --- a/Glamourer/Dalamud.cs +++ b/Glamourer/Dalamud.cs @@ -16,14 +16,14 @@ public class Dalamud public static void Initialize(DalamudPluginInterface pluginInterface) => pluginInterface.Create(); - // @formatter:off - [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; + // @formatter:off + [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; // @formatter:on } diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index 5a9fdc0..5193727 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -1,4 +1,8 @@ using System; +using System.Runtime.InteropServices; +using Glamourer.State; +using Glamourer.Structs; +using Penumbra.GameData.Structs; namespace Glamourer.Designs; @@ -14,3 +18,9 @@ public class Design public override string ToString() => Name; } + +public struct ArmorData +{ + public CharacterArmor Model; + public bool Ignore; +} diff --git a/Glamourer/Designs/FixedDesigns.cs b/Glamourer/Designs/FixedDesigns.cs index ee2d5f3..b486e27 100644 --- a/Glamourer/Designs/FixedDesigns.cs +++ b/Glamourer/Designs/FixedDesigns.cs @@ -1,7 +1,172 @@ using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection.Metadata.Ecma335; +using Dalamud.Logging; +using System.Runtime; +using System.Text; +using Dalamud.Utility; +using Glamourer.Interop; +using Glamourer.Structs; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; namespace Glamourer.Designs; +public struct FixedCondition +{ + private const ulong _territoryFlag = 1ul << 32; + private const ulong _jobFlag = 1ul << 33; + private ulong _data; + + public static FixedCondition TerritoryCondition(ushort territoryType) + => new() { _data = territoryType | _territoryFlag }; + + public static FixedCondition JobCondition(JobGroup group) + => new() { _data = group.Id | _jobFlag }; + + public bool Check(Actor actor) + { + if ((_data & (_territoryFlag | _jobFlag)) == 0) + return true; + + if ((_data & _territoryFlag) != 0) + return Dalamud.ClientState.TerritoryType == (ushort)_data; + + if (actor && GameData.JobGroups(Dalamud.GameData).TryGetValue((ushort)_data, out var group) && group.Fits(actor.Job)) + return true; + + return true; + } + + public override string ToString() + => _data.ToString(); +} + +public class FixedDesign +{ + public const int CurrentVersion = 0; + + public string Name { get; private set; } + public bool Enabled; + public List Actors; + public List<(FixedCondition, Design)> Customization; + public List<(FixedCondition, Design)> Equipment; + public List<(FixedCondition, Design)> Weapons; + + public FixedDesign(string name) + { + Name = name; + Actors = new List(); + Customization = new List<(FixedCondition, Design)>(); + Equipment = new List<(FixedCondition, Design)>(); + Weapons = new List<(FixedCondition, Design)>(); + } + + public static FixedDesign? Load(JObject j) + { + try + { + var name = j[nameof(Name)]?.Value(); + if (name.IsNullOrEmpty()) + return null; + + var version = j["Version"]?.Value(); + if (version == null) + return null; + + return version switch + { + CurrentVersion => LoadCurrentVersion(j, name), + _ => null, + }; + } + catch (Exception e) + { + PluginLog.Error($"Error loading fixed design:\n{e}"); + return null; + } + } + + private static FixedDesign? LoadCurrentVersion(JObject j, string name) + { + var enabled = j[nameof(Enabled)]?.Value() ?? false; + var ret = new FixedDesign(name) + { + Enabled = enabled, + }; + + var actors = j[nameof(Actors)]; + //foreach(var pair in actors?.Children().) + return null; + } + + + public void Save(FileInfo file) + { + try + { + using var s = file.Exists ? file.Open(FileMode.Truncate) : file.Open(FileMode.CreateNew); + using var w = new StreamWriter(s, Encoding.UTF8); + using var j = new JsonTextWriter(w) + { + Formatting = Formatting.Indented, + }; + j.WriteStartObject(); + j.WritePropertyName(nameof(Name)); + j.WriteValue(Name); + j.WritePropertyName("Version"); + j.WriteValue(CurrentVersion); + j.WritePropertyName(nameof(Enabled)); + j.WriteValue(Enabled); + j.WritePropertyName(nameof(Actors)); + j.WriteStartArray(); + foreach (var actor in Actors) + actor.ToJson(j); + j.WriteEndArray(); + j.WritePropertyName(nameof(Customization)); + j.WriteStartArray(); + foreach (var (condition, design) in Customization) + { + j.WritePropertyName(condition.ToString()); + j.WriteValue(design.Name); + } + + j.WriteEndArray(); + j.WritePropertyName(nameof(Equipment)); + j.WriteStartArray(); + foreach (var (condition, design) in Equipment) + { + j.WritePropertyName(condition.ToString()); + j.WriteValue(design.Name); + } + + j.WriteEndArray(); + j.WritePropertyName(nameof(Weapons)); + j.WriteStartArray(); + foreach (var (condition, design) in Weapons) + { + j.WritePropertyName(condition.ToString()); + j.WriteValue(design.Name); + } + + j.WriteEndArray(); + } + catch (Exception e) + { + PluginLog.Error($"Could not save collection {Name}:\n{e}"); + } + } + + public static bool Load(FileInfo path, [NotNullWhen(true)] out FixedDesign? result) + { + result = null; + return true; + } +} + public class FixedDesigns : IDisposable { //public class FixedDesign diff --git a/Glamourer/Designs/RevertableDesigns.cs b/Glamourer/Designs/RevertableDesigns.cs index bfa0517..513d43a 100644 --- a/Glamourer/Designs/RevertableDesigns.cs +++ b/Glamourer/Designs/RevertableDesigns.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; +using Glamourer.State; namespace Glamourer.Designs; diff --git a/Glamourer/Glamourer.cs b/Glamourer/Glamourer.cs index 42a7381..a00f66a 100644 --- a/Glamourer/Glamourer.cs +++ b/Glamourer/Glamourer.cs @@ -1,10 +1,18 @@ -using System.Reflection; +using System; +using System.Reflection; using Dalamud.Game.Command; +using Dalamud.Hooking; using Dalamud.Interface.Windowing; +using Dalamud.Logging; using Dalamud.Plugin; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Api; using Glamourer.Customization; using Glamourer.Gui; +using Glamourer.Interop; +using Glamourer.State; +using Penumbra.GameData; namespace Glamourer; @@ -25,14 +33,18 @@ public class Glamourer : IDalamudPlugin public static GlamourerConfig Config = null!; - public static PenumbraAttach Penumbra = null!; - public static ICustomizationManager Customization = null!; - public static RedrawManager RedrawManager = null!; - private readonly WindowSystem _windowSystem = new("Glamourer"); - private readonly FixedDesigns _fixedDesigns; - private readonly CurrentManipulations _currentManipulations; + public static IObjectIdentifier Identifier = null!; + public static PenumbraAttach Penumbra = null!; + public static ICustomizationManager Customization = null!; + public static RestrictedGear RestrictedGear = null!; + public static ModelData Models = null!; + public static RedrawManager RedrawManager = null!; + public readonly FixedDesigns FixedDesigns; + public readonly CurrentManipulations CurrentManipulations; + + private readonly WindowSystem _windowSystem = new("Glamourer"); + private readonly Interface _interface; - private readonly Interface _interface; //public readonly DesignManager Designs; //public static RevertableDesigns RevertableDesigns = new(); @@ -41,15 +53,18 @@ public class Glamourer : IDalamudPlugin public unsafe Glamourer(DalamudPluginInterface pluginInterface) { Dalamud.Initialize(pluginInterface); - Customization = CustomizationManager.Create(Dalamud.PluginInterface, Dalamud.GameData, Dalamud.ClientState.ClientLanguage); - Config = GlamourerConfig.Load(); - Penumbra = new PenumbraAttach(Config.AttachToPenumbra); - _fixedDesigns = new FixedDesigns(); - + Customization = CustomizationManager.Create(Dalamud.PluginInterface, Dalamud.GameData, Dalamud.ClientState.ClientLanguage); + RestrictedGear = GameData.RestrictedGear(Dalamud.GameData); + Models = GameData.Models(Dalamud.GameData); + Identifier = global::Penumbra.GameData.GameData.GetIdentifier(Dalamud.GameData); + Config = GlamourerConfig.Load(); + Penumbra = new PenumbraAttach(Config.AttachToPenumbra); + FixedDesigns = new FixedDesigns(); + CurrentManipulations = new CurrentManipulations(); //Designs = new DesignManager(); //GlamourerIpc = new GlamourerIpc(Dalamud.ClientState, Dalamud.Objects, Dalamud.PluginInterface); - RedrawManager = new RedrawManager(_fixedDesigns, _currentManipulations); + RedrawManager = new RedrawManager(FixedDesigns, CurrentManipulations); Dalamud.Commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { @@ -149,11 +164,11 @@ public class Glamourer : IDalamudPlugin // public void OnGlamour(string command, string arguments) { - static void PrintHelp() - { - Dalamud.Chat.Print("Usage:"); - Dalamud.Chat.Print($" {HelpString}"); - } + //static void PrintHelp() + //{ + // Dalamud.Chat.Print("Usage:"); + // Dalamud.Chat.Print($" {HelpString}"); + //} //arguments = arguments.Trim(); //if (!arguments.Any()) diff --git a/Glamourer/Glamourer.csproj b/Glamourer/Glamourer.csproj index 70a70c3..7d30541 100644 --- a/Glamourer/Glamourer.csproj +++ b/Glamourer/Glamourer.csproj @@ -118,9 +118,7 @@ - - + + \ No newline at end of file diff --git a/Glamourer/Gui/Interface.Actors.cs b/Glamourer/Gui/Interface.Actors.cs index 7a42add..4d521e4 100644 --- a/Glamourer/Gui/Interface.Actors.cs +++ b/Glamourer/Gui/Interface.Actors.cs @@ -1,12 +1,14 @@ using System; +using System.Linq; using System.Numerics; using Dalamud.Interface; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Glamourer.Interop; +using Glamourer.State; using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; namespace Glamourer.Gui; @@ -14,8 +16,15 @@ internal partial class Interface { private class ActorTab { - private ObjectManager.ActorData _data = new(string.Empty, new Actor.InvalidIdentifier(), Actor.Null, false, Actor.Null); - private Actor _nextSelect = Actor.Null; + private readonly CurrentManipulations _manipulations; + + public ActorTab(CurrentManipulations manipulations) + => _manipulations = manipulations; + + private Actor.IIdentifier _identifier = Actor.IIdentifier.Invalid; + private ObjectManager.ActorData _currentData = ObjectManager.ActorData.Invalid; + private string _currentLabel = string.Empty; + private CurrentDesign? _currentSave; public void Draw() { @@ -24,62 +33,107 @@ internal partial class Interface return; DrawActorSelector(); - if (_data.Label.Length == 0) - return; + if (!ObjectManager.Actors.TryGetValue(_identifier, out _currentData)) + _currentData = ObjectManager.ActorData.Invalid; + else + _currentLabel = _currentData.Label; ImGui.SameLine(); - if (_data.Actor.IsHuman) - DrawActorPanel(); - else - DrawMonsterPanel(); + DrawPanel(); } - private void DrawActorPanel() + private unsafe void DrawPanel() { + if (_identifier == Actor.IIdentifier.Invalid) + return; + + using var group = ImRaii.Group(); - if (!Glamourer.RedrawManager.CurrentManipulations.GetSave(_data.Actor, out var save)) + DrawPanelHeader(); + using var child = ImRaii.Child("##ActorPanel", -Vector2.One, true); + if (!child || _currentSave == null) return; - if (DrawCustomization(save.Customize, save.Equipment, !_data.Modifiable)) - { - //Glamourer.RedrawManager.Set(_data.Actor.Address, _character); - Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true); - } + if (_currentData.Valid) + _currentSave.Update(_currentData.Objects[0]); - if (ImGui.Button("Set Machinist Goggles")) - { - Glamourer.RedrawManager.ChangeEquip(_data.Actor.Address, EquipSlot.Head, new CharacterArmor(265, 1, 0)); - } - - if (ImGui.Button("Set Weapon")) - { - Glamourer.RedrawManager.LoadWeapon(_data.Actor.Address, new CharacterWeapon(0x00C9, 0x004E, 0x0001, 0x00), new CharacterWeapon(0x0065, 0x003D, 0x0001, 0x00)); - } + var d = _currentData.Objects[0].DrawObject.Pointer; + var x = (*(delegate* unmanaged**)d)[50](d); + ImGui.Text($"{x} {_currentData.Objects[0].ModelId}"); + if (x == 1) + CustomizationDrawer.Draw(_currentSave.Data.Customize, _currentSave.Data.Equipment, _currentData.Objects, + _identifier is Actor.SpecialIdentifier); } - private void DrawMonsterPanel() + private const uint RedHeaderColor = 0xFF1818C0; + private const uint GreenHeaderColor = 0xFF18C018; + + private void DrawPanelHeader() { - using var group = ImRaii.Group(); - var currentModel = (uint)_data.Actor.ModelId; - var models = GameData.Models(Dalamud.GameData); - var currentData = models.Models.TryGetValue(currentModel, out var c) ? c.FirstName : $"#{currentModel}"; - using var combo = ImRaii.Combo("Model Id", currentData); - if (!combo) - return; - - foreach (var (id, data) in models.Models) - { - if (ImGui.Selectable(data.FirstName, id == currentModel) && id != currentModel) - { - _data.Actor.SetModelId((int)id); - _data.Actor.ObjectKind = - Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true); - } - ImGuiUtil.HoverTooltip(data.AllNames); - } + var color = _currentData.Valid ? GreenHeaderColor : RedHeaderColor; + var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg); + using var c = ImRaii.PushColor(ImGuiCol.Text, color) + .Push(ImGuiCol.Button, buttonColor) + .Push(ImGuiCol.ButtonHovered, buttonColor) + .Push(ImGuiCol.ButtonActive, buttonColor); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameRounding, 0); + ImGui.Button($"{_currentLabel}##playerHeader", -Vector2.UnitX); } + //private void DrawActorPanel() + //{ + // using var group = ImRaii.Group(); + // if (!_data.Identifier.IsValid) + // return; + // + // if (DrawCustomization(_currentSave.Customize, _currentSave.Equipment, !_data.Modifiable)) + // //Glamourer.RedrawManager.Set(_data.Actor.Address, _character); + // Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true); + // + // if (ImGui.Button("Set Machinist Goggles")) + // Glamourer.RedrawManager.ChangeEquip(_data.Actor, EquipSlot.Head, new CharacterArmor(265, 1, 0)); + // + // if (ImGui.Button("Set Weapon")) + // Glamourer.RedrawManager.LoadWeapon(_data.Actor.Address, new CharacterWeapon(0x00C9, 0x004E, 0x0001, 0x00), + // new CharacterWeapon(0x0065, 0x003D, 0x0001, 0x00)); + // + // if (ImGui.Button("Set Customize")) + // { + // unsafe + // { + // var data = _data.Actor.Customize.Data->Clone(); + // Glamourer.RedrawManager.UpdateCustomize(_data.Actor.DrawObject, new Customize(&data) + // { + // SkinColor = 154, + // }); + // } + // } + //} + // + //private void DrawMonsterPanel() + //{ + // using var group = ImRaii.Group(); + // var currentModel = (uint)_data.Actor.ModelId; + // var models = GameData.Models(Dalamud.GameData); + // var currentData = models.Models.TryGetValue(currentModel, out var c) ? c.FirstName : $"#{currentModel}"; + // using var combo = ImRaii.Combo("Model Id", currentData); + // if (!combo) + // return; + // + // foreach (var (id, data) in models.Models) + // { + // if (ImGui.Selectable(data.FirstName, id == currentModel) && id != currentModel) + // { + // _data.Actor.SetModelId((int)id); + // Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true); + // } + // + // ImGuiUtil.HoverTooltip(data.AllNames); + // } + //} + private LowerString _actorFilter = LowerString.Empty; @@ -91,63 +145,55 @@ internal partial class Interface .Push(ImGuiStyleVar.FrameRounding, 0); ImGui.SetNextItemWidth(_actorSelectorWidth); LowerString.InputWithHint("##actorFilter", "Filter...", ref _actorFilter, 64); - using (var child = ImRaii.Child("##actorSelector", new Vector2(_actorSelectorWidth, -ImGui.GetFrameHeight()), true)) - { - if (!child) - return; - - _data.Actor = Actor.Null; - _data.GPose = Actor.Null; - _data.Modifiable = false; - - style.Push(ImGuiStyleVar.ItemSpacing, oldSpacing); - var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight()); - var remainder = ImGuiClip.FilteredClippedDraw(ObjectManager.GetEnumerator(), skips, CheckFilter, DrawSelectable); - ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight()); - style.Pop(); - } + DrawSelector(oldSpacing); DrawSelectionButtons(); } - private void UpdateSelection(ObjectManager.ActorData data) + private void DrawSelector(Vector2 oldSpacing) { - _data = data; - //_character.Load(_data.Actor); + using var child = ImRaii.Child("##actorSelector", new Vector2(_actorSelectorWidth, -ImGui.GetFrameHeight()), true); + if (!child) + return; + + ObjectManager.Update(); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, oldSpacing); + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight()); + var remainder = ImGuiClip.FilteredClippedDraw(ObjectManager.List, skips, CheckFilter, DrawSelectable); + ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight()); } - private bool CheckFilter(ObjectManager.ActorData data) - { - if (_nextSelect && _nextSelect == data.Actor || data.Label == _data.Label) - UpdateSelection(data); - return data.Label.Contains(_actorFilter.Lower, StringComparison.OrdinalIgnoreCase); - } + private bool CheckFilter((Actor.IIdentifier, ObjectManager.ActorData) pair) + => _actorFilter.IsEmpty || pair.Item2.Label.Contains(_actorFilter.Lower, StringComparison.OrdinalIgnoreCase); - private void DrawSelectable(ObjectManager.ActorData data) + private void DrawSelectable((Actor.IIdentifier, ObjectManager.ActorData) pair) { - var equal = data.Label == _data.Label; - if (ImGui.Selectable(data.Label, equal) && !equal) - UpdateSelection(data); + var equal = pair.Item1.Equals(_identifier); + if (ImGui.Selectable(pair.Item2.Label, equal) && !equal) + { + _identifier = pair.Item1.CreatePermanent(); + _currentData = pair.Item2; + _currentSave = _currentData.Valid ? _manipulations.GetOrCreateSave(_currentData.Objects[0]) : null; + } } private void DrawSelectionButtons() { - _nextSelect = Actor.Null; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) .Push(ImGuiStyleVar.FrameRounding, 0); var buttonWidth = new Vector2(_actorSelectorWidth / 2, 0); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth , "Select the local player character.", !ObjectManager.Player, true)) - _nextSelect = _inGPose ? ObjectManager.GPosePlayer : ObjectManager.Player; + _identifier = ObjectManager.Player.GetIdentifier(); + ImGui.SameLine(); Actor targetActor = Dalamud.Targets.Target?.Address; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth, - "Select the current target, if it is in the list.", _inGPose || !targetActor, true)) - _nextSelect = targetActor; + "Select the current target, if it is in the list.", ObjectManager.IsInGPose || !targetActor, true)) + _identifier = targetActor.GetIdentifier(); } } - - private readonly ActorTab _actorTab = new(); } //internal partial class Interface diff --git a/Glamourer/Gui/Interface.Customization.cs b/Glamourer/Gui/Interface.Customization.cs index 6439bfc..3ef734f 100644 --- a/Glamourer/Gui/Interface.Customization.cs +++ b/Glamourer/Gui/Interface.Customization.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Interface; using Dalamud.Logging; using Glamourer.Customization; +using Glamourer.Interop; +using Glamourer.Util; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -15,405 +18,385 @@ namespace Glamourer.Gui; internal partial class Interface { - private static byte _tempStorage; - private static CustomizationId _tempType; - - private static bool DrawCustomization(Customize customize, CharacterEquip equip, bool locked) + private class CustomizationDrawer { - if (!ImGui.CollapsingHeader("Character Customization")) - return false; + private Customize _customize; + private CharacterEquip _equip; + private IReadOnlyCollection _actors = Array.Empty(); + private CustomizationSet _set = null!; - var ret = DrawRaceGenderSelector(customize, equip, locked); - var set = Glamourer.Customization.GetList(customize.Clan, customize.Gender); - - foreach (var id in set.Order[CharaMakeParams.MenuType.Percentage]) - ret |= PercentageSelector(set, id, customize, locked); - - Functions.IteratePairwise(set.Order[CharaMakeParams.MenuType.IconSelector], c => DrawIconSelector(set, c, customize, locked), - ImGui.SameLine); - - ret |= DrawMultiIconSelector(set, customize, locked); - - foreach (var id in set.Order[CharaMakeParams.MenuType.ListSelector]) - ret |= DrawListSelector(set, id, customize, locked); - - Functions.IteratePairwise(set.Order[CharaMakeParams.MenuType.ColorPicker], c => DrawColorPicker(set, c, customize, locked), - ImGui.SameLine); - - ret |= Checkbox(set.Option(CustomizationId.HighlightsOnFlag), customize.HighlightsOn, b => customize.HighlightsOn = b, locked); - var xPos = _inputIntSize + _framedIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X; - ImGui.SameLine(xPos); - ret |= Checkbox($"{Glamourer.Customization.GetName(CustomName.Reverse)} {set.Option(CustomizationId.FacePaint)}", - customize.FacePaintReversed, b => customize.FacePaintReversed = b, locked); - ret |= Checkbox($"{Glamourer.Customization.GetName(CustomName.IrisSmall)} {Glamourer.Customization.GetName(CustomName.IrisSize)}", - customize.SmallIris, b => customize.SmallIris = b, locked); - - if (customize.Race != Race.Hrothgar) + public static void Draw(Customize customize, CharacterEquip equip, IReadOnlyCollection actors, bool locked) { + var d = new CustomizationDrawer() + { + _customize = customize, + _equip = equip, + _actors = actors, + }; + + + if (!ImGui.CollapsingHeader("Character Customization")) + return; + + using var disabled = ImRaii.Disabled(locked); + + d.DrawRaceGenderSelector(); + + d._set = Glamourer.Customization.GetList(customize.Clan, customize.Gender); + + foreach (var id in d._set.Order[CharaMakeParams.MenuType.Percentage]) + d.PercentageSelector(id); + + Functions.IteratePairwise(d._set.Order[CharaMakeParams.MenuType.IconSelector], d.DrawIconSelector, ImGui.SameLine); + + d.DrawMultiIconSelector(); + + foreach (var id in d._set.Order[CharaMakeParams.MenuType.ListSelector]) + d.DrawListSelector(id); + + Functions.IteratePairwise(d._set.Order[CharaMakeParams.MenuType.ColorPicker], d.DrawColorPicker, ImGui.SameLine); + + d.Checkbox(d._set.Option(CustomizationId.HighlightsOnFlag), customize.HighlightsOn, b => customize.HighlightsOn = b); + var xPos = _inputIntSize + _framedIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X; ImGui.SameLine(xPos); - ret |= Checkbox(set.Option(CustomizationId.LipColor), customize.Lipstick, b => customize.Lipstick = b, locked); - } + d.Checkbox($"{Glamourer.Customization.GetName(CustomName.Reverse)} {d._set.Option(CustomizationId.FacePaint)}", + customize.FacePaintReversed, b => customize.FacePaintReversed = b); + d.Checkbox($"{Glamourer.Customization.GetName(CustomName.IrisSmall)} {Glamourer.Customization.GetName(CustomName.IrisSize)}", + customize.SmallIris, b => customize.SmallIris = b); - return ret; - } - - private static bool DrawRaceGenderSelector(Customize customize, CharacterEquip equip, bool locked) - { - var ret = DrawGenderSelector(customize, equip, locked); - ImGui.SameLine(); - using var group = ImRaii.Group(); - ret |= DrawRaceCombo(customize, equip, locked); - var gender = Glamourer.Customization.GetName(CustomName.Gender); - var clan = Glamourer.Customization.GetName(CustomName.Clan); - ImGui.TextUnformatted($"{gender} & {clan}"); - return ret; - } - - private static bool DrawGenderSelector(Customize customize, CharacterEquip equip, bool locked) - { - using var font = ImRaii.PushFont(UiBuilder.IconFont); - var icon = customize.Gender == Gender.Male ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus; - var restricted = customize.Race == Race.Hrothgar; - if (restricted) - icon = FontAwesomeIcon.MarsDouble; - - if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, restricted || locked, true)) - return false; - - var gender = customize.Gender == Gender.Male ? Gender.Female : Gender.Male; - return false; //customize.ChangeGender(gender, locked ? CharacterEquip.Null : equip); - } - - private static bool DrawRaceCombo(Customize customize, CharacterEquip equip, bool locked) - { - using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked); - ImGui.SetNextItemWidth(_raceSelectorWidth); - using var combo = ImRaii.Combo("##subRaceCombo", customize.ClanName()); - if (!combo) - return false; - - if (locked) - ImGui.CloseCurrentPopup(); - - var ret = false; - foreach (var subRace in Enum.GetValues().Skip(1)) // Skip Unknown - { - if (ImGui.Selectable(CustomizeExtensions.ClanName(subRace, customize.Gender), subRace == customize.Clan)) - ret |= false; //customize.ChangeRace(subRace, equip); - } - - return ret; - } - - private static bool Checkbox(string label, bool current, Action setter, bool locked) - { - var tmp = current; - var ret = false; - using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked); - if (ImGui.Checkbox($"##{label}", ref tmp) && tmp == current && !locked) - { - setter(tmp); - ret = true; - } - - alpha.Pop(); - - ImGui.SameLine(); - ImGui.TextUnformatted(label); - - return ret; - } - - private static bool PercentageSelector(CustomizationSet set, CustomizationId id, Customize customization, bool locked) - { - using var bigGroup = ImRaii.Group(); - using var _ = ImRaii.PushId((int)id); - int value = id == _tempType ? _tempStorage : customization[id]; - var count = set.Count(id); - ImGui.SetNextItemWidth(_comboSelectorSize); - - var (min, max) = locked ? (value, value) : (0, count - 1); - using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked); - if (ImGui.SliderInt("##slider", ref value, min, max, string.Empty, ImGuiSliderFlags.AlwaysClamp) && !locked) - { - _tempStorage = (byte)value; - _tempType = id; - } - - var ret = ImGui.IsItemDeactivatedAfterEdit(); - - ImGui.SameLine(); - ret |= InputInt("##input", id, --value, min, max, locked); - - alpha.Pop(); - - ImGui.SameLine(); - ImGui.TextUnformatted(set.OptionName[(int)id]); - - if (ret) - customization[id] = _tempStorage; - - return ret; - } - - private static bool InputInt(string label, CustomizationId id, int startValue, int minValue, int maxValue, bool locked) - { - var tmp = startValue + 1; - ImGui.SetNextItemWidth(_inputIntSize); - if (ImGui.InputInt(label, ref tmp, 1, 1, ImGuiInputTextFlags.EnterReturnsTrue) - && !locked - && tmp != startValue + 1 - && tmp >= minValue - && tmp <= maxValue) - { - _tempType = id; - _tempStorage = (byte)(tmp - 1); - } - - var ret = ImGui.IsItemDeactivatedAfterEdit() && !locked; - if (!locked) - ImGuiUtil.HoverTooltip($"Input Range: [{minValue}, {maxValue}]"); - return ret; - } - - private static bool DrawIconSelector(CustomizationSet set, CustomizationId id, Customize customize, bool locked) - { - const string popupName = "Style Picker"; - - using var bigGroup = ImRaii.Group(); - using var _ = ImRaii.PushId((int)id); - var count = set.Count(id); - var label = set.Option(id); - - var current = set.DataByValue(id, _tempType == id ? _tempStorage : customize[id], out var custom); - if (current < 0) - { - label = $"{label} (Custom #{customize[id]})"; - current = 0; - custom = set.Data(id, 0); - } - - var icon = Glamourer.Customization.GetIcon(custom!.Value.IconId); - using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked); - if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize) && !locked) - ImGui.OpenPopup(popupName); - - ImGuiUtil.HoverIconTooltip(icon, _iconSize); - - ImGui.SameLine(); - using var group = ImRaii.Group(); - var (min, max) = locked ? (current, current) : (1, count); - var ret = InputInt("##text", id, current, min, max, locked); - if (ret) - customize[id] = set.Data(id, _tempStorage).Value; - - ImGui.TextUnformatted($"{label} ({custom.Value.Value})"); - - ret |= DrawIconPickerPopup(popupName, set, id, customize); - - return ret; - } - - private static bool DrawIconPickerPopup(string label, CustomizationSet set, CustomizationId id, Customize customize) - { - using var popup = ImRaii.Popup(label, ImGuiWindowFlags.AlwaysAutoResize); - if (!popup) - return false; - - var ret = false; - var count = set.Count(id); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) - .Push(ImGuiStyleVar.FrameRounding, 0); - for (var i = 0; i < count; ++i) - { - var custom = set.Data(id, i); - var icon = Glamourer.Customization.GetIcon(custom.IconId); - using var group = ImRaii.Group(); - if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) + if (customize.Race != Race.Hrothgar) { - customize[id] = custom.Value; - ret = true; - ImGui.CloseCurrentPopup(); - } - - ImGuiUtil.HoverIconTooltip(icon, _iconSize); - - var text = custom.Value.ToString(); - var textWidth = ImGui.CalcTextSize(text).X; - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (_iconSize.X - textWidth + 2 * ImGui.GetStyle().FramePadding.X) / 2); - ImGui.TextUnformatted(text); - group.Dispose(); - - if (i % 8 != 7) - ImGui.SameLine(); - } - - return ret; - } - - private static bool DrawColorPicker(CustomizationSet set, CustomizationId id, Customize customize, bool locked) - { - const string popupName = "Color Picker"; - using var _ = ImRaii.PushId((int)id); - var ret = false; - var count = set.Count(id); - var label = set.Option(id); - var (current, custom) = GetCurrentCustomization(set, id, customize); - - using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked); - if (ImGui.ColorButton($"{current + 1}##color", ImGui.ColorConvertU32ToFloat4(custom.Color), ImGuiColorEditFlags.None, _framedIconSize) - && !locked) - ImGui.OpenPopup(popupName); - - ImGui.SameLine(); - - using (var group = ImRaii.Group()) - { - var (min, max) = locked ? (current, current) : (1, count); - if (InputInt("##text", id, current, min, max, locked)) - { - customize[id] = set.Data(id, current).Value; - ret = true; + ImGui.SameLine(xPos); + d.Checkbox(d._set.Option(CustomizationId.LipColor), customize.Lipstick, b => customize.Lipstick = b); + } + } + + private void DrawRaceGenderSelector() + { + DrawGenderSelector(); + ImGui.SameLine(); + using var group = ImRaii.Group(); + DrawRaceCombo(); + var gender = Glamourer.Customization.GetName(CustomName.Gender); + var clan = Glamourer.Customization.GetName(CustomName.Clan); + ImGui.TextUnformatted($"{gender} & {clan}"); + } + + private void DrawGenderSelector() + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + var icon = _customize.Gender == Gender.Male ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus; + var restricted = _customize.Race == Race.Hrothgar; + if (restricted) + icon = FontAwesomeIcon.MarsDouble; + + if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, restricted, true)) + return; + + var gender = _customize.Gender == Gender.Male ? Gender.Female : Gender.Male; + if (!_customize.ChangeGender(_equip, gender)) + return; + + foreach (var actor in _actors.Where(a => a)) + Glamourer.Penumbra.RedrawObject(actor.Character, RedrawType.Redraw, false); + } + + private void DrawRaceCombo() + { + ImGui.SetNextItemWidth(_raceSelectorWidth); + using var combo = ImRaii.Combo("##subRaceCombo", _customize.ClanName()); + if (!combo) + return; + + foreach (var subRace in Enum.GetValues().Skip(1)) // Skip Unknown + { + if (ImGui.Selectable(CustomizeExtensions.ClanName(subRace, _customize.Gender), subRace == _customize.Clan) + && _customize.ChangeRace(_equip, subRace)) + foreach (var actor in _actors.Where(a => a && a.DrawObject)) + Glamourer.Penumbra.RedrawObject(actor.Character, RedrawType.Redraw, false); + } + } + + private void Checkbox(string label, bool current, Action setter) + { + var tmp = current; + if (ImGui.Checkbox($"##{label}", ref tmp) && tmp != current) + { + setter(tmp); + foreach (var actor in _actors.Where(a => a && a.DrawObject)) + Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); } + ImGui.SameLine(); ImGui.TextUnformatted(label); } - return ret | DrawColorPickerPopup(popupName, set, id, customize); - } - - private static (int, Customization.Customization) GetCurrentCustomization(CustomizationSet set, CustomizationId id, - Customize customize) - { - var current = set.DataByValue(id, customize[id], out var custom); - if (set.IsAvailable(id) && current < 0) + private void PercentageSelector(CustomizationId id) { - PluginLog.Warning($"Read invalid customization value {customize[id]} for {id}."); - current = 0; - custom = set.Data(id, 0); - } + using var bigGroup = ImRaii.Group(); + using var _ = ImRaii.PushId((int)id); + int value = _customize[id]; + var count = _set.Count(id); + ImGui.SetNextItemWidth(_comboSelectorSize); - return (current, custom!.Value); - } - - private static bool DrawColorPickerPopup(string label, CustomizationSet set, CustomizationId id, Customize customize) - { - using var popup = ImRaii.Popup(label, ImGuiWindowFlags.AlwaysAutoResize); - if (!popup) - return false; - - var ret = false; - var count = set.Count(id); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) - .Push(ImGuiStyleVar.FrameRounding, 0); - for (var i = 0; i < count; ++i) - { - var custom = set.Data(id, i); - if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color))) + void OnChange(int v) { - customize[id] = custom.Value; - ret = true; - ImGui.CloseCurrentPopup(); + _customize[id] = (byte)v; + foreach (var actor in _actors.Where(a => a && a.DrawObject)) + Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); } - if (i % 8 != 7) - ImGui.SameLine(); + if (ImGui.SliderInt("##slider", ref value, 0, count - 1, "%i", ImGuiSliderFlags.AlwaysClamp)) + OnChange(value); + + ImGui.SameLine(); + InputInt("##input", --value, 0, count - 1, OnChange); + + ImGui.SameLine(); + ImGui.TextUnformatted(_set.OptionName[(int)id]); } - return ret; - } - - private static bool DrawMultiIconSelector(CustomizationSet set, Customize customize, bool locked) - { - using var bigGroup = ImRaii.Group(); - using var _ = ImRaii.PushId((int)CustomizationId.FacialFeaturesTattoos); - using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked); - var ret = DrawMultiIcons(set, customize, locked); - ImGui.SameLine(); - using var group = ImRaii.Group(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() + 3 * ImGui.GetStyle().ItemSpacing.Y / 2); - int value = customize[CustomizationId.FacialFeaturesTattoos]; - var (min, max) = locked ? (value, value) : (1, 256); - if (InputInt(string.Empty, CustomizationId.FacialFeaturesTattoos, value, min, max, locked)) + private static void InputInt(string label, int startValue, int minValue, int maxValue, Action setter) { - customize[CustomizationId.FacialFeaturesTattoos] = (byte)value; - ret = true; + var tmp = startValue + 1; + ImGui.SetNextItemWidth(_inputIntSize); + if (ImGui.InputInt(label, ref tmp, 1, 1, ImGuiInputTextFlags.EnterReturnsTrue) + && tmp != startValue + 1 + && tmp >= minValue + && tmp <= maxValue) + setter(tmp); + + ImGuiUtil.HoverTooltip($"Input Range: [{minValue}, {maxValue}]"); } - ImGui.TextUnformatted(set.Option(CustomizationId.FacialFeaturesTattoos)); - - return ret; - } - - private static bool DrawMultiIcons(CustomizationSet set, Customize customize, bool locked) - { - using var _ = ImRaii.Group(); - var face = customize.Face; - if (set.Faces.Count < face) - face = 1; - - var ret = false; - var count = set.Count(CustomizationId.FacialFeaturesTattoos); - for (var i = 0; i < count; ++i) + private void DrawIconSelector(CustomizationId id) { - var enabled = customize.FacialFeatures[i]; - var feature = set.FacialFeature(face, i); - var icon = i == count - 1 - ? LegacyTattoo ?? Glamourer.Customization.GetIcon(feature.IconId) - : Glamourer.Customization.GetIcon(feature.IconId); - if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X, - Vector4.Zero, enabled ? Vector4.One : RedTint) - && !locked) + const string popupName = "Style Picker"; + + using var bigGroup = ImRaii.Group(); + using var _ = ImRaii.PushId((int)id); + var count = _set.Count(id, _customize.Face); + var label = _set.Option(id); + + var current = _set.DataByValue(id, _customize[id], out var custom); + if (current < 0) { - customize.FacialFeatures.Set(i, !enabled); - ret = true; + label = $"{label} (Custom #{_customize[id]})"; + current = 0; + custom = _set.Data(id, 0); } - using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 1f, !locked); + var icon = Glamourer.Customization.GetIcon(custom!.Value.IconId); + if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) + ImGui.OpenPopup(popupName); + ImGuiUtil.HoverIconTooltip(icon, _iconSize); - if (i % 4 != 3) - ImGui.SameLine(); + void OnChange(int v) + { + var value = _set.Data(id, v - 1).Value; + // Hrothgar hack + if (_set.Race == Race.Hrothgar && id == CustomizationId.Face) + value += 4; + + if (_customize[id] == value) + return; + + _customize[id] = value; + foreach (var actor in _actors.Where(a => a && a.DrawObject)) + Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); + } + + ImGui.SameLine(); + using var group = ImRaii.Group(); + InputInt("##text", current, 1, count, OnChange); + + ImGui.TextUnformatted($"{label} ({custom.Value.Value})"); + + DrawIconPickerPopup(popupName, id, OnChange); } - return ret; - } - - private static bool DrawListSelector(CustomizationSet set, CustomizationId id, Customize customize, bool locked) - { - using var _ = ImRaii.PushId((int)id); - using var bigGroup = ImRaii.Group(); - var ret = false; - int current = customize[id]; - var count = set.Count(id); - - ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale); - using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked); - using (var combo = ImRaii.Combo("##combo", $"{set.Option(id)} #{current + 1}")) + private void DrawIconPickerPopup(string label, CustomizationId id, Action setter) { - if (combo) - for (var i = 0; i < count; ++i) + using var popup = ImRaii.Popup(label, ImGuiWindowFlags.AlwaysAutoResize); + if (!popup) + return; + + var count = _set.Count(id, _customize.Face); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameRounding, 0); + for (var i = 0; i < count; ++i) + { + var custom = _set.Data(id, i, _customize.Face); + var icon = Glamourer.Customization.GetIcon(custom.IconId); + using var group = ImRaii.Group(); + if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) { - if (!ImGui.Selectable($"{set.Option(id)} #{i + 1}##combo", i == current) || i == current || locked) - continue; - - customize[id] = (byte)i; - ret = true; + setter(custom.Value); + ImGui.CloseCurrentPopup(); } + + ImGuiUtil.HoverIconTooltip(icon, _iconSize); + + var text = custom.Value.ToString(); + var textWidth = ImGui.CalcTextSize(text).X; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (_iconSize.X - textWidth + 2 * ImGui.GetStyle().FramePadding.X) / 2); + ImGui.TextUnformatted(text); + group.Dispose(); + + if (i % 8 != 7) + ImGui.SameLine(); + } } - ImGui.SameLine(); - var (min, max) = locked ? (current, current) : (1, count); - if (InputInt("##text", id, current, min, max, locked)) + private void DrawColorPicker(CustomizationId id) { - customize[id] = (byte)current; - ret = true; + const string popupName = "Color Picker"; + using var _ = ImRaii.PushId((int)id); + var count = _set.Count(id); + var label = _set.Option(id); + var (current, custom) = GetCurrentCustomization(id); + + if (ImGui.ColorButton($"{current + 1}##color", ImGui.ColorConvertU32ToFloat4(custom.Color), ImGuiColorEditFlags.None, + _framedIconSize)) + ImGui.OpenPopup(popupName); + + ImGui.SameLine(); + + void OnChange(int v) + { + _customize[id] = _set.Data(id, v).Value; + foreach (var actor in _actors.Where(a => a && a.DrawObject)) + Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); + } + + using (var group = ImRaii.Group()) + { + InputInt("##text", current, 1, count, OnChange); + ImGui.TextUnformatted(label); + } + + DrawColorPickerPopup(popupName, id, OnChange); } - ImGui.SameLine(); - alpha.Pop(); - ImGui.TextUnformatted(set.Option(id)); + private (int, Customization.Customization) GetCurrentCustomization(CustomizationId id) + { + var current = _set.DataByValue(id, _customize[id], out var custom); + if (_set.IsAvailable(id) && current < 0) + { + PluginLog.Warning($"Read invalid customization value {_customize[id]} for {id}."); + current = 0; + custom = _set.Data(id, 0); + } - return ret; + return (current, custom!.Value); + } + + private void DrawColorPickerPopup(string label, CustomizationId id, Action setter) + { + using var popup = ImRaii.Popup(label, ImGuiWindowFlags.AlwaysAutoResize); + if (!popup) + return; + + var count = _set.Count(id); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameRounding, 0); + for (var i = 0; i < count; ++i) + { + var custom = _set.Data(id, i); + if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color))) + { + setter(custom.Value); + ImGui.CloseCurrentPopup(); + } + + if (i % 8 != 7) + ImGui.SameLine(); + } + } + + private void DrawMultiIconSelector() + { + using var bigGroup = ImRaii.Group(); + using var _ = ImRaii.PushId((int)CustomizationId.FacialFeaturesTattoos); + + void OnChange(int v) + { + _customize[CustomizationId.FacialFeaturesTattoos] = (byte)v; + foreach (var actor in _actors.Where(a => a && a.DrawObject)) + Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); + } + + DrawMultiIcons(); + ImGui.SameLine(); + int value = _customize[CustomizationId.FacialFeaturesTattoos]; + using var group = ImRaii.Group(); + ImGui.Dummy(new Vector2(0, ImGui.GetTextLineHeightWithSpacing() + ImGui.GetStyle().ItemSpacing.Y / 2)); + InputInt(string.Empty, --value, 0, 255, OnChange); + + ImGui.TextUnformatted(_set.Option(CustomizationId.FacialFeaturesTattoos)); + } + + private void DrawMultiIcons() + { + using var _ = ImRaii.Group(); + var face = _customize.Face; + + var ret = false; + var count = _set.Count(CustomizationId.FacialFeaturesTattoos); + for (var i = 0; i < count; ++i) + { + var enabled = _customize.FacialFeatures[i]; + var feature = _set.FacialFeature(face, i); + var icon = i == count - 1 + ? LegacyTattoo ?? Glamourer.Customization.GetIcon(feature.IconId) + : Glamourer.Customization.GetIcon(feature.IconId); + if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X, + Vector4.Zero, enabled ? Vector4.One : RedTint)) + { + _customize.FacialFeatures.Set(i, !enabled); + foreach (var actor in _actors.Where(a => a && a.DrawObject)) + Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); + } + + ImGuiUtil.HoverIconTooltip(icon, _iconSize); + if (i % 4 != 3) + ImGui.SameLine(); + } + } + + private void DrawListSelector(CustomizationId id) + { + using var _ = ImRaii.PushId((int)id); + using var bigGroup = ImRaii.Group(); + int current = _customize[id]; + var count = _set.Count(id); + + void OnChange(int v) + { + _customize[id] = (byte)v; + foreach (var actor in _actors.Where(a => a && a.DrawObject)) + Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize); + } + + ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale); + using (var combo = ImRaii.Combo("##combo", $"{_set.Option(id)} #{current + 1}")) + { + if (combo) + for (var i = 0; i < count; ++i) + { + if (!ImGui.Selectable($"{_set.Option(id)} #{i + 1}##combo", i == current) || i == current) + continue; + + OnChange(i); + } + } + + ImGui.SameLine(); + InputInt("##text", current, 1, count, OnChange); + + ImGui.SameLine(); + ImGui.TextUnformatted(_set.Option(id)); + } } } diff --git a/Glamourer/Gui/Interface.SettingsTab.cs b/Glamourer/Gui/Interface.SettingsTab.cs index ecb8a5b..1cf6be8 100644 --- a/Glamourer/Gui/Interface.SettingsTab.cs +++ b/Glamourer/Gui/Interface.SettingsTab.cs @@ -1,5 +1,6 @@ using System; using System.Numerics; +using Glamourer.State; using ImGuiNET; using OtterGui; using OtterGui.Raii; diff --git a/Glamourer/Gui/Interface.State.cs b/Glamourer/Gui/Interface.State.cs index 4e5dcde..1639135 100644 --- a/Glamourer/Gui/Interface.State.cs +++ b/Glamourer/Gui/Interface.State.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Reflection; using Dalamud.Interface; using Glamourer.Customization; +using Glamourer.Interop; using ImGuiNET; namespace Glamourer.Gui; @@ -20,13 +21,11 @@ internal partial class Interface private static float _inputIntSize; private static float _comboSelectorSize; private static float _raceSelectorWidth; - private static bool _inGPose; private static void UpdateState() { // General - _inGPose = ObjectManager.IsInGPose(); _spacing = _spacing with { Y = ImGui.GetTextLineHeightWithSpacing() / 2 }; _actorSelectorWidth = 200 * ImGuiHelpers.GlobalScale; diff --git a/Glamourer/Gui/Interface.cs b/Glamourer/Gui/Interface.cs index f257b3c..77478f7 100644 --- a/Glamourer/Gui/Interface.cs +++ b/Glamourer/Gui/Interface.cs @@ -1,17 +1,108 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Numerics; using Dalamud.Interface.Windowing; using Dalamud.Logging; +using Glamourer.Interop; +using Glamourer.State; using ImGuiNET; +using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; -using OtterGui.Widgets; namespace Glamourer.Gui; internal partial class Interface : Window, IDisposable { + private class DebugStateTab + { + private readonly CurrentManipulations _currentManipulations; + + private LowerString _manipulationFilter = LowerString.Empty; + private Actor.IIdentifier _selection = Actor.IIdentifier.Invalid; + private CurrentDesign? _save = null; + private bool _delete = false; + + public DebugStateTab(CurrentManipulations currentManipulations) + => _currentManipulations = currentManipulations; + + public void Draw() + { + using var tab = ImRaii.TabItem("Current Manipulations"); + if (!tab) + return; + + DrawManipulationSelector(); + if (_save == null) + return; + + ImGui.SameLine(); + DrawActorPanel(); + if (_delete) + { + _delete = false; + _currentManipulations.DeleteSave(_selection); + _selection = Actor.IIdentifier.Invalid; + } + } + + private void DrawSelector(Vector2 oldSpacing) + { + using var child = ImRaii.Child("##actorSelector", new Vector2(_actorSelectorWidth, -1), true); + if (!child) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, oldSpacing); + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight()); + var remainder = ImGuiClip.FilteredClippedDraw(_currentManipulations, skips, CheckFilter, DrawSelectable); + ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight()); + } + + private void DrawManipulationSelector() + { + using var group = ImRaii.Group(); + var oldSpacing = ImGui.GetStyle().ItemSpacing; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameRounding, 0); + ImGui.SetNextItemWidth(_actorSelectorWidth); + LowerString.InputWithHint("##actorFilter", "Filter...", ref _manipulationFilter, 64); + + _save = null; + DrawSelector(oldSpacing); + } + + private bool CheckFilter(KeyValuePair data) + { + if (data.Key.Equals(_selection)) + _save = data.Value; + return _manipulationFilter.Length == 0 || _manipulationFilter.IsContained(data.Key.ToString()!); + } + + private void DrawSelectable(KeyValuePair data) + { + var equal = data.Key.Equals(_selection); + if (ImGui.Selectable(data.Key.ToString(), equal)) + { + _selection = data.Key; + _save = data.Value; + } + } + + private void DrawActorPanel() + { + using var group = ImRaii.Group(); + if (ImGui.Button("Delete")) + _delete = true; + CustomizationDrawer.Draw(_save!.Data.Customize, _save.Data.Equipment, Array.Empty(), false); + } + } + private readonly Glamourer _plugin; + private readonly ActorTab _actorTab; + private readonly DebugStateTab _debugStateTab; + public Interface(Glamourer plugin) : base(GetLabel()) { @@ -23,6 +114,8 @@ internal partial class Interface : Window, IDisposable MinimumSize = new Vector2(675, 675), MaximumSize = ImGui.GetIO().DisplaySize, }; + _actorTab = new ActorTab(_plugin.CurrentManipulations); + _debugStateTab = new DebugStateTab(_plugin.CurrentManipulations); } public override void Draw() @@ -31,13 +124,21 @@ internal partial class Interface : Window, IDisposable if (!tabBar) return; - UpdateState(); + try + { + UpdateState(); - _actorTab.Draw(); - DrawSettingsTab(); - // DrawSaves(); - // DrawFixedDesignsTab(); - // DrawRevertablesTab(); + _actorTab.Draw(); + DrawSettingsTab(); + _debugStateTab.Draw(); + // DrawSaves(); + // DrawFixedDesignsTab(); + // DrawRevertablesTab(); + } + catch (Exception e) + { + PluginLog.Error($"Unexpected Error during Draw:\n{e}"); + } } public void Dispose() diff --git a/Glamourer/Gui/InterfaceActorPanel.cs b/Glamourer/Gui/InterfaceActorPanel.cs index ad377da..34042bf 100644 --- a/Glamourer/Gui/InterfaceActorPanel.cs +++ b/Glamourer/Gui/InterfaceActorPanel.cs @@ -9,8 +9,7 @@ internal partial class Interface //private bool _holdShift; //private bool _holdCtrl; //private const string DesignNamePopupLabel = "Save Design As..."; - //private const uint RedHeaderColor = 0xFF1818C0; - //private const uint GreenHeaderColor = 0xFF18C018; + // //private void DrawPlayerHeader() //{ diff --git a/Glamourer/Interop/Actor.Identifier.cs b/Glamourer/Interop/Actor.Identifier.cs new file mode 100644 index 0000000..898de52 --- /dev/null +++ b/Glamourer/Interop/Actor.Identifier.cs @@ -0,0 +1,350 @@ +using System; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.ByteString; + +namespace Glamourer.Interop; + +public unsafe partial struct Actor +{ + public interface IIdentifier : IEquatable + { + Utf8String Name { get; } + public bool IsValid { get; } + + public IIdentifier CreatePermanent(); + + public static readonly InvalidIdentifier Invalid = new(); + + public void ToJson(JsonTextWriter j); + + public static IIdentifier? FromJson(JObject j) + { + switch (j["Type"]?.Value() ?? string.Empty) + { + case nameof(PlayerIdentifier): + { + var name = j[nameof(Name)]?.Value(); + if (name.IsNullOrEmpty()) + return null; + + var serverId = j[nameof(PlayerIdentifier.HomeWorld)]?.Value() ?? ushort.MaxValue; + return new PlayerIdentifier(Utf8String.FromStringUnsafe(name, false), serverId); + } + case nameof(SpecialIdentifier): + { + var index = j[nameof(SpecialIdentifier.Index)]?.Value() ?? ushort.MaxValue; + return new SpecialIdentifier(index); + } + case nameof(OwnedIdentifier): + { + var name = j[nameof(Name)]?.Value(); + if (name.IsNullOrEmpty()) + return null; + + var ownerName = j[nameof(OwnedIdentifier.OwnerName)]?.Value(); + if (ownerName.IsNullOrEmpty()) + return null; + + var ownerHomeWorld = j[nameof(OwnedIdentifier.OwnerHomeWorld)]?.Value() ?? ushort.MaxValue; + var dataId = j[nameof(OwnedIdentifier.DataId)]?.Value() ?? ushort.MaxValue; + var kind = j[nameof(OwnedIdentifier.Kind)]?.Value() ?? ObjectKind.Player; + + return new OwnedIdentifier(Utf8String.FromStringUnsafe(name, false), Utf8String.FromStringUnsafe(ownerName, false), + ownerHomeWorld, dataId, kind); + } + case nameof(NpcIdentifier): + { + var name = j[nameof(Name)]?.Value(); + if (name.IsNullOrEmpty()) + return null; + + var dataId = j[nameof(NpcIdentifier.DataId)]?.Value() ?? uint.MaxValue; + + return new NpcIdentifier(Utf8String.FromStringUnsafe(name, false), ushort.MaxValue, dataId); + } + default: return null; + } + } + } + + public class InvalidIdentifier : IIdentifier + { + public Utf8String Name + => Utf8String.Empty; + + public bool IsValid + => false; + + public bool Equals(IIdentifier? other) + => false; + + public override int GetHashCode() + => 0; + + public override string ToString() + => "Invalid"; + + public IIdentifier CreatePermanent() + => this; + + public void ToJson(JsonTextWriter j) + { } + } + + public class PlayerIdentifier : IIdentifier, IEquatable + { + public Utf8String Name { get; } + public readonly ushort HomeWorld; + + public bool IsValid + => true; + + public PlayerIdentifier(Utf8String name, ushort homeWorld) + { + Name = name; + HomeWorld = homeWorld; + } + + public bool Equals(IIdentifier? other) + => Equals(other as PlayerIdentifier); + + public bool Equals(PlayerIdentifier? other) + => other?.HomeWorld == HomeWorld && other.Name.Equals(Name); + + public override int GetHashCode() + => HashCode.Combine(Name.Crc32, HomeWorld); + + public override string ToString() + => $"{Name} ({HomeWorld})"; + + public IIdentifier CreatePermanent() + => new PlayerIdentifier(Name.Clone(), HomeWorld); + + public void ToJson(JsonTextWriter j) + { + j.WriteStartObject(); + j.WritePropertyName("Type"); + j.WriteValue(GetType().Name); + j.WritePropertyName(nameof(Name)); + j.WriteValue(Name); + j.WritePropertyName(nameof(HomeWorld)); + j.WriteValue(HomeWorld); + j.WriteEndObject(); + } + } + + public class SpecialIdentifier : IIdentifier, IEquatable + { + public Utf8String Name + => Utf8String.Empty; + + public readonly ushort Index; + + public bool IsValid + => true; + + public SpecialIdentifier(ushort index) + => Index = index; + + public bool Equals(IIdentifier? other) + => Equals(other as SpecialIdentifier); + + public bool Equals(SpecialIdentifier? other) + => other?.Index == Index; + + public override int GetHashCode() + => Index; + + public override string ToString() + => $"Special Actor {Index}"; + + public IIdentifier CreatePermanent() + => this; + + public void ToJson(JsonTextWriter j) + { + j.WriteStartObject(); + j.WritePropertyName("Type"); + j.WriteValue(GetType().Name); + j.WritePropertyName(nameof(Index)); + j.WriteValue(Index); + j.WriteEndObject(); + } + } + + + public class OwnedIdentifier : IIdentifier, IEquatable + { + public Utf8String Name { get; } + public readonly Utf8String OwnerName; + public readonly uint DataId; + public readonly ushort OwnerHomeWorld; + public readonly ObjectKind Kind; + + public bool IsValid + => true; + + public OwnedIdentifier(Utf8String name, Utf8String ownerName, ushort ownerHomeWorld, uint dataId, ObjectKind kind) + { + Name = name; + OwnerName = ownerName; + OwnerHomeWorld = ownerHomeWorld; + DataId = dataId; + Kind = kind; + } + + public bool Equals(IIdentifier? other) + => Equals(other as OwnedIdentifier); + + public bool Equals(OwnedIdentifier? other) + => other?.DataId == DataId + && other.OwnerHomeWorld == OwnerHomeWorld + && other.Kind == Kind + && other.OwnerName.Equals(OwnerName); + + + public override int GetHashCode() + => HashCode.Combine(OwnerName.Crc32, OwnerHomeWorld, DataId, Kind); + + public override string ToString() + => $"{OwnerName}s {Name}"; + + public IIdentifier CreatePermanent() + => new OwnedIdentifier(Name.Clone(), OwnerName.Clone(), OwnerHomeWorld, DataId, Kind); + + public void ToJson(JsonTextWriter j) + { + j.WriteStartObject(); + j.WritePropertyName("Type"); + j.WriteValue(GetType().Name); + j.WritePropertyName(nameof(Name)); + j.WriteValue(Name); + j.WritePropertyName(nameof(OwnerName)); + j.WriteValue(OwnerName); + j.WritePropertyName(nameof(OwnerHomeWorld)); + j.WriteValue(OwnerHomeWorld); + j.WritePropertyName(nameof(Kind)); + j.WriteValue(Kind); + j.WritePropertyName(nameof(DataId)); + j.WriteValue(DataId); + j.WriteEndObject(); + } + } + + public class NpcIdentifier : IIdentifier, IEquatable + { + public Utf8String Name { get; } + public readonly uint DataId; + public readonly ushort ObjectIndex; + + public bool IsValid + => true; + + public NpcIdentifier(Utf8String actorName, ushort objectIndex = ushort.MaxValue, uint dataId = uint.MaxValue) + { + Name = actorName; + ObjectIndex = objectIndex; + DataId = dataId; + } + + public bool Equals(IIdentifier? other) + => Equals(other as NpcIdentifier); + + public bool Equals(NpcIdentifier? other) + => (other?.Name.Equals(Name) ?? false) + && (other.DataId == uint.MaxValue || DataId == uint.MaxValue || other.DataId == DataId) + && (other.ObjectIndex == ushort.MaxValue || ObjectIndex == ushort.MaxValue || other.ObjectIndex == ObjectIndex); + + public override int GetHashCode() + => Name.Crc32; + + public override string ToString() + => DataId == uint.MaxValue ? ObjectIndex == ushort.MaxValue ? Name.ToString() : $"{Name} at {ObjectIndex}" : + ObjectIndex == ushort.MaxValue ? $"{Name} ({DataId})" : $"{Name} ({DataId}) at {ObjectIndex}"; + + public IIdentifier CreatePermanent() + => new NpcIdentifier(Name.Clone(), ObjectIndex, DataId); + + public void ToJson(JsonTextWriter j) + { + j.WriteStartObject(); + j.WritePropertyName("Type"); + j.WriteValue(GetType().Name); + j.WritePropertyName(nameof(Name)); + j.WriteValue(Name); + j.WritePropertyName(nameof(DataId)); + j.WriteValue(DataId); + j.WriteEndObject(); + } + } + + private static IIdentifier CreateIdentifier(Actor actor) + { + if (!actor.Valid) + return IIdentifier.Invalid; + + var objectIdx = actor.Pointer->GameObject.ObjectIndex; + if (objectIdx is >= 200 and < 240) + { + var parentIdx = Glamourer.Penumbra.CutsceneParent(objectIdx); + if (parentIdx >= 0) + { + var parent = (Actor)Dalamud.Objects.GetObjectAddress(parentIdx); + if (!parent) + return IIdentifier.Invalid; + + return CreateIdentifier(parent); + } + } + + switch (actor.ObjectKind) + { + case ObjectKind.Player: + { + var name = actor.Utf8Name; + if (name.Length > 0 && actor.Pointer->HomeWorld is > 0 and < ushort.MaxValue) + return new PlayerIdentifier(actor.Utf8Name, actor.Pointer->HomeWorld); + + return IIdentifier.Invalid; + } + case ObjectKind.BattleNpc: + { + var ownerId = actor.Pointer->GameObject.OwnerID; + if (ownerId != 0xE0000000) + { + var owner = (Actor)Dalamud.Objects.SearchById(ownerId)?.Address; + if (!owner) + return new InvalidIdentifier(); + + return new OwnedIdentifier(actor.Utf8Name, owner.Utf8Name, owner.Pointer->HomeWorld, + actor.Pointer->GameObject.DataID, ObjectKind.BattleNpc); + } + + return new NpcIdentifier(actor.Utf8Name, actor.Pointer->GameObject.ObjectIndex, + actor.Pointer->GameObject.DataID); + } + case ObjectKind.Retainer: + case ObjectKind.EventNpc: + return new NpcIdentifier(actor.Utf8Name, actor.Pointer->GameObject.ObjectIndex, + actor.Pointer->GameObject.DataID); + case ObjectKind.MountType: + case ObjectKind.Companion: + { + var idx = actor.Pointer->GameObject.ObjectIndex; + if (idx % 2 == 0) + return new InvalidIdentifier(); + + var owner = (Actor)Dalamud.Objects[idx - 1]?.Address; + if (!owner) + return new InvalidIdentifier(); + + return new OwnedIdentifier(actor.Utf8Name, owner.Utf8Name, owner.Pointer->HomeWorld, + actor.Pointer->GameObject.DataID, actor.ObjectKind); + } + default: return new InvalidIdentifier(); + } + } +} diff --git a/Glamourer/Interop/Actor.cs b/Glamourer/Interop/Actor.cs new file mode 100644 index 0000000..214ac52 --- /dev/null +++ b/Glamourer/Interop/Actor.cs @@ -0,0 +1,232 @@ +using System; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Glamourer.Customization; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop; + +public interface IDesignable +{ + public bool Valid { get; } + public uint ModelId { get; } + public Customize Customize { get; } + public CharacterEquip Equip { get; } + public CharacterWeapon MainHand { get; } + public CharacterWeapon OffHand { get; } + public bool VisorEnabled { get; } + public bool WeaponEnabled { get; } +} + +public unsafe partial struct DrawObject : IEquatable, IDesignable +{ + public Human* Pointer; + + public IntPtr Address + => (IntPtr)Pointer; + + public static implicit operator DrawObject(IntPtr? pointer) + => new() { Pointer = (Human*)(pointer ?? IntPtr.Zero) }; + + public static implicit operator IntPtr(DrawObject drawObject) + => drawObject.Pointer == null ? IntPtr.Zero : (IntPtr)drawObject.Pointer; + + public bool Valid + => Pointer != null; + + public uint ModelId + => 0; + + public uint Type + => (*(delegate* unmanaged**)Pointer)[50](Pointer); + + public Customize Customize + => new((CustomizeData*)Pointer->CustomizeData); + + public CharacterEquip Equip + => new((CharacterArmor*)Pointer->EquipSlotData); + + public unsafe CharacterWeapon MainHand + => CharacterWeapon.Empty; + + public unsafe CharacterWeapon OffHand + => CharacterWeapon.Empty; + + public unsafe bool VisorEnabled + => (*(byte*)(Address + 0x90) & 0x40) != 0; + + public unsafe bool WeaponEnabled + => false; + + public static implicit operator bool(DrawObject actor) + => actor.Pointer != null; + + public static bool operator true(DrawObject actor) + => actor.Pointer != null; + + public static bool operator false(DrawObject actor) + => actor.Pointer == null; + + public static bool operator !(DrawObject actor) + => actor.Pointer == null; + + public bool Equals(DrawObject other) + => Pointer == other.Pointer; + + public override bool Equals(object? obj) + => obj is DrawObject other && Equals(other); + + public override int GetHashCode() + => unchecked((int)(long)Pointer); + + public static bool operator ==(DrawObject lhs, DrawObject rhs) + => lhs.Pointer == rhs.Pointer; + + public static bool operator !=(DrawObject lhs, DrawObject rhs) + => lhs.Pointer != rhs.Pointer; +} + +public unsafe partial struct Actor : IEquatable, IDesignable +{ + public static readonly Actor Null = new() { Pointer = null }; + + public FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Pointer; + + public IntPtr Address + => (IntPtr)Pointer; + + public static implicit operator Actor(IntPtr? pointer) + => new() { Pointer = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)(pointer ?? IntPtr.Zero) }; + + public static implicit operator IntPtr(Actor actor) + => actor.Pointer == null ? IntPtr.Zero : (IntPtr)actor.Pointer; + + public IIdentifier GetIdentifier() + => CreateIdentifier(this); + + public bool Identifier(out IIdentifier ident) + { + if (Valid) + { + ident = GetIdentifier(); + return true; + } + ident = IIdentifier.Invalid; + return false; + } + + public Character? Character + => Pointer == null ? null : Dalamud.Objects[Pointer->GameObject.ObjectIndex] as Character; + + public bool IsAvailable + => Pointer->GameObject.GetIsTargetable(); + + public bool IsHuman + => Pointer != null && Pointer->ModelCharaId == 0; + + public ObjectKind ObjectKind + { + get => (ObjectKind)Pointer->GameObject.ObjectKind; + set => Pointer->GameObject.ObjectKind = (byte)value; + } + + public Utf8String Utf8Name + => new(Pointer->GameObject.Name); + + public byte Job + => Pointer->ClassJob; + + public DrawObject DrawObject + => (IntPtr)Pointer->GameObject.DrawObject; + + public bool Valid + => Pointer != null; + + public uint ModelId + { + get => (uint)Pointer->ModelCharaId; + set => Pointer->ModelCharaId = (int)value; + } + + public Customize Customize + => new((CustomizeData*)Pointer->CustomizeData); + + public CharacterEquip Equip + => new((CharacterArmor*)Pointer->EquipSlotData); + + public unsafe CharacterWeapon MainHand + { + get => *(CharacterWeapon*)(Address + 0x06C0 + 0x10); + set => *(CharacterWeapon*)(Address + 0x06C0 + 0x10) = value; + } + + public unsafe CharacterWeapon OffHand + { + get => *(CharacterWeapon*)(Address + 0x06C0 + 0x10 + 0x68); + set => *(CharacterWeapon*)(Address + 0x06C0 + 0x10 + 0x68) = value; + } + + public unsafe bool VisorEnabled + { + get => (*(byte*)(Address + Offsets.Character.VisorToggled) & Offsets.Character.Flags.IsVisorToggled) != 0; + set => *(byte*)(Address + Offsets.Character.VisorToggled) = (byte)(value + ? *(byte*)(Address + Offsets.Character.VisorToggled) | Offsets.Character.Flags.IsVisorToggled + : *(byte*)(Address + Offsets.Character.VisorToggled) & ~Offsets.Character.Flags.IsVisorToggled); + } + + public unsafe bool WeaponEnabled + { + get => (*(byte*)(Address + Offsets.Character.WeaponHidden1) & Offsets.Character.Flags.IsWeaponHidden1) == 0; + set + { + ref var w1 = ref *(byte*)(Address + Offsets.Character.WeaponHidden1); + ref var w2 = ref *(byte*)(Address + Offsets.Character.WeaponHidden2); + if (value) + { + w1 = (byte)(w1 & ~Offsets.Character.Flags.IsWeaponHidden1); + w2 = (byte)(w2 & ~Offsets.Character.Flags.IsWeaponHidden2); + } + else + { + w1 = (byte)(w1 | Offsets.Character.Flags.IsWeaponHidden1); + w2 = (byte)(w2 | Offsets.Character.Flags.IsWeaponHidden2); + } + } + } + + + public void SetModelId(int value) + { + if (Pointer != null) + Pointer->ModelCharaId = value; + } + + public static implicit operator bool(Actor actor) + => actor.Pointer != null; + + public static bool operator true(Actor actor) + => actor.Pointer != null; + + public static bool operator false(Actor actor) + => actor.Pointer == null; + + public static bool operator !(Actor actor) + => actor.Pointer == null; + + public bool Equals(Actor other) + => Pointer == other.Pointer; + + public override bool Equals(object? obj) + => obj is Actor other && Equals(other); + + public override int GetHashCode() + => ((ulong)Pointer).GetHashCode(); + + public static bool operator ==(Actor lhs, Actor rhs) + => lhs.Pointer == rhs.Pointer; + + public static bool operator !=(Actor lhs, Actor rhs) + => lhs.Pointer != rhs.Pointer; +} diff --git a/Glamourer/Interop/ObjectManager.cs b/Glamourer/Interop/ObjectManager.cs new file mode 100644 index 0000000..6b85ae8 --- /dev/null +++ b/Glamourer/Interop/ObjectManager.cs @@ -0,0 +1,193 @@ +using System.Collections.Generic; +using System.Text; +using Dalamud.Game.ClientState.Objects.Enums; +using Lumina.Excel.GeneratedSheets; +using static Glamourer.Interop.Actor; + +namespace Glamourer.Interop; + +public static class ObjectManager +{ + private const int CutsceneIndex = 200; + private const int GPosePlayerIndex = 201; + private const int CharacterScreenIndex = 240; + private const int ExamineScreenIndex = 241; + private const int FittingRoomIndex = 242; + private const int DyePreviewIndex = 243; + private const int PortraitIndex = 244; + + public readonly struct ActorData + { + public readonly List Objects; + public readonly string Label; + + public bool Valid + => Objects.Count > 0; + + public ActorData(Actor actor, string label) + { + Objects = new List { actor }; + Label = label; + } + + public static readonly ActorData Invalid = new(false); + + private ActorData(bool _) + { + Objects = new List(0); + Label = string.Empty; + } + } + + public static bool IsInGPose { get; private set; } + public static ushort World { get; private set; } + + public static IReadOnlyDictionary Actors + => Identifiers; + + public static IReadOnlyList<(IIdentifier, ActorData)> List + => ListData; + + private static readonly Dictionary Identifiers = new(200); + private static readonly List<(IIdentifier, ActorData)> ListData = new(Dalamud.Objects.Length); + + private static void HandleIdentifier(IIdentifier identifier, Actor character) + { + if (!character.DrawObject) + return; + + switch (identifier) + { + case PlayerIdentifier p: + if (!Identifiers.TryGetValue(p, out var data)) + { + data = new ActorData(character, + World != p.HomeWorld + ? $"{p.Name} ({Dalamud.GameData.GetExcelSheet()!.GetRow(p.HomeWorld)!.Name})" + : p.Name.ToString()); + Identifiers[p] = data; + ListData.Add((p, data)); + } + else + { + data.Objects.Add(character); + } + + break; + case NpcIdentifier n when !n.Name.IsEmpty: + if (!Identifiers.TryGetValue(n, out data)) + { + data = new ActorData(character, $"{n.Name} (at {n.ObjectIndex})"); + Identifiers[n] = data; + ListData.Add((n, data)); + } + else + { + data.Objects.Add(character); + } + + break; + case OwnedIdentifier o: + if (!Identifiers.TryGetValue(o, out data)) + { + data = new ActorData(character, + World != o.OwnerHomeWorld + ? $"{o.OwnerName}s {o.Name} ({Dalamud.GameData.GetExcelSheet()!.GetRow(o.OwnerHomeWorld)!.Name})" + : $"{o.OwnerName}s {o.Name}"); + Identifiers[o] = data; + ListData.Add((o, data)); + } + else + { + data.Objects.Add(character); + } + + break; + } + } + + public static void Update() + { + World = (ushort)(Dalamud.ClientState.LocalPlayer?.CurrentWorld.Id ?? 0u); + Identifiers.Clear(); + ListData.Clear(); + + for (var i = 0; i < CutsceneIndex; ++i) + { + Actor character = Dalamud.Objects.GetObjectAddress(i); + if (character.Identifier(out var identifier)) + HandleIdentifier(identifier, character); + } + + for (var i = CutsceneIndex; i < CharacterScreenIndex; ++i) + { + Actor character = Dalamud.Objects.GetObjectAddress(i); + if (!character.Identifier(out var identifier)) + break; + + HandleIdentifier(identifier, character); + } + + void AddSpecial(int idx, string label) + { + Actor actor = Dalamud.Objects.GetObjectAddress(idx); + if (actor.Identifier(out var ident)) + { + var data = new ActorData(actor, label); + Identifiers.Add(ident, data); + ListData.Add((ident, data)); + } + } + + AddSpecial(CharacterScreenIndex, "Character Screen Actor"); + AddSpecial(ExamineScreenIndex, "Examine Screen Actor"); + AddSpecial(FittingRoomIndex, "Fitting Room Actor"); + AddSpecial(DyePreviewIndex, "Dye Preview Actor"); + AddSpecial(PortraitIndex, "Portrait Actor"); + + for (var i = PortraitIndex + 1; i < Dalamud.Objects.Length; ++i) + { + Actor character = Dalamud.Objects.GetObjectAddress(i); + if (character.Identifier(out var identifier)) + HandleIdentifier(identifier, character); + } + + + Actor gPose = Dalamud.Objects.GetObjectAddress(GPosePlayerIndex); + IsInGPose = gPose && gPose.Utf8Name.Length > 0; + } + + public static Actor GPosePlayer + => Dalamud.Objects.GetObjectAddress(GPosePlayerIndex); + + public static Actor Player + => Dalamud.Objects.GetObjectAddress(0); + + private static unsafe string GetLabel(Actor player, string playerName, int num, bool gPose) + { + var sb = new StringBuilder(64); + sb.Append(playerName); + + if (gPose) + { + sb.Append(" (GPose"); + + if (player.ObjectKind == ObjectKind.Player) + sb.Append(')'); + else + sb.Append(player.ModelId == 0 ? ", NPC)" : ", Monster)"); + } + else if (player.ObjectKind != ObjectKind.Player) + { + sb.Append(player.ModelId == 0 ? " (NPC)" : " (Monster)"); + } + + if (num > 1) + { + sb.Append(" #"); + sb.Append(num); + } + + return sb.ToString(); + } +} diff --git a/Glamourer/Interop/Offsets.cs b/Glamourer/Interop/Offsets.cs new file mode 100644 index 0000000..7127e8e --- /dev/null +++ b/Glamourer/Interop/Offsets.cs @@ -0,0 +1,23 @@ +namespace Glamourer.Interop; + +public static class Offsets +{ + public static class Character + { + public const int Wetness = 0x1ADA; + public const int HatVisible = 0x84E; + public const int VisorToggled = 0x84F; + public const int WeaponHidden1 = 0x84F; + public const int WeaponHidden2 = 0x72C; + public const int Alpha = 0x19E0; + + public static class Flags + { + public const byte IsHatHidden = 0x01; + public const byte IsVisorToggled = 0x08; + public const byte IsWet = 0x80; + public const byte IsWeaponHidden1 = 0x01; + public const byte IsWeaponHidden2 = 0x02; + } + } +} diff --git a/Glamourer/Interop/RedrawManager.cs b/Glamourer/Interop/RedrawManager.cs new file mode 100644 index 0000000..02d7254 --- /dev/null +++ b/Glamourer/Interop/RedrawManager.cs @@ -0,0 +1,287 @@ +using System; +using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Glamourer.Customization; +using Glamourer.State; +using Glamourer.Structs; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Race = Penumbra.GameData.Enums.Race; + +namespace Glamourer.Interop; + +public unsafe partial class RedrawManager +{ + private delegate void ChangeJobDelegate(IntPtr data, uint job); + + [Signature("88 51 ?? 44 3B CA", DetourName = nameof(ChangeJobDetour))] + private readonly Hook _changeJobHook = null!; + + private void ChangeJobDetour(IntPtr data, uint job) + { + _changeJobHook.Original(data, job); + JobChanged?.Invoke(data - 0x1A8, GameData.Jobs(Dalamud.GameData)[(byte)job]); + } + + public event Action? JobChanged; +} + +public unsafe partial class RedrawManager +{ + public delegate ulong FlagSlotForUpdateDelegate(Human* drawObject, uint slot, CharacterArmor* data); + + // This gets called when one of the ten equip items of an existing draw object gets changed. + [Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A", DetourName = nameof(FlagSlotForUpdateDetour))] + private readonly Hook _flagSlotForUpdateHook = null!; + + private ulong FlagSlotForUpdateDetour(Human* drawObject, uint slotIdx, CharacterArmor* data) + { + var slot = slotIdx.ToEquipSlot(); + try + { + var actor = Glamourer.Penumbra.GameObjectFromDrawObject((IntPtr)drawObject); + var identifier = actor.GetIdentifier(); + + if (_fixedDesigns.TryGetDesign(identifier, out var save)) + { + PluginLog.Information($"Loaded {slot} from fixed design for {identifier}."); + (var replaced, *data) = + Glamourer.RestrictedGear.ResolveRestricted(save.Equipment[slot], slot, (Race)drawObject->Race, (Gender)drawObject->Sex); + } + else if (_currentManipulations.TryGetDesign(identifier, out var save2)) + { + PluginLog.Information($"Updated {slot} from current designs for {identifier}."); + (var replaced, *data) = + Glamourer.RestrictedGear.ResolveRestricted(*data, slot, (Race)drawObject->Race, (Gender)drawObject->Sex); + save2.Data.Equipment[slot] = *data; + } + } + catch (Exception e) + { + PluginLog.Error($"Error on loading new gear:\n{e}"); + } + + return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data); + } + + + public bool ChangeEquip(DrawObject drawObject, EquipSlot slot, CharacterArmor data) + { + if (!drawObject) + return false; + + var slotIndex = slot.ToIndex(); + if (slotIndex > 9) + return false; + + return FlagSlotForUpdateDetour(drawObject.Pointer, slotIndex, &data) != 0; + } + + public bool ChangeEquip(Actor actor, EquipSlot slot, CharacterArmor data) + => actor && ChangeEquip(actor.DrawObject, slot, data); +} + +public unsafe partial class RedrawManager +{ + // The character weapon object manipulated is inside the actual character. + public const int CharacterWeaponOffset = 0x6C0; + + public delegate void LoadWeaponDelegate(IntPtr offsetCharacter, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, + byte skipGameObject, + byte unk4); + + // Weapons for a specific character are reloaded with this function. + // The first argument is a pointer to the game object but shifted a bit inside. + // slot is 0 for main hand, 1 for offhand, 2 for unknown (always called with empty data. + // weapon argument is the new weapon data. + // redrawOnEquality controls whether the game does anything if the new weapon is identical to the old one. + // skipGameObject seems to control whether the new weapons are written to the game object or just influence the draw object. (1 = skip, 0 = change) + // unk4 seemed to be the same as unk1. + [Signature("E8 ?? ?? ?? ?? 44 8B 9F", DetourName = nameof(LoadWeaponDetour))] + private readonly Hook _loadWeaponHook = null!; + + private void LoadWeaponDetour(IntPtr characterOffset, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject, + byte unk4) + { + var oldWeapon = weapon; + var character = (Actor)(characterOffset - CharacterWeaponOffset); + try + { + var identifier = character.GetIdentifier(); + if (_fixedDesigns.TryGetDesign(identifier, out var save)) + { + PluginLog.Information($"Loaded weapon from fixed design for {identifier}."); + weapon = slot switch + { + 0 => save.MainHand.Value, + 1 => save.OffHand.Value, + _ => weapon, + }; + } + else if (redrawOnEquality == 1 && _currentManipulations.TryGetDesign(identifier, out var save2)) + { + PluginLog.Information($"Loaded weapon from current design for {identifier}."); + switch (slot) + { + //case 0: + // save2.Data.MainHand = new CharacterWeapon(weapon); + // break; + //case 1: + // save.OffHand = new CharacterWeapon(weapon); + // break; + } + } + } + catch (Exception e) + { + PluginLog.Error($"Error on loading new weapon:\n{e}"); + } + + // First call the regular function. + _loadWeaponHook.Original(characterOffset, slot, oldWeapon, redrawOnEquality, unk2, skipGameObject, unk4); + // If something changed the weapon, call it again with the actual change, not forcing redraws and skipping applying it to the game object. + if (oldWeapon != weapon) + _loadWeaponHook.Original(characterOffset, slot, weapon, 0 /* redraw */, unk2, 1 /* skip */, unk4); + // If we're not actively changing the offhand and the game object has no offhand, redraw an empty offhand to fix animation problems. + else if (slot != 1 && character.OffHand.Value == 0) + _loadWeaponHook.Original(characterOffset, 1, 0, 1 /* redraw */, unk2, 1 /* skip */, unk4); + } + + // Load a specific weapon for a character by its data and slot. + public void LoadWeapon(Actor character, EquipSlot slot, CharacterWeapon weapon) + { + switch (slot) + { + case EquipSlot.MainHand: + LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, weapon.Value, 0, 0, 1, 0); + return; + case EquipSlot.OffHand: + LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, weapon.Value, 0, 0, 1, 0); + return; + case EquipSlot.BothHand: + LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, weapon.Value, 0, 0, 1, 0); + LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, CharacterWeapon.Empty.Value, 0, 0, 1, 0); + return; + // function can also be called with '2', but does not seem to ever be. + } + } + + // Load specific Main- and Offhand weapons. + public void LoadWeapon(Actor character, CharacterWeapon main, CharacterWeapon off) + { + LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, main.Value, 0, 0, 1, 0); + LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, off.Value, 0, 0, 1, 0); + } +} + +public unsafe partial class RedrawManager : IDisposable +{ + private readonly FixedDesigns _fixedDesigns; + private readonly CurrentManipulations _currentManipulations; + + public RedrawManager(FixedDesigns fixedDesigns, CurrentManipulations currentManipulations) + { + SignatureHelper.Initialise(this); + Glamourer.Penumbra.CreatingCharacterBase += OnCharacterRedraw; + Glamourer.Penumbra.CreatedCharacterBase += OnCharacterRedrawFinished; + _fixedDesigns = fixedDesigns; + _currentManipulations = currentManipulations; + _flagSlotForUpdateHook.Enable(); + _loadWeaponHook.Enable(); + _changeJobHook.Enable(); + } + + public void Dispose() + { + _flagSlotForUpdateHook.Dispose(); + _loadWeaponHook.Dispose(); + _changeJobHook.Dispose(); + Glamourer.Penumbra.CreatingCharacterBase -= OnCharacterRedraw; + Glamourer.Penumbra.CreatedCharacterBase -= OnCharacterRedrawFinished; + } + + private void OnCharacterRedraw(Actor actor, uint* modelId, Customize customize, CharacterEquip equip) + { + // Do not apply anything if the game object model id does not correspond to the draw object model id. + // This is the case if the actor is transformed to a different creature. + if (actor.ModelId != *modelId) + return; + + // Check if we have a current design in use, or if not if the actor has a fixed design. + var identifier = actor.GetIdentifier(); + if (!(_currentManipulations.TryGetDesign(identifier, out var save) || _fixedDesigns.TryGetDesign(identifier, out var save2))) + return; + + + // Compare game object customize data against draw object customize data for transformations. + // Apply customization if they correspond and there is customization to apply. + //var gameObjectCustomize = new Customize((CustomizeData*)actor.Pointer->CustomizeData); + //if (gameObjectCustomize.Equals(customize)) + // customize.Load(save.Customize); + // + //// Compare game object equip data against draw object equip data for transformations. + //// Apply each piece of equip that should be applied if they correspond. + //var gameObjectEquip = new CharacterEquip((CharacterArmor*)actor.Pointer->EquipSlotData); + //if (gameObjectEquip.Equals(equip)) + //{ + // var saveEquip = save.Equipment; + // foreach (var slot in EquipSlotExtensions.EqdpSlots) + // { + // (var _, equip[slot]) = + // Glamourer.RestrictedGear.ResolveRestricted(true ? equip[slot] : saveEquip[slot], slot, customize.Race, customize.Gender); + // } + //} + } + + private void OnCharacterRedraw(IntPtr gameObject, IntPtr modelId, IntPtr customize, IntPtr equipData) + { + try + { + OnCharacterRedraw(gameObject, (uint*)modelId, new Customize((CustomizeData*)customize), + new CharacterEquip((CharacterArmor*)equipData)); + } + catch (Exception e) + { + PluginLog.Error($"Error on new draw object creation:\n{e}"); + } + } + + private static void OnCharacterRedrawFinished(IntPtr gameObject, IntPtr drawObject) + { + //SetVisor((Human*)drawObject, true); + if (Glamourer.Models.FromCharacterBase((CharacterBase*)drawObject, out var data)) + PluginLog.Information($"Name: {data.FirstName} ({data.Id})"); + else + PluginLog.Information($"Key: {Glamourer.Models.KeyFromCharacterBase((CharacterBase*)drawObject):X16}"); + } + + // Update + public delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment); + + [Signature("E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86")] + private readonly ChangeCustomizeDelegate _changeCustomize = null!; + + public bool UpdateCustomize(DrawObject drawObject, Customize customize) + { + if (!drawObject.Valid) + return false; + + return _changeCustomize(drawObject.Pointer, (byte*)customize.Data, 1); + } + + + public static void SetVisor(Human* data, bool on) + { + if (data == null) + return; + + var flags = &data->CharacterBase.UnkFlags_01; + var state = (*flags & 0x40) != 0; + if (state == on) + return; + + *flags = (byte)((on ? *flags | 0x40 : *flags & 0xBF) | 0x80); + } +} diff --git a/Glamourer/ObjectManager.cs b/Glamourer/ObjectManager.cs deleted file mode 100644 index 9e8d401..0000000 --- a/Glamourer/ObjectManager.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System.Collections.Generic; -using Dalamud.Game.ClientState.Objects.Enums; -using Penumbra.GameData.ByteString; - -namespace Glamourer; - -public static class ObjectManager -{ - private const int GPosePlayerIndex = 201; - private const int CharacterScreenIndex = 240; - private const int ExamineScreenIndex = 241; - private const int FittingRoomIndex = 242; - private const int DyePreviewIndex = 243; - - private static readonly Dictionary NameCounters = new(); - private static readonly Dictionary GPoseActors = new(CharacterScreenIndex - GPosePlayerIndex); - - public static bool IsInGPose() - => Dalamud.Objects[GPosePlayerIndex] != null; - - public static Actor GPosePlayer - => Dalamud.Objects[GPosePlayerIndex]?.Address; - - public static Actor Player - => Dalamud.ClientState.LocalPlayer?.Address; - - public record struct ActorData(string Label, Actor.IIdentifier Identifier, Actor Actor, bool Modifiable, Actor GPose); - - public static IEnumerable GetEnumerator() - { - NameCounters.Clear(); - GPoseActors.Clear(); - for (var i = GPosePlayerIndex; i < CharacterScreenIndex; ++i) - { - Actor character = Dalamud.Objects[i]?.Address; - if (!character) - break; - - var identifier = character.GetIdentifier(); - GPoseActors[identifier] = character.Address; - yield return new ActorData(GetLabel(character, character.Utf8Name.ToString(), 0, true), identifier, character.Address, true, - Actor.Null); - } - - Actor actor = Dalamud.Objects[CharacterScreenIndex]?.Address; - if (actor) - yield return new ActorData("Character Screen Actor", actor.GetIdentifier(), actor.Address, false, Actor.Null); - - actor = Dalamud.Objects[ExamineScreenIndex]?.Address; - if (actor) - yield return new ActorData("Examine Screen Actor", actor.GetIdentifier(), actor.Address, false, Actor.Null); - - actor = Dalamud.Objects[FittingRoomIndex]?.Address; - if (actor) - yield return new ActorData("Fitting Room Actor", actor.GetIdentifier(), actor.Address, false, Actor.Null); - - actor = Dalamud.Objects[DyePreviewIndex]?.Address; - if (actor) - yield return new ActorData("Dye Preview Actor", actor.GetIdentifier(), actor.Address, false, Actor.Null); - - for (var i = 0; i < GPosePlayerIndex; ++i) - { - actor = Dalamud.Objects[i]?.Address; - if (!actor - || actor.ObjectKind is not (ObjectKind.Player or ObjectKind.BattleNpc or ObjectKind.EventNpc or ObjectKind.Companion - or ObjectKind.Retainer)) - continue; - - var identifier = actor.GetIdentifier(); - if (actor.Utf8Name.Length == 0) - continue; - - if (NameCounters.TryGetValue(identifier.Name, out var num)) - NameCounters[identifier.Name] = ++num; - else - NameCounters[identifier.Name] = num = 1; - - if (!GPoseActors.TryGetValue(identifier, out var gPose)) - gPose = Actor.Null; - - yield return new ActorData(GetLabel(actor, identifier.Name.ToString(), num, false), identifier, actor.Address, true, gPose); - } - - for (var i = DyePreviewIndex + 1; i < Dalamud.Objects.Length; ++i) - { - actor = Dalamud.Objects[i]?.Address; - if (!actor - || actor.ObjectKind is not (ObjectKind.Player or ObjectKind.BattleNpc or ObjectKind.EventNpc or ObjectKind.Companion - or ObjectKind.Retainer)) - continue; - - var identifier = actor.GetIdentifier(); - if (identifier.Name.Length == 0) - continue; - - if (NameCounters.TryGetValue(identifier.Name, out var num)) - NameCounters[identifier.Name] = ++num; - else - NameCounters[identifier.Name] = num = 1; - - if (!GPoseActors.TryGetValue(identifier, out var gPose)) - gPose = Actor.Null; - - yield return new ActorData(GetLabel(actor, identifier.Name.ToString(), num, false), identifier, actor.Address, true, gPose); - } - } - - private static unsafe string GetLabel(Actor player, string playerName, int num, bool gPose) - { - if (player.ObjectKind == ObjectKind.Player) - return gPose ? $"{playerName} (GPose)" : num == 1 ? playerName : $"{playerName} #{num}"; - - if (((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)player!.Address)->ModelCharaId == 0) - return gPose ? $"{playerName} (GPose, NPC)" : num == 1 ? $"{playerName} (NPC)" : $"{playerName} #{num} (NPC)"; - - return gPose ? $"{playerName} (GPose, Monster)" : num == 1 ? $"{playerName} (Monster)" : $"{playerName} #{num} (Monster)"; - } -} diff --git a/Glamourer/Offsets.cs b/Glamourer/Offsets.cs deleted file mode 100644 index 8c08073..0000000 --- a/Glamourer/Offsets.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Glamourer; - -public static class Offsets -{ - public static class Character - { - public const int Wetness = 0x1ADA; - public const int HatVisible = 0x84E; - public const int VisorToggled = 0x84F; - public const int WeaponHidden1 = 0x84F; - public const int WeaponHidden2 = 0x72C; - public const int Alpha = 0x19E0; - - public static class Flags - { - public const byte IsHatHidden = 0x01; - public const byte IsVisorToggled = 0x08; - public const byte IsWet = 0x80; - public const byte IsWeaponHidden1 = 0x01; - public const byte IsWeaponHidden2 = 0x02; - } - } -} diff --git a/Glamourer/RedrawManager.cs b/Glamourer/RedrawManager.cs deleted file mode 100644 index e24cd5d..0000000 --- a/Glamourer/RedrawManager.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System; -using Dalamud.Hooking; -using Dalamud.Logging; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using static Glamourer.Actor; - -namespace Glamourer; - -public unsafe partial class RedrawManager -{ - public delegate ulong FlagSlotForUpdateDelegate(Human* drawObject, uint slot, CharacterArmor* data); - - // This gets called when one of the ten equip items of an existing draw object gets changed. - [Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A", DetourName = nameof(FlagSlotForUpdateDetour))] - private readonly Hook? _flagSlotForUpdateHook; - - private ulong FlagSlotForUpdateDetour(Human* drawObject, uint slot, CharacterArmor* data) - { - try - { - var actor = Glamourer.Penumbra.GameObjectFromDrawObject((IntPtr)drawObject); - var identifier = actor.GetIdentifier(); - - if (_fixedDesigns.TryGetDesign(identifier, out var save)) - PluginLog.Information($"Loaded {slot.ToEquipSlot()} from fixed design for {identifier}."); - else if (_currentManipulations.TryGetDesign(identifier, out save)) - PluginLog.Information($"Updated {slot.ToEquipSlot()} from current designs for {identifier}."); - } - catch (Exception e) - { - PluginLog.Error($"Error on loading new gear:\n{e}"); - } - - return _flagSlotForUpdateHook!.Original(drawObject, slot, data); - } - - public bool ChangeEquip(Actor actor, EquipSlot slot, CharacterArmor data) - { - if (actor && actor.DrawObject != null) - return _flagSlotForUpdateHook?.Original(actor.DrawObject, slot.ToIndex(), &data) != 0; - - return false; - } -} - -public unsafe partial class RedrawManager -{ - // The character weapon object manipulated is inside the actual character. - public const int CharacterWeaponOffset = 0xD8 * 8; - - public delegate void LoadWeaponDelegate(IntPtr offsetCharacter, uint slot, CharacterWeapon weapon, byte unk1, byte unk2, byte unk3, - byte unk4); - - // Weapons for a specific character are reloaded with this function. - // The first argument is a pointer to the game object but shifted a bit inside. - // slot is 0 for main hand, 1 for offhand, 2 for unknown (always called with empty data. - // weapon argument is the new weapon data. - // unk1 seems to be 0 when re-equipping and 1 when redrawing the entire actor. - // unk2 seemed to always be 1. - // unk3 seemed to always be 0. - // unk4 seemed to be the same as unk1. - [Signature("E8 ?? ?? ?? ?? 44 8B 9F", DetourName = nameof(LoadWeaponDetour))] - private readonly Hook? _loadWeaponHook; - - private void LoadWeaponDetour(IntPtr characterOffset, uint slot, CharacterWeapon weapon, byte unk1, byte unk2, byte unk3, byte unk4) - { - try - { - var character = (Actor)(characterOffset - CharacterWeaponOffset); - var identifier = character.GetIdentifier(); - if (_fixedDesigns.TryGetDesign(identifier, out var save)) - PluginLog.Information($"Loaded weapon from fixed design for {identifier}."); - else if (unk1 == 1 && _currentManipulations.TryGetDesign(identifier, out save)) - PluginLog.Information($"Loaded weapon from current design for {identifier}."); - } - catch (Exception e) - { - PluginLog.Error($"Error on loading new weapon:\n{e}"); - } - - _loadWeaponHook!.Original(characterOffset, slot, weapon, unk1, unk2, unk3, unk4); - } - - // Load a specific weapon for a character by its data and slot. - public void LoadWeapon(IntPtr character, EquipSlot slot, CharacterWeapon weapon) - { - switch (slot) - { - case EquipSlot.MainHand: - LoadWeaponDetour(character + CharacterWeaponOffset, 0, weapon, 0, 1, 0, 0); - return; - case EquipSlot.OffHand: - LoadWeaponDetour(character + CharacterWeaponOffset, 1, weapon, 0, 1, 0, 0); - return; - case EquipSlot.BothHand: - LoadWeaponDetour(character + CharacterWeaponOffset, 0, weapon, 0, 1, 0, 0); - LoadWeaponDetour(character + CharacterWeaponOffset, 1, CharacterWeapon.Empty, 0, 1, 0, 0); - return; - // function can also be called with '2', but does not seem to ever be. - } - } - - public void LoadWeapon(Character* character, EquipSlot slot, CharacterWeapon weapon) - => LoadWeapon((IntPtr)character, slot, weapon); - - // Load specific Main- and Offhand weapons. - public void LoadWeapon(IntPtr character, CharacterWeapon main, CharacterWeapon off) - { - LoadWeaponDetour(character + CharacterWeaponOffset, 0, main, 0, 1, 0, 0); - LoadWeaponDetour(character + CharacterWeaponOffset, 1, off, 0, 1, 0, 0); - } - - public void LoadWeapon(Character* character, CharacterWeapon main, CharacterWeapon off) - => LoadWeapon((IntPtr)character, main, off); -} - -public unsafe partial class RedrawManager : IDisposable -{ - internal readonly CurrentManipulations CurrentManipulations = new(); - - private readonly FixedDesigns _fixedDesigns; - private readonly CurrentManipulations _currentManipulations; - - public RedrawManager(FixedDesigns fixedDesigns, CurrentManipulations currentManipulations) - { - SignatureHelper.Initialise(this); - Glamourer.Penumbra.CreatingCharacterBase += OnCharacterRedraw; - _fixedDesigns = fixedDesigns; - _currentManipulations = currentManipulations; - //_flagSlotForUpdateHook?.Enable(); - //_loadWeaponHook?.Enable(); - } - - public void Dispose() - { - _flagSlotForUpdateHook?.Dispose(); - _loadWeaponHook?.Dispose(); - Glamourer.Penumbra.CreatingCharacterBase -= OnCharacterRedraw; - } - - private void OnCharacterRedraw(IntPtr addr, IntPtr modelId, IntPtr customize, IntPtr equipData) - { - try - { - var actor = (Actor)addr; - var identifier = actor.GetIdentifier(); - - if (_currentManipulations.TryGetDesign(identifier, out var save)) - PluginLog.Information($"Loaded current design for {identifier}."); - else if (_fixedDesigns.TryGetDesign(identifier, out save)) - PluginLog.Information($"Loaded fixed design for {identifier}."); - } - catch (Exception e) - { - PluginLog.Error($"Error on new draw object creation:\n{e}"); - } - } -} diff --git a/Glamourer/State/CharacterSave.cs b/Glamourer/State/CharacterSave.cs new file mode 100644 index 0000000..2fce80c --- /dev/null +++ b/Glamourer/State/CharacterSave.cs @@ -0,0 +1,208 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security.AccessControl; +using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Glamourer.Customization; +using Glamourer.Interop; +using Glamourer.Structs; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using DrawObject = Glamourer.Interop.DrawObject; +using Functions = Penumbra.GameData.Util.Functions; + +namespace Glamourer.State; + +public class CharacterSaveConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, CharacterSave? value, JsonSerializer serializer) + { + var s = value?.ToBase64() ?? string.Empty; + serializer.Serialize(writer, s); + } + + public override CharacterSave ReadJson(JsonReader reader, Type objectType, CharacterSave? existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + var token = JToken.Load(reader); + var s = token.ToObject(); + return CharacterSave.FromString(s!); + } +} + +[Flags] +public enum ApplicationFlags : uint +{ + Customizations = 0x000001, + MainHand = 0x000002, + OffHand = 0x000004, + Head = 0x000008, + Body = 0x000010, + Hands = 0x000020, + Legs = 0x000040, + Feet = 0x000080, + Ears = 0x000100, + Neck = 0x000200, + Wrist = 0x000400, + RFinger = 0x000800, + LFinger = 0x001000, + SetVisor = 0x002000, + Visor = 0x004000, + SetWeapon = 0x008000, + Weapon = 0x010000, + SetWet = 0x020000, + Wet = 0x040000, +} + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct CharacterData +{ + public const byte CurrentVersion = 3; + + public uint ModelId; + public ApplicationFlags Flags; + public CustomizeData CustomizeData; + public CharacterWeapon MainHand; + public CharacterWeapon OffHand; + public CharacterArmor Head; + public CharacterArmor Body; + public CharacterArmor Hands; + public CharacterArmor Legs; + public CharacterArmor Feet; + public CharacterArmor Ears; + public CharacterArmor Neck; + public CharacterArmor Wrist; + public CharacterArmor RFinger; + public CharacterArmor LFinger; + + public unsafe Customize Customize + { + get + { + fixed (CustomizeData* ptr = &CustomizeData) + { + return new Customize(ptr); + } + } + } + + public unsafe CharacterEquip Equipment + { + get + { + fixed (CharacterArmor* ptr = &Head) + { + return new CharacterEquip(ptr); + } + } + } + + public static readonly CharacterData Default + = new() + { + ModelId = 0, + Flags = 0, + CustomizeData = Customize.Default, + MainHand = CharacterWeapon.Empty, + OffHand = CharacterWeapon.Empty, + Head = CharacterArmor.Empty, + Body = CharacterArmor.Empty, + Hands = CharacterArmor.Empty, + Legs = CharacterArmor.Empty, + Feet = CharacterArmor.Empty, + Ears = CharacterArmor.Empty, + Neck = CharacterArmor.Empty, + Wrist = CharacterArmor.Empty, + RFinger = CharacterArmor.Empty, + LFinger = CharacterArmor.Empty, + }; + + public unsafe CharacterData Clone() + { + var data = new CharacterData(); + fixed (void* ptr = &this) + { + Functions.MemCpyUnchecked(&data, ptr, sizeof(CharacterData)); + } + + return data; + } + + private const ApplicationFlags SaveFlags = ApplicationFlags.Customizations + | ApplicationFlags.Head + | ApplicationFlags.Body + | ApplicationFlags.Hands + | ApplicationFlags.Legs + | ApplicationFlags.Feet + | ApplicationFlags.Ears + | ApplicationFlags.Neck + | ApplicationFlags.Wrist + | ApplicationFlags.RFinger + | ApplicationFlags.LFinger + | ApplicationFlags.MainHand + | ApplicationFlags.OffHand + | ApplicationFlags.SetVisor + | ApplicationFlags.SetWeapon; + + + public void Load(IDesignable designable) + { + ModelId = designable.ModelId; + Customize.Load(designable.Customize); + Equipment.Load(designable.Equip); + Flags = SaveFlags | (designable.VisorEnabled ? ApplicationFlags.Visor : 0) | (designable.WeaponEnabled ? ApplicationFlags.Weapon : 0); + } +} + +public interface ICharacterData +{ + public CharacterData Data { get; } + + //public bool ApplyModel(); + //public bool ApplyCustomize(Customize target); + //public bool ApplyWeapon(ref CharacterWeapon weapon, bool mainHand, bool offHand); + //public bool ApplyGear(ref CharacterArmor armor, EquipSlot slot); + //public unsafe bool ApplyWetness(CharacterBase* drawObject); + //public unsafe bool ApplyVisorState(CharacterBase* drawObject); + //public unsafe bool ApplyWeaponState(CharacterBase* drawObject); +} + +[JsonConverter(typeof(CharacterSaveConverter))] +public class CharacterSave +{ + private CharacterData _data = CharacterData.Default; + + public CharacterSave() + { } + + public CharacterSave(Actor actor) + { + Load(actor); + } + + public void Load(T actor) where T : IDesignable + { + _data.Load(actor); + } + + public string ToBase64() + => string.Empty; + + public Customize Customize + => _data.Customize; + + public CharacterEquip Equipment + => _data.Equipment; + + public ref CharacterWeapon MainHand + => ref _data.MainHand; + + public ref CharacterWeapon OffHand + => ref _data.OffHand; + + public static CharacterSave FromString(string data) + => new(); +} diff --git a/Glamourer/State/CurrentDesign.cs b/Glamourer/State/CurrentDesign.cs new file mode 100644 index 0000000..6e7693a --- /dev/null +++ b/Glamourer/State/CurrentDesign.cs @@ -0,0 +1,63 @@ +using Glamourer.Interop; +using Penumbra.GameData.Enums; + +namespace Glamourer.State; + +public unsafe class CurrentDesign : ICharacterData +{ + public CharacterData Data + => _drawData; + + private CharacterData _drawData; + private CharacterData _initialData; + + public CurrentDesign(Actor actor) + { + _initialData = new CharacterData(); + if (!actor) + return; + + _initialData.Load(actor); + var drawObject = actor.DrawObject; + if (drawObject.Valid) + _drawData.Load(drawObject); + else + _drawData = _initialData.Clone(); + } + + public void Update(Actor actor) + { + if (!actor) + return; + + if (!_initialData.Customize.Equals(actor.Customize)) + { + _initialData.Customize.Load(actor.Customize); + _drawData.Customize.Load(actor.Customize); + } + + var initialEquip = _initialData.Equipment; + var currentEquip = actor.Equip; + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var current = currentEquip[slot]; + if (initialEquip[slot] != current) + { + initialEquip[slot] = current; + _drawData.Equipment[slot] = current; + } + } + + if (_initialData.MainHand != actor.MainHand) + { + _initialData.MainHand = actor.MainHand; + _drawData.MainHand = actor.MainHand; + } + + if (_initialData.OffHand != actor.OffHand) + { + _initialData.OffHand = actor.OffHand; + _drawData.OffHand = actor.OffHand; + } + } +} diff --git a/Glamourer/State/CurrentManipulations.cs b/Glamourer/State/CurrentManipulations.cs new file mode 100644 index 0000000..a4963c7 --- /dev/null +++ b/Glamourer/State/CurrentManipulations.cs @@ -0,0 +1,73 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Glamourer.Interop; + +namespace Glamourer.State; + +public class CurrentManipulations : IReadOnlyCollection> +{ + private readonly Dictionary _characterSaves = new(); + + public IEnumerator> GetEnumerator() + => _characterSaves.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _characterSaves.Count; + + public CurrentDesign GetOrCreateSave(Actor actor) + { + var id = actor.GetIdentifier(); + if (_characterSaves.TryGetValue(id, out var save)) + { + save.Update(actor); + return save; + } + + save = new CurrentDesign(actor); + _characterSaves.Add(id.CreatePermanent(), save); + return save; + } + + public void DeleteSave(Actor.IIdentifier identifier) + => _characterSaves.Remove(identifier); + + public bool TryGetDesign(Actor.IIdentifier identifier, [NotNullWhen(true)] out CurrentDesign? save) + => _characterSaves.TryGetValue(identifier, out save); + + //public CharacterArmor? ChangeEquip(Actor actor, EquipSlot slot, CharacterArmor data) + //{ + // var save = CreateSave(actor); + // (_, data) = _restrictedGear.ResolveRestricted(data, slot, save.Customize.Race, save.Customize.Gender); + // if (save.Equipment[slot] == data) + // return null; + // + // save.Equipment[slot] = data; + // return data; + //} + // + //public bool ChangeWeapon(Actor actor, CharacterWeapon main) + //{ + // var save = CreateSave(actor); + // if (save.MainHand == main) + // return false; + // + // save.MainHand = main; + // return true; + //} + // + //public bool ChangeWeapon(Actor actor, CharacterWeapon main, CharacterWeapon off) + //{ + // var save = CreateSave(actor); + // if (main == save.MainHand && off == save.OffHand) + // return false; + // + // save.MainHand = main; + // save.OffHand = off; + // return true; + //} + // +} diff --git a/Glamourer/FixedDesigns.cs b/Glamourer/State/FixedDesigns.cs similarity index 77% rename from Glamourer/FixedDesigns.cs rename to Glamourer/State/FixedDesigns.cs index b7e2c46..a41c6c6 100644 --- a/Glamourer/FixedDesigns.cs +++ b/Glamourer/State/FixedDesigns.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; +using Glamourer.Interop; -namespace Glamourer; +namespace Glamourer.State; public class FixedDesigns { diff --git a/Glamourer/GlamourerConfig.cs b/Glamourer/State/GlamourerConfig.cs similarity index 61% rename from Glamourer/GlamourerConfig.cs rename to Glamourer/State/GlamourerConfig.cs index 39b04f3..2ea0fde 100644 --- a/Glamourer/GlamourerConfig.cs +++ b/Glamourer/State/GlamourerConfig.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Dalamud.Configuration; -namespace Glamourer +namespace Glamourer.State { public class GlamourerConfig : IPluginConfiguration { @@ -9,25 +9,25 @@ namespace Glamourer { public string Name = string.Empty; public string Path = string.Empty; - public uint JobGroups; + public uint JobGroups; public bool Enabled; } public int Version { get; set; } = 1; public const uint DefaultCustomizationColor = 0xFFC000C0; - public const uint DefaultStateColor = 0xFF00C0C0; - public const uint DefaultEquipmentColor = 0xFF00C000; + public const uint DefaultStateColor = 0xFF00C0C0; + public const uint DefaultEquipmentColor = 0xFF00C000; - public bool FoldersFirst { get; set; } = false; - public bool ColorDesigns { get; set; } = true; - public bool ShowLocks { get; set; } = true; - public bool AttachToPenumbra { get; set; } = true; + public bool FoldersFirst { get; set; } = false; + public bool ColorDesigns { get; set; } = true; + public bool ShowLocks { get; set; } = true; + public bool AttachToPenumbra { get; set; } = true; public bool ApplyFixedDesigns { get; set; } = true; public uint CustomizationColor { get; set; } = DefaultCustomizationColor; - public uint StateColor { get; set; } = DefaultStateColor; - public uint EquipmentColor { get; set; } = DefaultEquipmentColor; + public uint StateColor { get; set; } = DefaultStateColor; + public uint EquipmentColor { get; set; } = DefaultEquipmentColor; public List FixedDesigns { get; set; } = new(); diff --git a/Glamourer/CharacterExtensions.cs b/Glamourer/Util/CharacterExtensions.cs similarity index 93% rename from Glamourer/CharacterExtensions.cs rename to Glamourer/Util/CharacterExtensions.cs index 0d28e13..703f8dc 100644 --- a/Glamourer/CharacterExtensions.cs +++ b/Glamourer/Util/CharacterExtensions.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Objects.Types; +using Glamourer.Interop; -namespace Glamourer; +namespace Glamourer.Util; public static class CharacterExtensions { @@ -27,7 +28,7 @@ public static class CharacterExtensions public static unsafe bool SetHatVisible(this Character a, bool visible) { - var current = IsHatVisible(a); + var current = a.IsHatVisible(); if (current == visible) return false; @@ -46,7 +47,7 @@ public static class CharacterExtensions public static unsafe bool SetVisorToggled(this Character a, bool toggled) { - var current = IsVisorToggled(a); + var current = a.IsVisorToggled(); if (current == toggled) return false; @@ -67,7 +68,7 @@ public static class CharacterExtensions public static unsafe bool SetWeaponHidden(this Character a, bool value) { - var hidden = IsWeaponHidden(a); + var hidden = a.IsWeaponHidden(); if (hidden == value) return false; diff --git a/Glamourer/CustomizeExtensions.cs b/Glamourer/Util/CustomizeExtensions.cs similarity index 55% rename from Glamourer/CustomizeExtensions.cs rename to Glamourer/Util/CustomizeExtensions.cs index 32f8814..d08824c 100644 --- a/Glamourer/CustomizeExtensions.cs +++ b/Glamourer/Util/CustomizeExtensions.cs @@ -1,9 +1,11 @@ using System; +using System.Net.Http; using Glamourer.Customization; +using Glamourer.State; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -namespace Glamourer; +namespace Glamourer.Util; public static unsafe class CustomizeExtensions { @@ -54,4 +56,87 @@ public static unsafe class CustomizeExtensions public static string ClanName(this Customize customize) => ClanName(customize.Clan, customize.Gender); + + + // Change a gender and fix up all required customizations afterwards. + public static bool ChangeGender(this Customize customize, CharacterEquip equip, Gender gender) + { + if (customize.Gender == gender) + return false; + + FixRestrictedGear(customize, equip, gender, customize.Race); + customize.Gender = gender; + FixUpAttributes(customize); + return true; + } + + // Change a race and fix up all required customizations afterwards. + public static bool ChangeRace(this Customize customize, CharacterEquip equip, SubRace clan) + { + if (customize.Clan == clan) + return false; + + var race = clan.ToRace(); + var gender = race == Race.Hrothgar ? Gender.Male : customize.Gender; // TODO Female Hrothgar + FixRestrictedGear(customize, equip, gender, race); + customize.Gender = gender; + customize.Race = race; + customize.Clan = clan; + FixUpAttributes(customize); + return true; + } + + public static void ChangeCustomization(this Customize customize, CharacterEquip equip, Customize newCustomize) + { + FixRestrictedGear(customize, equip, newCustomize.Gender, newCustomize.Race); + customize.Load(newCustomize); + } + + public static bool ChangeCustomization(this Customize customize, CharacterEquip equip, CustomizationId id, byte value) + { + switch (id) + { + case CustomizationId.Race: return customize.ChangeRace(equip, (SubRace)value); + case CustomizationId.Gender: return customize.ChangeGender(equip, (Gender)value); + } + + if (customize[id] == value) + return false; + + customize[id] = value; + return true; + } + + // Go through a whole customization struct and fix up all settings that need fixing. + private static void FixUpAttributes(Customize customize) + { + var set = Glamourer.Customization.GetList(customize.Clan, customize.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: break; + default: + var count = set.Count(id); + if (set.DataByValue(id, customize[id], out _) < 0) + customize[id] = count == 0 ? (byte)0 : set.Data(id, 0).Value; + break; + } + } + } + + private static void FixRestrictedGear(Customize customize, CharacterEquip equip, Gender gender, Race race) + { + if (race == customize.Race && gender == customize.Gender) + return; + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + (_, equip[slot]) = Glamourer.RestrictedGear.ResolveRestricted(equip[slot], slot, race, gender); + } }