From d10cb3137f2b5170a5998213cd8a6c2a12537e9b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 Jun 2023 00:49:26 +0200 Subject: [PATCH] .. --- .../Customization/CustomizeIndex.cs | 2 +- Glamourer/Designs/Design.cs | 384 ++++++-------- Glamourer/Designs/DesignBase64Migration.cs | 27 +- Glamourer/Designs/DesignData.cs | 4 +- Glamourer/Designs/DesignFileSystem.cs | 200 ++++++++ Glamourer/Designs/DesignManager.cs | 470 ++++++++++++++++++ Glamourer/Events/DesignChanged.cs | 76 +++ Glamourer/Gui/Tabs/DebugTab.cs | 206 ++++++-- Glamourer/Services/CustomizationService.cs | 211 ++++++++ Glamourer/Services/FilenameService.cs | 1 + Glamourer/Services/ItemManager.cs | 86 +++- Glamourer/Services/ServiceManager.cs | 9 +- Glamourer/Services/ServiceWrapper.cs | 56 +-- GlamourerOld/Designs/DesignManager.cs | 7 +- 14 files changed, 1366 insertions(+), 373 deletions(-) create mode 100644 Glamourer/Designs/DesignFileSystem.cs create mode 100644 Glamourer/Designs/DesignManager.cs create mode 100644 Glamourer/Events/DesignChanged.cs create mode 100644 Glamourer/Services/CustomizationService.cs diff --git a/Glamourer.GameData/Customization/CustomizeIndex.cs b/Glamourer.GameData/Customization/CustomizeIndex.cs index 6d30734..0147c79 100644 --- a/Glamourer.GameData/Customization/CustomizeIndex.cs +++ b/Glamourer.GameData/Customization/CustomizeIndex.cs @@ -55,7 +55,7 @@ public static class CustomizationExtensions CustomizeIndex.Clan => (4, 0xFF), CustomizeIndex.Face => (5, 0xFF), CustomizeIndex.Hairstyle => (6, 0xFF), - CustomizeIndex.Highlights => (7, 0xFF), + CustomizeIndex.Highlights => (7, 0x80), CustomizeIndex.SkinColor => (8, 0xFF), CustomizeIndex.EyeColorRight => (9, 0xFF), CustomizeIndex.HairColor => (10, 0xFF), diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index 77a0dec..c3564dd 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -15,8 +15,12 @@ namespace Glamourer.Designs; public class Design : ISavable { + #region Data + internal Design(ItemManager items) - { } + { + SetDefaultEquipment(items); + } // Metadata public const int FileVersion = 1; @@ -31,6 +35,22 @@ public class Design : ISavable internal DesignData DesignData; + public void SetDefaultEquipment(ItemManager items) + { + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + DesignData.SetItem(slot, ItemManager.NothingItem(slot)); + DesignData.SetStain(slot, 0); + } + + DesignData.SetItem(EquipSlot.MainHand, items.DefaultSword); + DesignData.SetStain(EquipSlot.MainHand, 0); + DesignData.SetItem(EquipSlot.OffHand, ItemManager.NothingItem(FullEquipType.Shield)); + DesignData.SetStain(EquipSlot.OffHand, 0); + } + + #endregion + #region Application Data [Flags] @@ -42,9 +62,9 @@ public class Design : ISavable WriteProtected = 0x08, } - private CustomizeFlag _applyCustomize; - private EquipFlag _applyEquip; - private DesignFlags _designFlags; + internal CustomizeFlag ApplyCustomize = CustomizeFlagExtensions.All; + internal EquipFlag ApplyEquip = EquipFlagExtensions.All; + private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible; public bool DoApplyHatVisible() => _designFlags.HasFlag(DesignFlags.ApplyHatVisible); @@ -100,47 +120,47 @@ public class Design : ISavable public bool DoApplyEquip(EquipSlot slot) - => _applyEquip.HasFlag(slot.ToFlag()); + => ApplyEquip.HasFlag(slot.ToFlag()); public bool DoApplyStain(EquipSlot slot) - => _applyEquip.HasFlag(slot.ToStainFlag()); + => ApplyEquip.HasFlag(slot.ToStainFlag()); public bool DoApplyCustomize(CustomizeIndex idx) - => _applyCustomize.HasFlag(idx.ToFlag()); + => ApplyCustomize.HasFlag(idx.ToFlag()); internal bool SetApplyEquip(EquipSlot slot, bool value) { - var newValue = value ? _applyEquip | slot.ToFlag() : _applyEquip & ~slot.ToFlag(); - if (newValue == _applyEquip) + var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag(); + if (newValue == ApplyEquip) return false; - _applyEquip = newValue; + ApplyEquip = newValue; return true; } internal bool SetApplyStain(EquipSlot slot, bool value) { - var newValue = value ? _applyEquip | slot.ToStainFlag() : _applyEquip & ~slot.ToStainFlag(); - if (newValue == _applyEquip) + var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag(); + if (newValue == ApplyEquip) return false; - _applyEquip = newValue; + ApplyEquip = newValue; return true; } internal bool SetApplyCustomize(CustomizeIndex idx, bool value) { - var newValue = value ? _applyCustomize | idx.ToFlag() : _applyCustomize & ~idx.ToFlag(); - if (newValue == _applyCustomize) + var newValue = value ? ApplyCustomize | idx.ToFlag() : ApplyCustomize & ~idx.ToFlag(); + if (newValue == ApplyCustomize) return false; - _applyCustomize = newValue; + ApplyCustomize = newValue; return true; } #endregion - #region ISavable + #region Serialization public JObject JsonSerialize() { @@ -207,17 +227,21 @@ public class Design : ISavable return ret; } - public static Design LoadDesign(CustomizationManager customizeManager, ItemManager items, JObject json, out bool changes) + #endregion + + #region Deserialization + + public static Design LoadDesign(CustomizationService customizations, ItemManager items, JObject json) { var version = json["FileVersion"]?.ToObject() ?? 0; return version switch { - 1 => LoadDesignV1(customizeManager, items, json, out changes), + 1 => LoadDesignV1(customizations, items, json), _ => throw new Exception("The design to be loaded has no valid Version."), }; } - private static Design LoadDesignV1(CustomizationManager customizeManager, ItemManager items, JObject json, out bool changes) + private static Design LoadDesignV1(CustomizationService customizations, ItemManager items, JObject json) { static string[] ParseTags(JObject json) { @@ -236,85 +260,23 @@ public class Design : ISavable Tags = ParseTags(json), LastEdit = json["LastEdit"]?.ToObject() ?? creationDate, }; + if (design.LastEdit < creationDate) + design.LastEdit = creationDate; - changes = LoadEquip(items, json["Equipment"], design); - changes |= LoadCustomize(customizeManager, json["Customize"], design); + LoadEquip(items, json["Equipment"], design); + LoadCustomize(customizations, json["Customize"], design); return design; } - private static bool ValidateItem(ItemManager items, EquipSlot slot, uint itemId, out EquipItem item) - { - item = items.Resolve(slot, itemId); - if (item.Valid) - return true; - - Glamourer.Chat.NotificationMessage($"The {slot.ToName()} item {itemId} does not exist, reset to Nothing.", "Warning", - NotificationType.Warning); - item = ItemManager.NothingItem(slot); - return false; - } - - private static bool ValidateStain(ItemManager items, StainId stain, out StainId ret) - { - if (stain.Value != 0 && !items.Stains.ContainsKey(stain)) - { - ret = 0; - Glamourer.Chat.NotificationMessage($"The Stain {stain} does not exist, reset to unstained."); - return false; - } - - ret = stain; - return true; - } - - private static bool ValidateWeapons(ItemManager items, uint mainId, uint offId, out EquipItem main, out EquipItem off) - { - var ret = true; - main = items.Resolve(EquipSlot.MainHand, mainId); - if (!main.Valid) - { - Glamourer.Chat.NotificationMessage($"The mainhand weapon {mainId} does not exist, reset to default sword.", "Warning", - NotificationType.Warning); - main = items.DefaultSword; - ret = false; - } - - off = items.Resolve(main.Type.Offhand(), offId); - if (off.Valid) - return ret; - - ret = false; - off = items.Resolve(main.Type.Offhand(), mainId); - if (off.Valid) - { - Glamourer.Chat.NotificationMessage($"The offhand weapon {offId} does not exist, reset to implied offhand.", "Warning", - NotificationType.Warning); - } - else - { - off = ItemManager.NothingItem(FullEquipType.Shield); - if (main.Type.Offhand() == FullEquipType.Shield) - { - Glamourer.Chat.NotificationMessage($"The offhand weapon {offId} does not exist, reset to no offhand.", "Warning", - NotificationType.Warning); - } - else - { - main = items.DefaultSword; - Glamourer.Chat.NotificationMessage( - $"The offhand weapon {offId} does not exist, but no default could be restored, reset mainhand to default sword and offhand to nothing.", - "Warning", - NotificationType.Warning); - } - } - - return ret; - } - - private static bool LoadEquip(ItemManager items, JToken? equip, Design design) + private static void LoadEquip(ItemManager items, JToken? equip, Design design) { if (equip == null) - return true; + { + design.SetDefaultEquipment(items); + Glamourer.Chat.NotificationMessage("The loaded design does not contain any equipment data, reset to default.", "Warning", + NotificationType.Warning); + return; + } static (uint, StainId, bool, bool) ParseItem(EquipSlot slot, JToken? item) { @@ -325,13 +287,19 @@ public class Design : ISavable return (id, stain, apply, applyStain); } - var changes = false; + void PrintWarning(string msg) + { + if (msg.Length > 0) + Glamourer.Chat.NotificationMessage($"{msg} ({design.Name})", "Warning", NotificationType.Warning); + } + foreach (var slot in EquipSlotExtensions.EqdpSlots) { - var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]); - changes |= !ValidateItem(items, slot, id, out var item); - changes |= !ValidateStain(items, stain, out stain); - design.DesignData.SetItem(item); + var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]); + + PrintWarning(items.ValidateItem(slot, id, out var item)); + PrintWarning(items.ValidateStain(stain, out stain)); + design.DesignData.SetItem(slot, item); design.DesignData.SetStain(slot, stain); design.SetApplyEquip(slot, apply); design.SetApplyStain(slot, applyStain); @@ -344,11 +312,12 @@ public class Design : ISavable var (idOff, stainOff, applyOff, applyStainOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]); if (id == ItemManager.NothingId(EquipSlot.OffHand)) id = ItemManager.NothingId(FullEquipType.Shield); - changes |= ValidateWeapons(items, id, idOff, out var main, out var off); - changes |= ValidateStain(items, stain, out stain); - changes |= ValidateStain(items, stainOff, out stainOff); - design.DesignData.SetItem(main); - design.DesignData.SetItem(off); + + PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off)); + PrintWarning(items.ValidateStain(stain, out stain)); + PrintWarning(items.ValidateStain(stainOff, out stainOff)); + design.DesignData.SetItem(EquipSlot.MainHand, main); + design.DesignData.SetItem(EquipSlot.OffHand, off); design.DesignData.SetStain(EquipSlot.MainHand, stain); design.DesignData.SetStain(EquipSlot.OffHand, stainOff); design.SetApplyEquip(EquipSlot.MainHand, apply); @@ -367,146 +336,89 @@ public class Design : ISavable metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse); design.SetApplyVisorToggle(metaValue.Enabled); design.DesignData.SetVisor(metaValue.ForcedValue); - - return changes; } - private static bool ValidateCustomize(CustomizationManager manager, ref Customize customize) - { - var ret = true; - if (!manager.Races.Contains(customize.Race)) - { - ret = false; - if (manager.Clans.Contains(customize.Clan)) - { - Glamourer.Chat.NotificationMessage( - $"The race {customize.Race.ToName()} is unknown, reset to {customize.Clan.ToRace().ToName()} from Clan.", "Warning", - NotificationType.Warning); - customize.Race = customize.Clan.ToRace(); - } - else - { - Glamourer.Chat.NotificationMessage( - $"The race {customize.Race.ToName()} is unknown, reset to {Race.Hyur.ToName()} {SubRace.Midlander.ToName()}.", "Warning", - NotificationType.Warning); - customize.Race = Race.Hyur; - customize.Clan = SubRace.Midlander; - } - } - - if (!manager.Clans.Contains(customize.Clan)) - { - ret = false; - var oldClan = customize.Clan; - customize.Clan = (SubRace)((byte)customize.Race * 2 - 1); - if (manager.Clans.Contains(customize.Clan)) - { - Glamourer.Chat.NotificationMessage($"The clan {oldClan.ToName()} is unknown, reset to {customize.Clan.ToName()} from race.", - "Warning", NotificationType.Warning); - } - else - { - customize.Race = Race.Hyur; - customize.Clan = SubRace.Midlander; - Glamourer.Chat.NotificationMessage( - $"The clan {oldClan.ToName()} is unknown, reset to {customize.Race.ToName()} {customize.Clan.ToName()}.", "Warning", - NotificationType.Warning); - } - } - - if (!manager.Genders.Contains(customize.Gender)) - { - ret = false; - Glamourer.Chat.NotificationMessage($"The gender {customize.Gender} is unknown, reset to {Gender.Male.ToName()}.", "Warning", - NotificationType.Warning); - customize.Gender = Gender.Male; - } - - // TODO: Female Hrothgar - if (customize.Gender == Gender.Female && customize.Race == Race.Hrothgar) - { - ret = false; - Glamourer.Chat.NotificationMessage($"Hrothgar do not currently support female characters, reset to male.", "Warning", - NotificationType.Warning); - customize.Gender = Gender.Male; - } - - var list = manager.GetList(customize.Clan, customize.Gender); - - // Face is handled first automatically so it should not conflict with other customizations when corrupt. - foreach (var index in Enum.GetValues().Where(list.IsAvailable)) - { - var value = customize.Get(index); - var count = list.Count(index, customize.Face); - var idx = list.DataByValue(index, value, out var data, customize.Face); - if (idx >= 0 && idx < count) - continue; - - ret = false; - var name = list.Option(index); - var newValue = list.Data(index, 0, customize.Face); - Glamourer.Chat.NotificationMessage( - $"Customization {name} for {customize.Race.ToName()} {customize.Gender.ToName()}s does not support value {value.Value}, reset to {newValue.Value.Value}"); - customize.Set(index, newValue.Value); - } - - return ret; - } - - private static bool ValidateModelId(ref uint modelId) - { - if (modelId != 0) - { - Glamourer.Chat.NotificationMessage($"Model IDs different from 0 are not currently allowed, reset {modelId} to 0.", "Warning", - NotificationType.Warning); - modelId = 0; - return false; - } - - return true; - } - - private static bool LoadCustomize(CustomizationManager manager, JToken? json, Design design) + private static void LoadCustomize(CustomizationService customizations, JToken? json, Design design) { if (json == null) - return true; + { + design.DesignData.ModelId = 0; + design.DesignData.Customize = Customize.Default; + Glamourer.Chat.NotificationMessage("The loaded design does not contain any customization data, reset to default.", "Warning", + NotificationType.Warning); + return; + } + + void PrintWarning(string msg) + { + if (msg.Length > 0) + Glamourer.Chat.NotificationMessage($"{msg} ({design.Name})", "Warning", NotificationType.Warning); + } design.DesignData.ModelId = json["ModelId"]?.ToObject() ?? 0; - var ret = !ValidateModelId(ref design.DesignData.ModelId); + PrintWarning(customizations.ValidateModelId(design.DesignData.ModelId, out design.DesignData.ModelId)); - foreach (var idx in Enum.GetValues()) + var race = (Race)(json[CustomizeIndex.Race.ToString()]?["Value"]?.ToObject() ?? 0); + var clan = (SubRace)(json[CustomizeIndex.Clan.ToString()]?["Value"]?.ToObject() ?? 0); + PrintWarning(customizations.ValidateClan(clan, race, out race, out clan)); + var gender = (Gender)((json[CustomizeIndex.Gender.ToString()]?["Value"]?.ToObject() ?? 0) + 1); + PrintWarning(customizations.ValidateGender(race, gender, out gender)); + design.DesignData.Customize.Race = race; + design.DesignData.Customize.Clan = clan; + design.DesignData.Customize.Gender = gender; + design.SetApplyCustomize(CustomizeIndex.Race, json[CustomizeIndex.Race.ToString()]?["Apply"]?.ToObject() ?? false); + design.SetApplyCustomize(CustomizeIndex.Clan, json[CustomizeIndex.Clan.ToString()]?["Apply"]?.ToObject() ?? false); + design.SetApplyCustomize(CustomizeIndex.Gender, json[CustomizeIndex.Gender.ToString()]?["Apply"]?.ToObject() ?? false); + + var set = customizations.AwaitedService.GetList(clan, gender); + + foreach (var idx in Enum.GetValues().Where(set.IsAvailable)) { - var tok = json[idx.ToString()]; - var data = (CustomizeValue)(tok?["Value"]?.ToObject() ?? 0); + var tok = json[idx.ToString()]; + var data = (CustomizeValue)(tok?["Value"]?.ToObject() ?? 0); + PrintWarning(CustomizationService.ValidateCustomizeValue(set, design.DesignData.Customize.Face, idx, data, out data)); var apply = tok?["Apply"]?.ToObject() ?? false; design.DesignData.Customize[idx] = data; design.SetApplyCustomize(idx, apply); } design.DesignData.SetIsWet(json["IsWet"]?.ToObject() ?? false); - ret |= !ValidateCustomize(manager, ref design.DesignData.Customize); - - return ret; } - //public void MigrateBase64(ItemManager items, string base64) - //{ - // var data = DesignBase64Migration.MigrateBase64(items, base64, out var applyEquip, out var applyCustomize, out var writeProtected, out var wet, - // out var hat, - // out var visor, out var weapon); - // UpdateMainhand(items, data.MainHand); - // UpdateOffhand(items, data.OffHand); - // foreach (var slot in EquipSlotExtensions.EqdpSlots) - // UpdateArmor(items, slot, data.Armor(slot), true); - // ModelData.Customize = data.Customize; - // _applyEquip = applyEquip; - // _applyCustomize = applyCustomize; - // WriteProtected = writeProtected; - // Wetness = wet; - // Hat = hat; - // Visor = visor; - // Weapon = weapon; - //} + #endregion + + #region ISavable + + public string ToFilename(FilenameService fileNames) + => fileNames.DesignFile(this); + + public void Save(StreamWriter writer) + { + using var j = new JsonTextWriter(writer) + { + Formatting = Formatting.Indented, + }; + var obj = JsonSerialize(); + obj.WriteTo(j); + } + + public string LogName(string fileName) + => Path.GetFileNameWithoutExtension(fileName); + + #endregion + + public void MigrateBase64(ItemManager items, string base64) + { + DesignData = DesignBase64Migration.MigrateBase64(items, base64, out var equipFlags, out var customizeFlags, out var writeProtected, + out var applyHat, out var applyVisor, out var applyWeapon); + ApplyEquip = equipFlags; + ApplyCustomize = customizeFlags; + SetWriteProtected(writeProtected); + SetApplyHatVisible(applyHat); + SetApplyVisorToggle(applyVisor); + SetApplyWeaponVisible(applyWeapon); + } + // //public static Design CreateTemporaryFromBase64(ItemManager items, string base64, bool customize, bool equip) //{ @@ -528,22 +440,4 @@ public class Design : ISavable // => DesignBase64Migration.CreateOldBase64(in ModelData, _applyEquip, _applyCustomize, Wetness == QuadBool.True, Hat.ForcedValue, // Hat.Enabled, // Visor.ForcedValue, Visor.Enabled, Weapon.ForcedValue, Weapon.Enabled, WriteProtected, 1f); - - public string ToFilename(FilenameService fileNames) - => fileNames.DesignFile(this); - - public void Save(StreamWriter writer) - { - using var j = new JsonTextWriter(writer) - { - Formatting = Formatting.Indented, - }; - var obj = JsonSerialize(); - obj.WriteTo(j); - } - - public string LogName(string fileName) - => Path.GetFileNameWithoutExtension(fileName); - - #endregion } diff --git a/Glamourer/Designs/DesignBase64Migration.cs b/Glamourer/Designs/DesignBase64Migration.cs index 6634b01..763ab7c 100644 --- a/Glamourer/Designs/DesignBase64Migration.cs +++ b/Glamourer/Designs/DesignBase64Migration.cs @@ -78,16 +78,16 @@ public static class DesignBase64Migration if (!main.Valid) throw new Exception($"Base64 string invalid, weapon could not be identified."); - data.SetItem(main); + data.SetItem(EquipSlot.MainHand, main); data.SetStain(EquipSlot.MainHand, cur[0].Stain); var off = items.Identify(EquipSlot.OffHand, cur[1].Set, cur[1].Type, (byte)cur[1].Variant, main.Type); if (!off.Valid) throw new Exception($"Base64 string invalid, weapon could not be identified."); - data.SetItem(off); + data.SetItem(EquipSlot.OffHand, off); data.SetStain(EquipSlot.OffHand, cur[1].Stain); - var eq = (CharacterArmor*)(ptr + 46); + var eq = (CharacterArmor*)(cur + 2); foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex()) { var mdl = eq[idx]; @@ -95,7 +95,7 @@ public static class DesignBase64Migration if (!item.Valid) throw new Exception($"Base64 string invalid, item could not be identified."); - data.SetItem(item); + data.SetItem(slot, item); data.SetStain(slot, mdl.Stain); } } @@ -130,20 +130,13 @@ public static class DesignBase64Migration save.Customize.Write((nint)data + 4); ((CharacterWeapon*)(data + 30))[0] = save.Item(EquipSlot.MainHand).Weapon(save.Stain(EquipSlot.MainHand)); ((CharacterWeapon*)(data + 30))[1] = save.Item(EquipSlot.OffHand).Weapon(save.Stain(EquipSlot.OffHand)); - ((CharacterArmor*)(data + 46))[0] = save.Item(EquipSlot.Head).Armor(save.Stain(EquipSlot.Head)); - ((CharacterArmor*)(data + 46))[1] = save.Item(EquipSlot.Body).Armor(save.Stain(EquipSlot.Body)); - ((CharacterArmor*)(data + 46))[2] = save.Item(EquipSlot.Hands).Armor(save.Stain(EquipSlot.Hands)); - ((CharacterArmor*)(data + 46))[3] = save.Item(EquipSlot.Legs).Armor(save.Stain(EquipSlot.Legs)); - ((CharacterArmor*)(data + 46))[4] = save.Item(EquipSlot.Feet).Armor(save.Stain(EquipSlot.Feet)); - ((CharacterArmor*)(data + 46))[5] = save.Item(EquipSlot.Ears).Armor(save.Stain(EquipSlot.Ears)); - ((CharacterArmor*)(data + 46))[6] = save.Item(EquipSlot.Neck).Armor(save.Stain(EquipSlot.Neck)); - ((CharacterArmor*)(data + 46))[7] = save.Item(EquipSlot.Wrists).Armor(save.Stain(EquipSlot.Wrists)); - ((CharacterArmor*)(data + 46))[8] = save.Item(EquipSlot.RFinger).Armor(save.Stain(EquipSlot.RFinger)); - ((CharacterArmor*)(data + 46))[9] = save.Item(EquipSlot.LFinger).Armor(save.Stain(EquipSlot.LFinger)); - *(float*)(data + 86) = 1f; - data[90] = (byte)((save.IsHatVisible() ? 0x01 : 0) + foreach (var slot in EquipSlotExtensions.EqdpSlots) + ((CharacterArmor*)(data + 44))[slot.ToIndex()] = save.Item(slot).Armor(save.Stain(slot)); + *(ushort*)(data + 84) = 1; // IsSet. + *(float*)(data + 86) = 1f; + data[90] = (byte)((save.IsHatVisible() ? 0x00 : 0x01) | (save.IsVisorToggled() ? 0x10 : 0) - | (save.IsWeaponVisible() ? 0x02 : 0)); + | (save.IsWeaponVisible() ? 0x00 : 0x02)); return Convert.ToBase64String(new Span(data, Base64Size)); } diff --git a/Glamourer/Designs/DesignData.cs b/Glamourer/Designs/DesignData.cs index 73df918..8bdbbb2 100644 --- a/Glamourer/Designs/DesignData.cs +++ b/Glamourer/Designs/DesignData.cs @@ -60,9 +60,9 @@ public unsafe struct DesignData // @formatter:on }; - public bool SetItem(EquipItem item) + public bool SetItem(EquipSlot slot, EquipItem item) { - var index = item.Type.ToSlot().ToIndex(); + var index = slot.ToIndex(); if (index > 11 || _itemIds[index] == item.Id) return false; diff --git a/Glamourer/Designs/DesignFileSystem.cs b/Glamourer/Designs/DesignFileSystem.cs new file mode 100644 index 0000000..4c6e130 --- /dev/null +++ b/Glamourer/Designs/DesignFileSystem.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Glamourer.Events; +using Glamourer.Services; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Filesystem; + +namespace Glamourer.Designs; + +public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable +{ + private readonly DesignChanged _designChanged; + + private readonly SaveService _saveService; + private readonly DesignManager _designManager; + + public DesignFileSystem(DesignManager designManager, SaveService saveService, DesignChanged designChanged) + { + _designManager = designManager; + _saveService = saveService; + _designChanged = designChanged; + _designChanged.Subscribe(OnDataChange, DesignChanged.Priority.DesignFileSystem); + Changed += OnChange; + Reload(); + } + + private void Reload() + { + if (Load(new FileInfo(_saveService.FileNames.DesignFileSystem), _designManager.Designs, DesignToIdentifier, DesignToName)) + _saveService.ImmediateSave(this); + + Glamourer.Log.Debug("Reloaded design filesystem."); + } + + public void Dispose() + { + _designChanged.Unsubscribe(OnDataChange); + } + + public struct CreationDate : ISortMode + { + public string Name + => "Creation Date (Older First)"; + + public string Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date."; + + public IEnumerable GetChildren(Folder f) + => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderBy(l => l.Value.CreationDate)); + } + + public struct UpdateDate : ISortMode + { + public string Name + => "Update Date (Older First)"; + + public string Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their last update date."; + + public IEnumerable GetChildren(Folder f) + => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderBy(l => l.Value.LastEdit)); + } + + public struct InverseCreationDate : ISortMode + { + public string Name + => "Creation Date (Newer First)"; + + public string Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date."; + + public IEnumerable GetChildren(Folder f) + => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderByDescending(l => l.Value.CreationDate)); + } + + public struct InverseUpdateDate : ISortMode + { + public string Name + => "Update Date (Newer First)"; + + public string Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse last update date."; + + public IEnumerable GetChildren(Folder f) + => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderByDescending(l => l.Value.LastEdit)); + } + + private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3) + { + if (type != FileSystemChangeType.Reload) + _saveService.QueueSave(this); + } + + private void OnDataChange(DesignChanged.Type type, Design design, object? data) + { + switch (type) + { + case DesignChanged.Type.Created: + var originalName = design.Name.Text.FixName(); + var name = originalName; + var counter = 1; + while (Find(name, out _)) + name = $"{originalName} ({++counter})"; + + CreateLeaf(Root, name, design); + break; + case DesignChanged.Type.Deleted: + if (FindLeaf(design, out var leaf)) + Delete(leaf); + break; + case DesignChanged.Type.ReloadedAll: + Reload(); + break; + case DesignChanged.Type.Renamed when data is string oldName: + var old = oldName.FixName(); + if (Find(old, out var child) && child is not Folder) + Rename(child, design.Name); + break; + } + } + + // Used for saving and loading. + private static string DesignToIdentifier(Design design) + => design.Identifier.ToString(); + + private static string DesignToName(Design design) + => design.Name.Text.FixName(); + + private static bool DesignHasDefaultPath(Design design, string fullPath) + { + var regex = new Regex($@"^{Regex.Escape(DesignToName(design))}( \(\d+\))?$"); + return regex.IsMatch(fullPath); + } + + private static (string, bool) SaveDesign(Design design, string fullPath) + // Only save pairs with non-default paths. + => DesignHasDefaultPath(design, fullPath) + ? (string.Empty, false) + : (DesignToIdentifier(design), true); + + // Search the entire filesystem for the leaf corresponding to a design. + public bool FindLeaf(Design design, [NotNullWhen(true)] out Leaf? leaf) + { + leaf = Root.GetAllDescendants(ISortMode.Lexicographical) + .OfType() + .FirstOrDefault(l => l.Value == design); + return leaf != null; + } + + internal static void MigrateOldPaths(SaveService saveService, Dictionary oldPaths) + { + if (oldPaths.Count == 0) + return; + + var file = saveService.FileNames.DesignFileSystem; + try + { + JObject jObject; + if (File.Exists(file)) + { + var text = File.ReadAllText(file); + jObject = JObject.Parse(text); + var dict = jObject["Data"]?.ToObject>(); + if (dict != null) + foreach (var (key, value) in dict) + oldPaths.TryAdd(key, value); + + jObject["Data"] = JToken.FromObject(oldPaths); + } + else + { + jObject = new JObject + { + ["Data"] = JToken.FromObject(oldPaths), + ["EmptyFolders"] = JToken.FromObject(Array.Empty()), + }; + } + + var data = jObject.ToString(Formatting.Indented); + File.WriteAllText(file, data); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not migrate old folder paths to new version:\n{ex}"); + } + } + + public string ToFilename(FilenameService fileNames) + => fileNames.DesignFileSystem; + + public void Save(StreamWriter writer) + { + SaveToFile(writer, SaveDesign, true); + } +} diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs new file mode 100644 index 0000000..f8bc71c --- /dev/null +++ b/Glamourer/Designs/DesignManager.cs @@ -0,0 +1,470 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Utility; +using Glamourer.Customization; +using Glamourer.Events; +using Glamourer.Services; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Designs; + +public class DesignManager +{ + private readonly CustomizationService _customizations; + private readonly ItemManager _items; + private readonly SaveService _saveService; + private readonly DesignChanged _event; + private readonly List _designs = new(); + + public IReadOnlyList Designs + => _designs; + + public DesignManager(SaveService saveService, ItemManager items, CustomizationService customizations, + DesignChanged @event) + { + _saveService = saveService; + _items = items; + _customizations = customizations; + _event = @event; + CreateDesignFolder(saveService); + LoadDesigns(); + MigrateOldDesigns(); + } + + /// + /// Clear currently loaded designs and load all designs anew from file. + /// Invalid data is fixed, but changes are not saved until manual changes. + /// + public void LoadDesigns() + { + _designs.Clear(); + List<(Design, string)> invalidNames = new(); + var skipped = 0; + foreach (var file in _saveService.FileNames.Designs()) + { + try + { + var text = File.ReadAllText(file.FullName); + var data = JObject.Parse(text); + var design = Design.LoadDesign(_customizations, _items, data); + if (design.Identifier.ToString() != Path.GetFileNameWithoutExtension(file.Name)) + invalidNames.Add((design, file.FullName)); + if (_designs.Any(f => f.Identifier == design.Identifier)) + throw new Exception($"Identifier {design.Identifier} was not unique."); + + design.Index = _designs.Count; + _designs.Add(design); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not load design, skipped:\n{ex}"); + ++skipped; + } + } + + var failed = MoveInvalidNames(invalidNames); + if (invalidNames.Count > 0) + Glamourer.Log.Information( + $"Moved {invalidNames.Count - failed} designs to correct names.{(failed > 0 ? $" Failed to move {failed} designs to correct names." : string.Empty)}"); + + Glamourer.Log.Information( + $"Loaded {_designs.Count} designs.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}"); + _event.Invoke(DesignChanged.Type.ReloadedAll, null!); + } + + /// Create a new design of a given name. + public Design Create(string name) + { + var design = new Design(_items) + { + CreationDate = DateTimeOffset.UtcNow, + LastEdit = DateTimeOffset.UtcNow, + Identifier = CreateNewGuid(), + Index = _designs.Count, + Name = name, + }; + _designs.Add(design); + Glamourer.Log.Debug($"Added new design {design.Identifier}."); + _saveService.ImmediateSave(design); + _event.Invoke(DesignChanged.Type.Created, design); + return design; + } + + /// Delete a design. + public void Delete(Design design) + { + foreach (var d in _designs.Skip(design.Index + 1)) + --d.Index; + _designs.RemoveAt(design.Index); + _saveService.ImmediateDelete(design); + _event.Invoke(DesignChanged.Type.Deleted, design); + } + + /// Rename a design. + public void Rename(Design design, string newName) + { + var oldName = design.Name.Text; + if (oldName == newName) + return; + + design.Name = newName; + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Renamed design {design.Identifier}."); + _event.Invoke(DesignChanged.Type.Renamed, design, oldName); + } + + /// Change the description of a design. + public void ChangeDescription(Design design, string description) + { + var oldDescription = design.Description; + if (oldDescription == description) + return; + + design.Description = description; + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Changed description of design {design.Identifier}."); + _event.Invoke(DesignChanged.Type.ChangedDescription, design, oldDescription); + } + + /// Add a new tag to a design. The tags remain sorted. + public void AddTag(Design design, string tag) + { + if (design.Tags.Contains(tag)) + return; + + design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray(); + design.LastEdit = DateTimeOffset.UtcNow; + var idx = design.Tags.IndexOf(tag); + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}."); + _event.Invoke(DesignChanged.Type.AddedTag, design, (tag, idx)); + } + + /// Remove a tag from a design if it exists. + public void RemoveTag(Design design, string tag) + => RemoveTag(design, design.Tags.IndexOf(tag)); + + /// Remove a tag from a design by its index. + public void RemoveTag(Design design, int tagIdx) + { + if (tagIdx < 0 || tagIdx >= design.Tags.Length) + return; + + var oldTag = design.Tags[tagIdx]; + design.Tags = design.Tags.Take(tagIdx).Concat(design.Tags.Skip(tagIdx + 1)).ToArray(); + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}."); + _event.Invoke(DesignChanged.Type.RemovedTag, design, (oldTag, tagIdx)); + } + + /// Rename a tag from a design by its index. The tags stay sorted. + public void RenameTag(Design design, int tagIdx, string newTag) + { + var oldTag = design.Tags[tagIdx]; + if (oldTag == newTag) + return; + + design.Tags[tagIdx] = newTag; + Array.Sort(design.Tags); + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags."); + _event.Invoke(DesignChanged.Type.ChangedTag, design, (oldTag, newTag, tagIdx)); + } + + /// Change a customization value. + public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value) + { + var oldValue = design.DesignData.Customize[idx]; + switch (idx) + { + case CustomizeIndex.Race: + case CustomizeIndex.BodyType: + Glamourer.Log.Error("Somehow race or body type was changed in a design. This should not happen."); + return; + case CustomizeIndex.Clan: + if (!_customizations.ChangeClan(ref design.DesignData.Customize, (SubRace)value.Value)) + return; + + break; + case CustomizeIndex.Gender: + if (!_customizations.ChangeGender(ref design.DesignData.Customize, (Gender)(value.Value + 1))) + return; + + break; + default: + if (!design.DesignData.Customize.Set(idx, value)) + return; + + break; + } + + Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}."); + _saveService.QueueSave(design); + _event.Invoke(DesignChanged.Type.Customize, design, (oldValue, value, idx)); + } + + /// Change whether to apply a specific customize value. + public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value) + { + if (!design.SetApplyCustomize(idx, value)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Set applying of customization {idx.ToDefaultName()} to {value}."); + _event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx); + } + + /// Change a non-weapon equipment piece. + public void ChangeEquip(Design design, EquipSlot slot, EquipItem item) + { + if (_items.ValidateItem(slot, item.Id, out item).Length > 0) + return; + + var old = design.DesignData.Item(slot); + if (!design.DesignData.SetItem(slot, item)) + return; + + Glamourer.Log.Debug( + $"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id})."); + _saveService.QueueSave(design); + _event.Invoke(DesignChanged.Type.Equip, design, (old, item, slot)); + } + + /// Change a weapon. + public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item) + { + var currentMain = design.DesignData.Item(EquipSlot.MainHand); + var currentOff = design.DesignData.Item(EquipSlot.OffHand); + switch (slot) + { + case EquipSlot.MainHand: + var newOff = currentOff; + if (item.Type == currentMain.Type) + { + if (_items.ValidateWeapons(item.Id, currentOff.Id, out _, out _).Length != 0) + return; + } + else + { + var newOffId = FullEquipTypeExtensions.OffhandTypes.Contains(item.Type) + ? item.Id + : ItemManager.NothingId(item.Type.Offhand()); + if (_items.ValidateWeapons(item.Id, newOffId, out _, out newOff).Length != 0) + return; + } + + design.DesignData.SetItem(EquipSlot.MainHand, item); + design.DesignData.SetItem(EquipSlot.OffHand, newOff); + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug( + $"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.Id}) to {item.Name} ({item.Id})."); + _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff)); + return; + case EquipSlot.OffHand: + if (item.Type != currentOff.Type) + return; + if (_items.ValidateWeapons(currentMain.Id, item.Id, out _, out _).Length > 0) + return; + + if (!design.DesignData.SetItem(EquipSlot.OffHand, item)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug( + $"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.Id}) to {item.Name} ({item.Id})."); + _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, currentMain, item)); + return; + default: return; + } + } + + /// Change whether to apply a specific equipment piece. + public void ChangeApplyEquip(Design design, EquipSlot slot, bool value) + { + if (!design.SetApplyEquip(slot, value)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}."); + _event.Invoke(DesignChanged.Type.ApplyEquip, design, slot); + } + + /// Change the stain for any equipment piece. + public void ChangeStain(Design design, EquipSlot slot, StainId stain) + { + if (_items.ValidateStain(stain, out _).Length > 0) + return; + + var oldStain = design.DesignData.Stain(slot); + if (!design.DesignData.SetStain(slot, stain)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Value}."); + _event.Invoke(DesignChanged.Type.Stain, design, (oldStain, stain, slot)); + } + + /// Change whether to apply a specific stain. + public void ChangeApplyStain(Design design, EquipSlot slot, bool value) + { + if (!design.SetApplyStain(slot, value)) + return; + + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}."); + _event.Invoke(DesignChanged.Type.ApplyStain, design, slot); + } + + private void MigrateOldDesigns() + { + if (!File.Exists(_saveService.FileNames.MigrationDesignFile)) + return; + + var errors = 0; + var skips = 0; + var successes = 0; + try + { + var text = File.ReadAllText(_saveService.FileNames.MigrationDesignFile); + var dict = JsonConvert.DeserializeObject>(text) ?? new Dictionary(); + var migratedFileSystemPaths = new Dictionary(dict.Count); + foreach (var (name, base64) in dict) + { + try + { + var actualName = Path.GetFileName(name); + var design = new Design(_items) + { + CreationDate = File.GetCreationTimeUtc(_saveService.FileNames.MigrationDesignFile), + LastEdit = File.GetLastWriteTimeUtc(_saveService.FileNames.MigrationDesignFile), + Identifier = CreateNewGuid(), + Name = actualName, + }; + design.MigrateBase64(_items, base64); + if (!_designs.Any(d => d.Name == design.Name && d.CreationDate == design.CreationDate)) + { + Add(design, $"Migrated old design to {design.Identifier}."); + migratedFileSystemPaths.Add(design.Identifier.ToString(), name); + ++successes; + } + else + { + Glamourer.Log.Debug( + "Skipped migrating old design because a design of the same name and creation date already existed."); + ++skips; + } + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not migrate design {name}:\n{ex}"); + ++errors; + } + } + + DesignFileSystem.MigrateOldPaths(_saveService, migratedFileSystemPaths); + Glamourer.Log.Information( + $"Successfully migrated {successes} old designs. Skipped {skips} already migrated designs. Failed to migrate {errors} designs."); + } + catch (Exception e) + { + Glamourer.Log.Error($"Could not migrate old design file {_saveService.FileNames.MigrationDesignFile}:\n{e}"); + } + + try + { + File.Move(_saveService.FileNames.MigrationDesignFile, + Path.ChangeExtension(_saveService.FileNames.MigrationDesignFile, ".json.bak")); + Glamourer.Log.Information($"Moved migrated design file {_saveService.FileNames.MigrationDesignFile} to backup file."); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not move migrated design file {_saveService.FileNames.MigrationDesignFile} to backup file:\n{ex}"); + } + } + + /// Try to ensure existence of the design folder. + private static void CreateDesignFolder(SaveService service) + { + var ret = service.FileNames.DesignDirectory; + if (Directory.Exists(ret)) + return; + + try + { + Directory.CreateDirectory(ret); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not create design folder directory at {ret}:\n{ex}"); + } + } + + /// Move all files that were discovered to have names not corresponding to their identifier to correct names, if possible. + /// The number of files that could not be moved. + private int MoveInvalidNames(IEnumerable<(Design, string)> invalidNames) + { + var failed = 0; + foreach (var (design, name) in invalidNames) + { + try + { + var correctName = _saveService.FileNames.DesignFile(design); + File.Move(name, correctName, false); + Glamourer.Log.Information($"Moved invalid design file from {Path.GetFileName(name)} to {Path.GetFileName(correctName)}."); + } + catch (Exception ex) + { + ++failed; + Glamourer.Log.Error($"Failed to move invalid design file from {Path.GetFileName(name)}:\n{ex}"); + } + } + + return failed; + } + + /// Create new GUIDs until we have one that is not in use. + private Guid CreateNewGuid() + { + while (true) + { + var guid = Guid.NewGuid(); + if (_designs.All(d => d.Identifier != guid)) + return guid; + } + } + + /// + /// Try to add an external design to the list. + /// Returns false if the design is already contained or if the identifier is already in use. + /// The design is treated as newly created and invokes an event. + /// + private bool Add(Design design, string? message) + { + if (_designs.Any(d => d == design || d.Identifier == design.Identifier)) + return false; + + design.Index = _designs.Count; + _designs.Add(design); + if (!message.IsNullOrEmpty()) + Glamourer.Log.Debug(message); + _saveService.ImmediateSave(design); + _event.Invoke(DesignChanged.Type.Created, design); + return true; + } +} diff --git a/Glamourer/Events/DesignChanged.cs b/Glamourer/Events/DesignChanged.cs new file mode 100644 index 0000000..bce39f1 --- /dev/null +++ b/Glamourer/Events/DesignChanged.cs @@ -0,0 +1,76 @@ +using System; +using Glamourer.Designs; +using OtterGui.Classes; + +namespace Glamourer.Events; + +/// +/// Triggered when a Design is edited in any way. +/// +/// Parameter is the type of the change +/// Parameter is the changed Design. +/// Parameter is any additional data depending on the type of change. +/// +/// +public sealed class DesignChanged : EventWrapper, DesignChanged.Priority> +{ + public enum Type + { + /// A new design was created. Data is null. + Created, + + /// An existing design was deleted. Data is null. + Deleted, + + /// Invoked on full reload. Design and Data are null. + ReloadedAll, + + /// An existing design was renamed. Data is the prior name [string]. + Renamed, + + /// An existing design had its description changed. Data is the prior description [string]. + ChangedDescription, + + /// An existing design had a new tag added. Data is the new tag and the index it was added at [(string, int)]. + AddedTag, + + /// An existing design had an existing tag removed. Data is the removed tag and the index it had before removal [(string, int)]. + RemovedTag, + + /// An existing design had an existing tag renamed. Data is the old name of the tag, the new name of the tag, and the index it had before being resorted [(string, string, int)]. + ChangedTag, + + /// An existing design had a customization changed. Data is the old value, the new value and the type [(CustomizeValue, CustomizeValue, CustomizeIndex)]. + Customize, + + /// An existing design had an equipment piece changed. Data is the old value, the new value and the slot [(EquipItem, EquipItem, EquipSlot)]. + Equip, + + /// An existing design had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand and the new offhand [(EquipItem, EquipItem, EquipItem, EquipItem)]. + Weapon, + + /// An existing design had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. + Stain, + + /// An existing design changed whether a specific customization is applied. Data is the type of customization [CustomizeIndex]. + ApplyCustomize, + + /// An existing design changed whether a specific equipment is applied. Data is the slot of the equipment [EquipSlot]. + ApplyEquip, + + /// An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot]. + ApplyStain, + } + + public enum Priority + { + DesignFileSystem = 0, + } + + public DesignChanged() + : base(nameof(DesignChanged)) + { } + + public void Invoke(Type type, Design design, object? data = null) + => Invoke(this, type, design, data); +} diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs index 2b76348..8a98345 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -19,7 +19,6 @@ using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using static OtterGui.Raii.ImRaii; namespace Glamourer.Gui.Tabs; @@ -37,11 +36,15 @@ public unsafe class DebugTab : ITab private readonly ActorService _actors; private readonly CustomizationService _customization; + private readonly DesignManager _designManager; + private readonly DesignFileSystem _designFileSystem; + private int _gameObjectIndex; public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, ObjectTable objects, - UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra, IdentifierService identifier, - ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager) + UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra, + ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager, + DesignFileSystem designFileSystem, DesignManager designManager) { _changeCustomizeService = changeCustomizeService; _visorService = visorService; @@ -53,6 +56,8 @@ public unsafe class DebugTab : ITab _items = items; _customization = customization; _objectManager = objectManager; + _designFileSystem = designFileSystem; + _designManager = designManager; } public ReadOnlySpan Label @@ -63,7 +68,7 @@ public unsafe class DebugTab : ITab DrawInteropHeader(); DrawGameDataHeader(); DrawPenumbraHeader(); - DrawDesignManager(); + DrawDesigns(); } #region Interop @@ -79,14 +84,14 @@ public unsafe class DebugTab : ITab private void DrawModelEvaluation() { - using var tree = TreeNode("Model Evaluation"); + using var tree = ImRaii.TreeNode("Model Evaluation"); if (!tree) return; ImGui.InputInt("Game Object Index", ref _gameObjectIndex, 0, 0); var actor = (Actor)_objects.GetObjectAddress(_gameObjectIndex); var model = actor.Model; - using var table = Table("##evaluationTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + using var table = ImRaii.Table("##evaluationTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableHeader("Actor"); @@ -127,13 +132,13 @@ public unsafe class DebugTab : ITab private void DrawObjectManager() { - using var tree = TreeNode("Object Manager"); + using var tree = ImRaii.TreeNode("Object Manager"); if (!tree) return; _objectManager.Update(); - using (var table = Table("##data", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) + using (var table = ImRaii.Table("##data", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) { if (!table) return; @@ -169,7 +174,7 @@ public unsafe class DebugTab : ITab } var filterChanged = ImGui.InputTextWithHint("##Filter", "Filter...", ref _objectFilter, 64); - using var table2 = Table("##data2", 3, + using var table2 = ImRaii.Table("##data2", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.ScrollY, new Vector2(-1, 20 * ImGui.GetTextLineHeightWithSpacing())); if (!table2) @@ -195,7 +200,7 @@ public unsafe class DebugTab : ITab private void DrawVisor(Actor actor, Model model) { - using var id = PushId("Visor"); + using var id = ImRaii.PushId("Visor"); ImGuiUtil.DrawTableColumn("Visor State"); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->DrawData.IsVisorToggled.ToString() : "No Character"); ImGuiUtil.DrawTableColumn(model.IsHuman ? _visorService.GetVisorState(model).ToString() : "No Human"); @@ -215,7 +220,7 @@ public unsafe class DebugTab : ITab private void DrawHatState(Actor actor, Model model) { - using var id = PushId("HatState"); + using var id = ImRaii.PushId("HatState"); ImGuiUtil.DrawTableColumn("Hat State"); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->DrawData.IsHatHidden ? "Hidden" : actor.GetArmor(EquipSlot.Head).ToString() @@ -240,7 +245,7 @@ public unsafe class DebugTab : ITab private void DrawWeaponState(Actor actor, Model model) { - using var id = PushId("WeaponState"); + using var id = ImRaii.PushId("WeaponState"); ImGuiUtil.DrawTableColumn("Weapon State"); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->DrawData.IsWeaponHidden ? "Hidden" : "Visible" @@ -272,7 +277,7 @@ public unsafe class DebugTab : ITab private void DrawWetness(Actor actor, Model model) { - using var id = PushId("Wetness"); + using var id = ImRaii.PushId("Wetness"); ImGuiUtil.DrawTableColumn("Wetness"); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->IsGPoseWet ? "GPose" : "None" : "No Character"); var modelString = model.IsCharacterBase @@ -298,10 +303,10 @@ public unsafe class DebugTab : ITab private void DrawEquip(Actor actor, Model model) { - using var id = PushId("Equipment"); + using var id = ImRaii.PushId("Equipment"); foreach (var slot in EquipSlotExtensions.EqdpSlots) { - using var id2 = PushId((int)slot); + using var id2 = ImRaii.PushId((int)slot); ImGuiUtil.DrawTableColumn(slot.ToName()); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetArmor(slot).ToString() : "No Character"); ImGuiUtil.DrawTableColumn(model.IsHuman ? model.GetArmor(slot).ToString() : "No Human"); @@ -323,7 +328,7 @@ public unsafe class DebugTab : ITab private void DrawCustomize(Actor actor, Model model) { - using var id = PushId("Customize"); + using var id = ImRaii.PushId("Customize"); var actorCustomize = new Customize(actor.IsCharacter ? *(Penumbra.GameData.Structs.CustomizeData*)&actor.AsCharacter->DrawData.CustomizeData : new Penumbra.GameData.Structs.CustomizeData()); @@ -332,7 +337,7 @@ public unsafe class DebugTab : ITab : new Penumbra.GameData.Structs.CustomizeData()); foreach (var type in Enum.GetValues()) { - using var id2 = PushId((int)type); + using var id2 = ImRaii.PushId((int)type); ImGuiUtil.DrawTableColumn(type.ToDefaultName()); ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actorCustomize[type].Value.ToString("X2") : "No Character"); ImGuiUtil.DrawTableColumn(model.IsHuman ? modelCustomize[type].Value.ToString("X2") : "No Human"); @@ -373,7 +378,7 @@ public unsafe class DebugTab : ITab if (!ImGui.CollapsingHeader("Penumbra")) return; - using var table = Table("##PenumbraTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + using var table = ImRaii.Table("##PenumbraTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) return; @@ -410,7 +415,7 @@ public unsafe class DebugTab : ITab ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); ImGui.InputInt("##redrawObject", ref _gameObjectIndex, 0, 0); ImGui.TableNextColumn(); - using (var disabled = Disabled(!_penumbra.Available)) + using (var disabled = ImRaii.Disabled(!_penumbra.Available)) { if (ImGui.SmallButton("Redraw")) _penumbra.RedrawObject(_objects.GetObjectAddress(_gameObjectIndex), RedrawType.Redraw); @@ -441,8 +446,8 @@ public unsafe class DebugTab : ITab private void DrawIdentifierService() { - using var disabled = Disabled(!_items.IdentifierService.Valid); - using var tree = TreeNode("Identifier Service"); + using var disabled = ImRaii.Disabled(!_items.IdentifierService.Valid); + using var tree = ImRaii.TreeNode("Identifier Service"); if (!tree || !_items.IdentifierService.Valid) return; @@ -486,7 +491,7 @@ public unsafe class DebugTab : ITab private void DrawRestrictedGear() { - using var tree = TreeNode("Restricted Gear Service"); + using var tree = ImRaii.TreeNode("Restricted Gear Service"); if (!tree) return; @@ -537,8 +542,8 @@ public unsafe class DebugTab : ITab private void DrawActorService() { - using var disabled = Disabled(!_actors.Valid); - using var tree = TreeNode("Actor Service"); + using var disabled = ImRaii.Disabled(!_actors.Valid); + using var tree = ImRaii.TreeNode("Actor Service"); if (!tree || !_actors.Valid) return; @@ -554,14 +559,14 @@ public unsafe class DebugTab : ITab private static void DrawNameTable(string label, ref string filter, IEnumerable<(uint, string)> names) { - using var _ = PushId(label); - using var tree = TreeNode(label); + using var _ = ImRaii.PushId(label); + using var tree = ImRaii.TreeNode(label); if (!tree) return; var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref filter, 256); var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - using var table = Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter, + using var table = ImRaii.Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter, new Vector2(-1, 10 * height)); if (!table) return; @@ -588,13 +593,13 @@ public unsafe class DebugTab : ITab private void DrawItemService() { - using var disabled = Disabled(!_items.ItemService.Valid); - using var tree = TreeNode("Item Manager"); + using var disabled = ImRaii.Disabled(!_items.ItemService.Valid); + using var tree = ImRaii.TreeNode("Item Manager"); if (!tree || !_items.ItemService.Valid) return; disabled.Dispose(); - TreeNode($"Default Sword: {_items.DefaultSword.Name} ({_items.DefaultSword.Id}) ({_items.DefaultSword.Weapon()})", + ImRaii.TreeNode($"Default Sword: {_items.DefaultSword.Name} ({_items.DefaultSword.Id}) ({_items.DefaultSword.Weapon()})", ImGuiTreeNodeFlags.Leaf).Dispose(); DrawNameTable("All Items (Main)", ref _itemFilter, _items.ItemService.AwaitedService.AllItems(true).Select(p => (p.Item1, @@ -616,13 +621,13 @@ public unsafe class DebugTab : ITab private void DrawStainService() { - using var tree = TreeNode("Stain Service"); + using var tree = ImRaii.TreeNode("Stain Service"); if (!tree) return; var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref _stainFilter, 256); var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - using var table = Table("##table", 4, + using var table = ImRaii.Table("##table", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.SizingFixedFit, new Vector2(-1, 10 * height)); if (!table) @@ -652,8 +657,8 @@ public unsafe class DebugTab : ITab private void DrawCustomizationService() { - using var disabled = Disabled(!_customization.Valid); - using var tree = TreeNode("Customization Service"); + using var disabled = ImRaii.Disabled(!_customization.Valid); + using var tree = ImRaii.TreeNode("Customization Service"); if (!tree || !_customization.Valid) return; @@ -668,11 +673,11 @@ public unsafe class DebugTab : ITab private void DrawCustomizationInfo(CustomizationSet set) { - using var tree = TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender}"); + using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender}"); if (!tree) return; - using var table = Table("data", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + using var table = ImRaii.Table("data", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) return; @@ -697,11 +702,42 @@ public unsafe class DebugTab : ITab private DesignData _parse64 = new(); private Exception? _parse64Failure; - private void DrawDesignManager() + private void DrawDesigns() { if (!ImGui.CollapsingHeader("Designs")) return; + DrawDesignManager(); + DrawDesignTester(); + } + + private void DrawDesignManager() + { + using var tree = ImRaii.TreeNode($"Design Manager ({_designManager.Designs.Count} Designs)###Design Manager"); + if (!tree) + return; + + foreach (var (design, idx) in _designManager.Designs.WithIndex()) + { + using var t = ImRaii.TreeNode($"{design.Name}##{idx}"); + if (!t) + continue; + + DrawDesign(design); + var base64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.ApplyEquip, design.ApplyCustomize, design.DoApplyHatVisible(), + design.DoApplyVisorToggle(), design.DoApplyWeaponVisible(), design.WriteProtected()); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtil.TextWrapped(base64); + } + } + + private void DrawDesignTester() + { + using var tree = ImRaii.TreeNode("Base64 Design Tester"); + if (!tree) + return; + + ImGui.SetNextItemWidth(-1); ImGui.InputTextWithHint("##base64", "Base 64 input...", ref _base64, 2048); if (ImGui.IsItemDeactivatedAfterEdit()) { @@ -721,7 +757,7 @@ public unsafe class DebugTab : ITab { _parse64 = DesignBase64Migration.MigrateBase64(_items, _base64, out var ef, out var cf, out var wp, out var ah, out var av, out var aw); - _restore = DesignBase64Migration.CreateOldBase64(in _parse64, ef, cf, ah, av, wp, aw); + _restore = DesignBase64Migration.CreateOldBase64(in _parse64, ef, cf, ah, av, aw, wp); _restoreBytes = Convert.FromBase64String(_restore); } catch (Exception ex) @@ -737,14 +773,14 @@ public unsafe class DebugTab : ITab } else if (_restore.Length > 0) { - DrawDesignData(_parse64); - using var font = PushFont(UiBuilder.MonoFont); + DrawDesignData(_parse64, true); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); ImGui.TextUnformatted(_base64); - using (var style = PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 0 })) + using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 0 })) { foreach (var (c1, c2) in _restore.Zip(_base64)) { - using var color = PushColor(ImGuiCol.Text, 0xFF4040D0, c1 != c2); + using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF4040D0, c1 != c2); ImGui.TextUnformatted(c1.ToString()); ImGui.SameLine(); } @@ -754,11 +790,11 @@ public unsafe class DebugTab : ITab foreach (var ((b1, b2), idx) in _base64Bytes.Zip(_restoreBytes).WithIndex()) { - using (var group = Group()) + using (var group = ImRaii.Group()) { ImGui.TextUnformatted(idx.ToString("D2")); ImGui.TextUnformatted(b1.ToString("X2")); - using var color = PushColor(ImGuiCol.Text, 0xFF4040D0, b1 != b2); + using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF4040D0, b1 != b2); ImGui.TextUnformatted(b2.ToString("X2")); } @@ -768,10 +804,10 @@ public unsafe class DebugTab : ITab if (_parse64Failure != null && _base64Bytes.Length > 0) { - using var font = PushFont(UiBuilder.MonoFont); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); foreach (var (b, idx) in _base64Bytes.WithIndex()) { - using (var group = Group()) + using (var group = ImRaii.Group()) { ImGui.TextUnformatted(idx.ToString("D2")); ImGui.TextUnformatted(b.ToString("X2")); @@ -782,9 +818,9 @@ public unsafe class DebugTab : ITab } } - private static void DrawDesignData(in DesignData data) + private static void DrawDesignData(in DesignData data, bool createTable) { - using var table = Table("##equip", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + using var table = createTable ? ImRaii.Table("##equip", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit) : null; foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) { var item = data.Item(slot); @@ -822,5 +858,79 @@ public unsafe class DebugTab : ITab ImGui.TableNextRow(); } + private void DrawDesign(Design design) + { + using var table = ImRaii.Table("##equip", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + ImGuiUtil.DrawTableColumn("Name"); + ImGuiUtil.DrawTableColumn(design.Name); + ImGuiUtil.DrawTableColumn($"({design.Index})"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted("Description (Hover)"); + ImGuiUtil.HoverTooltip(design.Description); + ImGui.TableNextRow(); + + ImGuiUtil.DrawTableColumn("Identifier"); + ImGuiUtil.DrawTableColumn(design.Identifier.ToString()); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Design File System Path"); + ImGuiUtil.DrawTableColumn(_designFileSystem.FindLeaf(design, out var leaf) ? leaf.FullName() : "No Path Known"); + ImGui.TableNextRow(); + + ImGuiUtil.DrawTableColumn("Creation"); + ImGuiUtil.DrawTableColumn(design.CreationDate.ToString()); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Update"); + ImGuiUtil.DrawTableColumn(design.LastEdit.ToString()); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Tags"); + ImGuiUtil.DrawTableColumn(string.Join(", ", design.Tags)); + ImGui.TableNextRow(); + + foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand)) + { + var item = design.DesignData.Item(slot); + var apply = design.DoApplyEquip(slot); + var stain = design.DesignData.Stain(slot); + var applyStain = design.DoApplyStain(slot); + ImGuiUtil.DrawTableColumn(slot.ToName()); + ImGuiUtil.DrawTableColumn(item.Name); + ImGuiUtil.DrawTableColumn(item.Id.ToString()); + ImGuiUtil.DrawTableColumn(apply ? "Apply" : "Keep"); + ImGuiUtil.DrawTableColumn(stain.ToString()); + ImGuiUtil.DrawTableColumn(applyStain ? "Apply" : "Keep"); + } + + ImGuiUtil.DrawTableColumn("Hat Visible"); + ImGuiUtil.DrawTableColumn(design.DesignData.IsHatVisible().ToString()); + ImGuiUtil.DrawTableColumn(design.DoApplyHatVisible() ? "Apply" : "Keep"); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Visor Toggled"); + ImGuiUtil.DrawTableColumn(design.DesignData.IsVisorToggled().ToString()); + ImGuiUtil.DrawTableColumn(design.DoApplyVisorToggle() ? "Apply" : "Keep"); + ImGui.TableNextRow(); + ImGuiUtil.DrawTableColumn("Weapon Visible"); + ImGuiUtil.DrawTableColumn(design.DesignData.IsWeaponVisible().ToString()); + ImGuiUtil.DrawTableColumn(design.DoApplyWeaponVisible() ? "Apply" : "Keep"); + ImGui.TableNextRow(); + + ImGuiUtil.DrawTableColumn("Model ID"); + ImGuiUtil.DrawTableColumn(design.DesignData.ModelId.ToString()); + ImGui.TableNextRow(); + + foreach (var index in Enum.GetValues()) + { + var value = design.DesignData.Customize[index]; + var apply = design.DoApplyCustomize(index); + ImGuiUtil.DrawTableColumn(index.ToDefaultName()); + ImGuiUtil.DrawTableColumn(value.Value.ToString()); + ImGuiUtil.DrawTableColumn(apply ? "Apply" : "Keep"); + ImGui.TableNextRow(); + } + + ImGuiUtil.DrawTableColumn("Is Wet"); + ImGuiUtil.DrawTableColumn(design.DesignData.IsWet().ToString()); + ImGui.TableNextRow(); + } + #endregion } diff --git a/Glamourer/Services/CustomizationService.cs b/Glamourer/Services/CustomizationService.cs new file mode 100644 index 0000000..8611090 --- /dev/null +++ b/Glamourer/Services/CustomizationService.cs @@ -0,0 +1,211 @@ +using System; +using System.Linq; +using System.Security.AccessControl; +using Dalamud.Data; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin; +using Glamourer.Customization; +using Penumbra.GameData.Enums; + +namespace Glamourer.Services; + +public sealed class CustomizationService : AsyncServiceWrapper +{ + public CustomizationService(DalamudPluginInterface pi, DataManager gameData) + : base(nameof(CustomizationService), () => CustomizationManager.Create(pi, gameData)) + { } + + /// In languages other than english the actual clan name may depend on gender. + public string ClanName(SubRace race, Gender gender) + { + if (gender == Gender.FemaleNpc) + gender = Gender.Female; + if (gender == Gender.MaleNpc) + gender = Gender.Male; + return (gender, race) switch + { + (Gender.Male, SubRace.Midlander) => AwaitedService.GetName(CustomName.MidlanderM), + (Gender.Male, SubRace.Highlander) => AwaitedService.GetName(CustomName.HighlanderM), + (Gender.Male, SubRace.Wildwood) => AwaitedService.GetName(CustomName.WildwoodM), + (Gender.Male, SubRace.Duskwight) => AwaitedService.GetName(CustomName.DuskwightM), + (Gender.Male, SubRace.Plainsfolk) => AwaitedService.GetName(CustomName.PlainsfolkM), + (Gender.Male, SubRace.Dunesfolk) => AwaitedService.GetName(CustomName.DunesfolkM), + (Gender.Male, SubRace.SeekerOfTheSun) => AwaitedService.GetName(CustomName.SeekerOfTheSunM), + (Gender.Male, SubRace.KeeperOfTheMoon) => AwaitedService.GetName(CustomName.KeeperOfTheMoonM), + (Gender.Male, SubRace.Seawolf) => AwaitedService.GetName(CustomName.SeawolfM), + (Gender.Male, SubRace.Hellsguard) => AwaitedService.GetName(CustomName.HellsguardM), + (Gender.Male, SubRace.Raen) => AwaitedService.GetName(CustomName.RaenM), + (Gender.Male, SubRace.Xaela) => AwaitedService.GetName(CustomName.XaelaM), + (Gender.Male, SubRace.Helion) => AwaitedService.GetName(CustomName.HelionM), + (Gender.Male, SubRace.Lost) => AwaitedService.GetName(CustomName.LostM), + (Gender.Male, SubRace.Rava) => AwaitedService.GetName(CustomName.RavaM), + (Gender.Male, SubRace.Veena) => AwaitedService.GetName(CustomName.VeenaM), + (Gender.Female, SubRace.Midlander) => AwaitedService.GetName(CustomName.MidlanderF), + (Gender.Female, SubRace.Highlander) => AwaitedService.GetName(CustomName.HighlanderF), + (Gender.Female, SubRace.Wildwood) => AwaitedService.GetName(CustomName.WildwoodF), + (Gender.Female, SubRace.Duskwight) => AwaitedService.GetName(CustomName.DuskwightF), + (Gender.Female, SubRace.Plainsfolk) => AwaitedService.GetName(CustomName.PlainsfolkF), + (Gender.Female, SubRace.Dunesfolk) => AwaitedService.GetName(CustomName.DunesfolkF), + (Gender.Female, SubRace.SeekerOfTheSun) => AwaitedService.GetName(CustomName.SeekerOfTheSunF), + (Gender.Female, SubRace.KeeperOfTheMoon) => AwaitedService.GetName(CustomName.KeeperOfTheMoonF), + (Gender.Female, SubRace.Seawolf) => AwaitedService.GetName(CustomName.SeawolfF), + (Gender.Female, SubRace.Hellsguard) => AwaitedService.GetName(CustomName.HellsguardF), + (Gender.Female, SubRace.Raen) => AwaitedService.GetName(CustomName.RaenF), + (Gender.Female, SubRace.Xaela) => AwaitedService.GetName(CustomName.XaelaF), + (Gender.Female, SubRace.Helion) => AwaitedService.GetName(CustomName.HelionM), + (Gender.Female, SubRace.Lost) => AwaitedService.GetName(CustomName.LostM), + (Gender.Female, SubRace.Rava) => AwaitedService.GetName(CustomName.RavaF), + (Gender.Female, SubRace.Veena) => AwaitedService.GetName(CustomName.VeenaF), + _ => "Unknown", + }; + } + + /// + /// Check that the given race and clan are valid. + /// The returned race and clan fit together and are valid. + /// The return value is an empty string if everything was correct and a warning otherwise. + /// + public string ValidateClan(SubRace clan, Race race, out Race actualRace, out SubRace actualClan) + { + if (AwaitedService.Clans.Contains(clan)) + { + actualClan = clan; + actualRace = actualClan.ToRace(); + if (race != actualRace) + return $"The race {race.ToName()} does not correspond to the clan {clan.ToName()}, changed to {actualRace.ToName()}."; + + return string.Empty; + } + + if (AwaitedService.Races.Contains(race)) + { + actualRace = race; + actualClan = AwaitedService.Clans.FirstOrDefault(c => c.ToRace() == race, SubRace.Unknown); + // This should not happen. + if (actualClan == SubRace.Unknown) + { + actualRace = Race.Hyur; + actualClan = SubRace.Midlander; + return + $"The clan {clan.ToName()} is invalid and the race {race.ToName()} does not correspond to any clan, reset to {Race.Hyur.ToName()} {SubRace.Midlander.ToName()}."; + } + + return $"The clan {clan.ToName()} is invalid, but the race {race.ToName()} is known, reset to {actualClan.ToName()}."; + } + + actualRace = Race.Hyur; + actualClan = SubRace.Midlander; + return + $"Both the clan {clan.ToName()} and the race {race.ToName()} are invalid, reset to {Race.Hyur.ToName()} {SubRace.Midlander.ToName()}."; + } + + /// + /// Check that the given gender is valid for that race. + /// The returned gender is valid for the race. + /// The return value is an empty string if everything was correct and a warning otherwise. + /// + public string ValidateGender(Race race, Gender gender, out Gender actualGender) + { + if (!AwaitedService.Genders.Contains(gender)) + { + actualGender = Gender.Male; + return $"The gender {gender.ToName()} is unknown, reset to {Gender.Male.ToName()}."; + } + + // TODO: Female Hrothgar + if (gender == Gender.Female && race == Race.Hrothgar) + { + actualGender = Gender.Male; + return $"{Race.Hrothgar.ToName()} do not currently support {Gender.Female.ToName()} characters, reset to {Gender.Male.ToName()}."; + } + + actualGender = gender; + return string.Empty; + } + + /// + /// Check that the given model id is valid. + /// The returned model id is 0. + /// The return value is an empty string if everything was correct and a warning otherwise. + /// + public string ValidateModelId(uint modelId, out uint actualModelId) + { + actualModelId = 0; + return modelId != 0 ? $"Model IDs different from 0 are not currently allowed, reset {modelId} to 0." : string.Empty; + } + + + /// + /// Validate a single customization value against a given set of race and gender (and face). + /// The returned actualValue is either the correct value or the one with index 0. + /// The return value is an empty string or a warning message. + /// + public static string ValidateCustomizeValue(CustomizationSet set, CustomizeValue face, CustomizeIndex index, CustomizeValue value, + out CustomizeValue actualValue) + { + var count = set.Count(index, face); + var idx = set.DataByValue(index, value, out var data, face); + if (idx >= 0 && idx < count) + { + actualValue = value; + return string.Empty; + } + + var name = set.Option(index); + var newValue = set.Data(index, 0, face); + actualValue = newValue.Value; + return + $"Customization {name} for {set.Race.ToName()} {set.Gender.ToName()}s does not support value {value.Value}, reset to {newValue.Value.Value}."; + } + + /// Change a clan while keeping all other customizations valid. + public bool ChangeClan(ref Customize customize, SubRace newClan) + { + if (customize.Clan == newClan) + return false; + + if (ValidateClan(newClan, newClan.ToRace(), out var newRace, out newClan).Length > 0) + return false; + + customize.Race = newRace; + customize.Clan = newClan; + + // TODO Female Hrothgar + if (newRace == Race.Hrothgar) + customize.Gender = Gender.Male; + + var set = AwaitedService.GetList(customize.Clan, customize.Gender); + FixValues(set, ref customize); + + return true; + } + + /// Change a gender while keeping all other customizations valid. + public bool ChangeGender(ref Customize customize, Gender newGender) + { + if (customize.Gender == newGender) + return false; + + // TODO Female Hrothgar + if (customize.Race is Race.Hrothgar) + return false; + + if (ValidateGender(customize.Race, newGender, out newGender).Length > 0) + return false; + + customize.Gender = newGender; + var set = AwaitedService.GetList(customize.Clan, customize.Gender); + FixValues(set, ref customize); + + return true; + } + + private static void FixValues(CustomizationSet set, ref Customize customize) + { + foreach (var idx in Enum.GetValues().Where(set.IsAvailable)) + { + if (ValidateCustomizeValue(set, customize.Face, idx, customize[idx], out var fixedValue).Length > 0) + customize[idx] = fixedValue; + } + } +} diff --git a/Glamourer/Services/FilenameService.cs b/Glamourer/Services/FilenameService.cs index 48c75a4..344bb6b 100644 --- a/Glamourer/Services/FilenameService.cs +++ b/Glamourer/Services/FilenameService.cs @@ -22,6 +22,7 @@ public class FilenameService DesignDirectory = Path.Combine(ConfigDirectory, "designs"); } + public IEnumerable Designs() { if (!Directory.Exists(DesignDirectory)) diff --git a/Glamourer/Services/ItemManager.cs b/Glamourer/Services/ItemManager.cs index afe0765..befac06 100644 --- a/Glamourer/Services/ItemManager.cs +++ b/Glamourer/Services/ItemManager.cs @@ -75,7 +75,7 @@ public class ItemManager : IDisposable if (itemId == SmallclothesId(slot)) return SmallClothesItem(slot); - if (!ItemService.AwaitedService.TryGetValue(itemId, slot is EquipSlot.MainHand, out var item)) + if (!ItemService.AwaitedService.TryGetValue(itemId, slot is not EquipSlot.OffHand, out var item)) return new EquipItem(string.Intern($"Unknown #{itemId}"), itemId, 0, 0, 0, 0, 0); if (item.Type.ToSlot() != slot) @@ -134,4 +134,88 @@ public class ItemManager : IDisposable ? item : new EquipItem($"Unknown ({id.Value}-{type.Value}-{variant})", 0, 0, id, type, variant, 0); } + + /// + /// Check whether an item id resolves to an existing item of the correct slot (which should not be weapons.) + /// The returned item is either the resolved correct item, or the Nothing item for that slot. + /// The return value is an empty string if there was no problem and a warning otherwise. + /// + public string ValidateItem(EquipSlot slot, uint itemId, out EquipItem item) + { + if (slot is EquipSlot.MainHand or EquipSlot.OffHand) + throw new Exception("Internal Error: Used armor functionality for weapons."); + + item = Resolve(slot, itemId); + if (item.Valid) + return string.Empty; + + item = NothingItem(slot); + return $"The {slot.ToName()} item {itemId} does not exist, reset to Nothing."; + } + + /// + /// Check whether a stain id is an existing stain. + /// The returned stain id is either the input or 0. + /// The return value is an empty string if there was no problem and a warning otherwise. + /// + public string ValidateStain(StainId stain, out StainId ret) + { + if (stain.Value == 0 || Stains.ContainsKey(stain)) + { + ret = stain; + return string.Empty; + } + + ret = 0; + return $"The Stain {stain} does not exist, reset to unstained."; + } + + /// + /// Check whether a combination of an item id for a mainhand and for an offhand is valid. + /// The returned items are either the resolved correct items, + /// the correct mainhand and an appropriate offhand (implicit offhand or nothing), + /// or the default sword and a nothing offhand. + /// The return value is an empty string if there was no problem and a warning otherwise. + /// + public string ValidateWeapons(uint mainId, uint offId, out EquipItem main, out EquipItem off) + { + var ret = string.Empty; + main = Resolve(EquipSlot.MainHand, mainId); + if (!main.Valid) + { + main = DefaultSword; + ret = $"The mainhand weapon {mainId} does not exist, reset to default sword."; + } + + var offhandType = main.Type.Offhand(); + off = Resolve(offhandType, offId); + if (off.Valid) + return ret; + + // Try implicit offhand. + off = Resolve(offhandType, mainId); + if (off.Valid) + { + // Can not be set to default sword before because then it could not be valid. + ret = $"The offhand weapon {offId} does not exist, reset to implied offhand."; + } + else + { + if (FullEquipTypeExtensions.OffhandTypes.Contains(offhandType)) + { + main = DefaultSword; + off = NothingItem(FullEquipType.Shield); + ret = + $"The offhand weapon {offId} does not exist, but no default could be restored, reset mainhand to default sword and offhand to nothing."; + } + else + { + off = NothingItem(offhandType); + if (ret.Length == 0) + ret = $"The offhand weapon {offId} does not exist, reset to no offhand."; + } + } + + return ret; + } } diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 763034c..742c2e8 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -1,4 +1,5 @@ using Dalamud.Plugin; +using Glamourer.Designs; using Glamourer.Events; using Glamourer.Gui; using Glamourer.Gui.Tabs; @@ -21,6 +22,7 @@ public static class ServiceManager .AddInterop() .AddEvents() .AddData() + .AddDesigns() .AddUi() .AddApi(); @@ -44,7 +46,8 @@ public static class ServiceManager private static IServiceCollection AddEvents(this IServiceCollection services) => services.AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddData(this IServiceCollection services) => services.AddSingleton() @@ -61,6 +64,10 @@ public static class ServiceManager .AddSingleton() .AddSingleton(); + private static IServiceCollection AddDesigns(this IServiceCollection services) + => services.AddSingleton() + .AddSingleton(); + private static IServiceCollection AddUi(this IServiceCollection services) => services.AddSingleton() .AddSingleton() diff --git a/Glamourer/Services/ServiceWrapper.cs b/Glamourer/Services/ServiceWrapper.cs index 22656de..b8292a2 100644 --- a/Glamourer/Services/ServiceWrapper.cs +++ b/Glamourer/Services/ServiceWrapper.cs @@ -7,11 +7,9 @@ using Penumbra.GameData.Actors; using System; using System.Threading.Tasks; using Dalamud.Game; -using Glamourer.Customization; using Glamourer.Interop.Penumbra; using Penumbra.GameData.Data; using Penumbra.GameData; -using Penumbra.GameData.Enums; namespace Glamourer.Services; @@ -96,56 +94,4 @@ public sealed class ActorService : AsyncServiceWrapper : base(nameof(ActorService), () => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)penumbra.CutsceneParent(idx))) { } -} - -public sealed class CustomizationService : AsyncServiceWrapper -{ - public CustomizationService(DalamudPluginInterface pi, DataManager gameData) - : base(nameof(CustomizationService), () => CustomizationManager.Create(pi, gameData)) - { } - - /// In languages other than english the actual clan name may depend on gender. - public string ClanName(SubRace race, Gender gender) - { - if (gender == Gender.FemaleNpc) - gender = Gender.Female; - if (gender == Gender.MaleNpc) - gender = Gender.Male; - return (gender, race) switch - { - (Gender.Male, SubRace.Midlander) => AwaitedService.GetName(CustomName.MidlanderM), - (Gender.Male, SubRace.Highlander) => AwaitedService.GetName(CustomName.HighlanderM), - (Gender.Male, SubRace.Wildwood) => AwaitedService.GetName(CustomName.WildwoodM), - (Gender.Male, SubRace.Duskwight) => AwaitedService.GetName(CustomName.DuskwightM), - (Gender.Male, SubRace.Plainsfolk) => AwaitedService.GetName(CustomName.PlainsfolkM), - (Gender.Male, SubRace.Dunesfolk) => AwaitedService.GetName(CustomName.DunesfolkM), - (Gender.Male, SubRace.SeekerOfTheSun) => AwaitedService.GetName(CustomName.SeekerOfTheSunM), - (Gender.Male, SubRace.KeeperOfTheMoon) => AwaitedService.GetName(CustomName.KeeperOfTheMoonM), - (Gender.Male, SubRace.Seawolf) => AwaitedService.GetName(CustomName.SeawolfM), - (Gender.Male, SubRace.Hellsguard) => AwaitedService.GetName(CustomName.HellsguardM), - (Gender.Male, SubRace.Raen) => AwaitedService.GetName(CustomName.RaenM), - (Gender.Male, SubRace.Xaela) => AwaitedService.GetName(CustomName.XaelaM), - (Gender.Male, SubRace.Helion) => AwaitedService.GetName(CustomName.HelionM), - (Gender.Male, SubRace.Lost) => AwaitedService.GetName(CustomName.LostM), - (Gender.Male, SubRace.Rava) => AwaitedService.GetName(CustomName.RavaM), - (Gender.Male, SubRace.Veena) => AwaitedService.GetName(CustomName.VeenaM), - (Gender.Female, SubRace.Midlander) => AwaitedService.GetName(CustomName.MidlanderF), - (Gender.Female, SubRace.Highlander) => AwaitedService.GetName(CustomName.HighlanderF), - (Gender.Female, SubRace.Wildwood) => AwaitedService.GetName(CustomName.WildwoodF), - (Gender.Female, SubRace.Duskwight) => AwaitedService.GetName(CustomName.DuskwightF), - (Gender.Female, SubRace.Plainsfolk) => AwaitedService.GetName(CustomName.PlainsfolkF), - (Gender.Female, SubRace.Dunesfolk) => AwaitedService.GetName(CustomName.DunesfolkF), - (Gender.Female, SubRace.SeekerOfTheSun) => AwaitedService.GetName(CustomName.SeekerOfTheSunF), - (Gender.Female, SubRace.KeeperOfTheMoon) => AwaitedService.GetName(CustomName.KeeperOfTheMoonF), - (Gender.Female, SubRace.Seawolf) => AwaitedService.GetName(CustomName.SeawolfF), - (Gender.Female, SubRace.Hellsguard) => AwaitedService.GetName(CustomName.HellsguardF), - (Gender.Female, SubRace.Raen) => AwaitedService.GetName(CustomName.RaenF), - (Gender.Female, SubRace.Xaela) => AwaitedService.GetName(CustomName.XaelaF), - (Gender.Female, SubRace.Helion) => AwaitedService.GetName(CustomName.HelionM), - (Gender.Female, SubRace.Lost) => AwaitedService.GetName(CustomName.LostM), - (Gender.Female, SubRace.Rava) => AwaitedService.GetName(CustomName.RavaF), - (Gender.Female, SubRace.Veena) => AwaitedService.GetName(CustomName.VeenaF), - _ => "Unknown", - }; - } -} +} \ No newline at end of file diff --git a/GlamourerOld/Designs/DesignManager.cs b/GlamourerOld/Designs/DesignManager.cs index 6532769..021d749 100644 --- a/GlamourerOld/Designs/DesignManager.cs +++ b/GlamourerOld/Designs/DesignManager.cs @@ -19,9 +19,10 @@ public class DesignManager public const string DesignFolderName = "designs"; public readonly string DesignFolder; - private readonly ItemManager _items; - private readonly SaveService _saveService; - private readonly List _designs = new(); + private readonly CustomizationManager _customizations; + private readonly ItemManager _items; + private readonly SaveService _saveService; + private readonly List _designs = new(); public enum DesignChangeType {