diff --git a/Glamourer.GameData/Customization/Customize.cs b/Glamourer.GameData/Customization/Customize.cs index 62f2e89..a43adbe 100644 --- a/Glamourer.GameData/Customization/Customize.cs +++ b/Glamourer.GameData/Customization/Customize.cs @@ -5,61 +5,53 @@ namespace Glamourer.Customization; public unsafe struct Customize { - public readonly Penumbra.GameData.Structs.CustomizeData* Data; + public Penumbra.GameData.Structs.CustomizeData Data; - public Customize(Penumbra.GameData.Structs.CustomizeData* data) + public Customize(in Penumbra.GameData.Structs.CustomizeData data) => Data = data; - public Customize(ref Penumbra.GameData.Structs.CustomizeData data) - { - fixed (Penumbra.GameData.Structs.CustomizeData* ptr = &data) - { - Data = ptr; - } - } - public Race Race { - get => (Race)Data->Get(CustomizeIndex.Race).Value; - set => Data->Set(CustomizeIndex.Race, (CustomizeValue)(byte)value); + get => (Race)Data.Get(CustomizeIndex.Race).Value; + set => Data.Set(CustomizeIndex.Race, (CustomizeValue)(byte)value); } public Gender Gender { - get => (Gender)Data->Get(CustomizeIndex.Gender).Value + 1; - set => Data->Set(CustomizeIndex.Gender, (CustomizeValue)(byte)value - 1); + get => (Gender)Data.Get(CustomizeIndex.Gender).Value + 1; + set => Data.Set(CustomizeIndex.Gender, (CustomizeValue)(byte)value - 1); } public CustomizeValue BodyType { - get => Data->Get(CustomizeIndex.BodyType); - set => Data->Set(CustomizeIndex.BodyType, value); + get => Data.Get(CustomizeIndex.BodyType); + set => Data.Set(CustomizeIndex.BodyType, value); } public SubRace Clan { - get => (SubRace)Data->Get(CustomizeIndex.Clan).Value; - set => Data->Set(CustomizeIndex.Clan, (CustomizeValue)(byte)value); + get => (SubRace)Data.Get(CustomizeIndex.Clan).Value; + set => Data.Set(CustomizeIndex.Clan, (CustomizeValue)(byte)value); } public CustomizeValue Face { - get => Data->Get(CustomizeIndex.Face); - set => Data->Set(CustomizeIndex.Face, value); + get => Data.Get(CustomizeIndex.Face); + set => Data.Set(CustomizeIndex.Face, value); } - public static readonly Penumbra.GameData.Structs.CustomizeData Default = GenerateDefault(); - public static readonly Penumbra.GameData.Structs.CustomizeData Empty = new(); + public static readonly Customize Default = GenerateDefault(); + public static readonly Customize Empty = new(); public CustomizeValue Get(CustomizeIndex index) - => Data->Get(index); + => Data.Get(index); - public void Set(CustomizeIndex flag, CustomizeValue index) - => Data->Set(flag, index); + public bool Set(CustomizeIndex flag, CustomizeValue index) + => Data.Set(flag, index); public bool Equals(Customize other) - => Penumbra.GameData.Structs.CustomizeData.Equals(Data, other.Data); + => Equals(Data, other.Data); public CustomizeValue this[CustomizeIndex index] { @@ -67,9 +59,9 @@ public unsafe struct Customize set => Set(index, value); } - private static Penumbra.GameData.Structs.CustomizeData GenerateDefault() + private static Customize GenerateDefault() { - var ret = new Penumbra.GameData.Structs.CustomizeData(); + var ret = new Customize(); ret.Set(CustomizeIndex.BodyType, (CustomizeValue)1); ret.Set(CustomizeIndex.Height, (CustomizeValue)50); ret.Set(CustomizeIndex.Face, (CustomizeValue)1); @@ -94,16 +86,16 @@ 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); + public void Write(nint target) + => Data.Write((void*)target); public bool LoadBase64(string data) - => Data->LoadBase64(data); + => Data.LoadBase64(data); public string WriteBase64() - => Data->WriteBase64(); + => Data.WriteBase64(); public static CustomizeFlag Compare(Customize lhs, Customize rhs) { diff --git a/Glamourer/Api/GlamourerIpc.cs b/Glamourer/Api/GlamourerIpc.cs index 6337895..1bdd18a 100644 --- a/Glamourer/Api/GlamourerIpc.cs +++ b/Glamourer/Api/GlamourerIpc.cs @@ -5,7 +5,9 @@ using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using System; +using Glamourer.Api; using Glamourer.Designs; +using Glamourer.Services; using Glamourer.State; using Penumbra.Api.Enums; @@ -30,6 +32,10 @@ public partial class Glamourer private readonly ObjectTable _objectTable; private readonly DalamudPluginInterface _pluginInterface; + private readonly ActiveDesign.Manager _stateManager; + private readonly ItemManager _items; + private readonly PenumbraAttach _penumbra; + private readonly ActorService _actors; internal ICallGateProvider? ProviderGetAllCustomization; internal ICallGateProvider? ProviderGetAllCustomizationFromCharacter; @@ -43,10 +49,15 @@ public partial class Glamourer internal ICallGateProvider? ProviderRevertCharacter; internal ICallGateProvider? ProviderGetApiVersion; - public GlamourerIpc(ObjectTable objectTable, DalamudPluginInterface pluginInterface) + public GlamourerIpc(ObjectTable objectTable, DalamudPluginInterface pluginInterface, ActiveDesign.Manager stateManager, + ItemManager items, PenumbraAttach penumbra, ActorService actors) { _objectTable = objectTable; _pluginInterface = pluginInterface; + _stateManager = stateManager; + _items = items; + _penumbra = penumbra; + _actors = actors; InitializeProviders(); } @@ -211,9 +222,9 @@ public partial class Glamourer if (character == null) return; - //var design = Design.CreateTemporaryFromBase64(customization, true, true); - //var active = _glamourer._stateManager.GetOrCreateSave(character.Address); - //_glamourer._stateManager.ApplyDesign(active, design, false); + var design = Design.CreateTemporaryFromBase64(_items, customization, true, true); + var active = _stateManager.GetOrCreateSave(character.Address); + _stateManager.ApplyDesign(active, design, false); } private void ApplyOnlyCustomization(string customization, string characterName) @@ -232,20 +243,21 @@ public partial class Glamourer { if (character == null) return; - //var design = Design.CreateTemporaryFromBase64(customization, true, false); - //var active = _glamourer._stateManager.GetOrCreateSave(character.Address); - //_glamourer._stateManager.ApplyDesign(active, design, false); + + var design = Design.CreateTemporaryFromBase64(_items, customization, true, false); + var active = _stateManager.GetOrCreateSave(character.Address); + _stateManager.ApplyDesign(active, design, false); } private void ApplyOnlyEquipment(string customization, string characterName) { foreach (var gameObject in _objectTable) { - if (gameObject.Name.ToString() == characterName) - { - ApplyOnlyEquipment(customization, gameObject as Character); - return; - } + if (gameObject.Name.ToString() != characterName) + continue; + + ApplyOnlyEquipment(customization, gameObject as Character); + return; } } @@ -253,9 +265,10 @@ public partial class Glamourer { if (character == null) return; - //var design = Design.CreateTemporaryFromBase64(customization, false, true); - //var active = _glamourer._stateManager.GetOrCreateSave(character.Address); - //_glamourer._stateManager.ApplyDesign(active, design, false); + + var design = Design.CreateTemporaryFromBase64(_items, customization, false, true); + var active = _stateManager.GetOrCreateSave(character.Address); + _stateManager.ApplyDesign(active, design, false); } private void Revert(string characterName) @@ -274,9 +287,9 @@ public partial class Glamourer if (character == null) return; - //var ident = Actors.FromObject(character, true, false, false); - //_glamourer._stateManager.DeleteSave(ident); - //_glamourer._penumbra.RedrawObject(character.Address, RedrawType.Redraw); + var ident = _actors.AwaitedService.FromObject(character, true, false, false); + _stateManager.DeleteSave(ident); + _penumbra.RedrawObject(character.Address, RedrawType.Redraw); } private string? GetAllCustomization(Character? character) @@ -284,12 +297,11 @@ public partial class Glamourer if (character == null) return null; - //var ident = Actors.FromObject(character, true, false, false); - //if (!_glamourer._stateManager.TryGetValue(ident, out var design)) - // design = new ActiveDesign(ident, character.Address); - // - //return design.CreateOldBase64(); - return null; + var ident = _actors.AwaitedService.FromObject(character, true, false, false); + if (!_stateManager.TryGetValue(ident, out var design)) + design = new ActiveDesign(_items, ident, character.Address); + + return design.CreateOldBase64(); } private string? GetAllCustomization(string characterName) diff --git a/Glamourer/Api/PenumbraAttach.cs b/Glamourer/Api/PenumbraAttach.cs index 0026689..20fe9f9 100644 --- a/Glamourer/Api/PenumbraAttach.cs +++ b/Glamourer/Api/PenumbraAttach.cs @@ -9,11 +9,6 @@ using Penumbra.Api.Helpers; namespace Glamourer.Api; -public class CommunicatorService -{ - -} - public unsafe class PenumbraAttach : IDisposable { public const int RequiredPenumbraBreakingVersion = 4; diff --git a/Glamourer/Designs/CharacterSave.cs b/Glamourer/Designs/CharacterSave.cs deleted file mode 100644 index 81102d7..0000000 --- a/Glamourer/Designs/CharacterSave.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Runtime.InteropServices; -using Glamourer.Customization; -using Glamourer.Interop; -using Penumbra.GameData.Structs; -using Penumbra.String.Functions; -using CustomizeData = Penumbra.GameData.Structs.CustomizeData; - -namespace Glamourer.Designs; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public struct CharacterData -{ - public uint ModelId; - 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 Wrists; - 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, - 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, - Wrists = CharacterArmor.Empty, - RFinger = CharacterArmor.Empty, - LFinger = CharacterArmor.Empty, - }; - - public readonly unsafe CharacterData Clone() - { - var data = new CharacterData(); - fixed (void* ptr = &this) - { - MemoryUtility.MemCpyUnchecked(&data, ptr, sizeof(CharacterData)); - } - - return data; - } - - public void Load(IDesignable designable) - { - ModelId = designable.ModelId; - Customize.Load(designable.Customize); - Equipment.Load(designable.Equip); - MainHand = designable.MainHand; - OffHand = designable.OffHand; - } -} diff --git a/Glamourer/Designs/Design.Manager.cs b/Glamourer/Designs/Design.Manager.cs index 262b9c1..6532769 100644 --- a/Glamourer/Designs/Design.Manager.cs +++ b/Glamourer/Designs/Design.Manager.cs @@ -9,421 +9,363 @@ using Glamourer.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; -using OtterGui.Classes; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; namespace Glamourer.Designs; -public partial class Design +public class DesignManager { - public partial class Manager + public const string DesignFolderName = "designs"; + public readonly string DesignFolder; + + private readonly ItemManager _items; + private readonly SaveService _saveService; + private readonly List _designs = new(); + + public enum DesignChangeType { - public const string DesignFolderName = "designs"; - public readonly string DesignFolder; + Created, + Deleted, + ReloadedAll, + Renamed, + ChangedDescription, + AddedTag, + RemovedTag, + ChangedTag, + Customize, + Equip, + Weapon, + Stain, + ApplyCustomize, + ApplyEquip, + Other, + } - private readonly ItemManager _items; - private readonly SaveService _saveService; - private readonly List _designs = new(); + public delegate void DesignChangeDelegate(DesignChangeType type, Design design, object? changeData = null); - public enum DesignChangeType - { - Created, - Deleted, - ReloadedAll, - Renamed, - ChangedDescription, - AddedTag, - RemovedTag, - ChangedTag, - Customize, - Equip, - Weapon, - Stain, - ApplyCustomize, - ApplyEquip, - Other, - } + public event DesignChangeDelegate? DesignChange; - public delegate void DesignChangeDelegate(DesignChangeType type, Design design, object? changeData = null); + public IReadOnlyList Designs + => _designs; - public event DesignChangeDelegate DesignChange; - - public IReadOnlyList Designs - => _designs; - - public Manager(DalamudPluginInterface pi, SaveService saveService, ItemManager items) - { - _saveService = saveService; - _items = items; - DesignFolder = SetDesignFolder(pi); - DesignChange += OnChange; - LoadDesigns(); - MigrateOldDesigns(pi, Path.Combine(new DirectoryInfo(DesignFolder).Parent!.FullName, "Designs.json")); - } - - private void OnChange(DesignChangeType type, Design design, object? _) - { - switch (type) - { - case DesignChangeType.Created: - SaveDesignInternal(design); - return; - case DesignChangeType.Renamed: - case DesignChangeType.ChangedDescription: - case DesignChangeType.AddedTag: - case DesignChangeType.RemovedTag: - case DesignChangeType.ChangedTag: - case DesignChangeType.Customize: - case DesignChangeType.Equip: - case DesignChangeType.Weapon: - case DesignChangeType.Stain: - case DesignChangeType.ApplyCustomize: - case DesignChangeType.ApplyEquip: - case DesignChangeType.Other: - SaveDesign(design); - return; - } - } - - private static string SetDesignFolder(DalamudPluginInterface pi) - { - var ret = Path.Combine(pi.GetPluginConfigDirectory(), DesignFolderName); - if (Directory.Exists(ret)) - return ret; - - try - { - Directory.CreateDirectory(ret); - } - catch (Exception ex) - { - Glamourer.Log.Error($"Could not create design folder directory at {ret}:\n{ex}"); - } + public DesignManager(DalamudPluginInterface pi, SaveService saveService, ItemManager items) + { + _saveService = saveService; + _items = items; + DesignFolder = SetDesignFolder(pi); + LoadDesigns(); + MigrateOldDesigns(pi, Path.Combine(new DirectoryInfo(DesignFolder).Parent!.FullName, "Designs.json")); + } + private static string SetDesignFolder(DalamudPluginInterface pi) + { + var ret = Path.Combine(pi.GetPluginConfigDirectory(), DesignFolderName); + if (Directory.Exists(ret)) return ret; + + try + { + Directory.CreateDirectory(ret); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not create design folder directory at {ret}:\n{ex}"); } - private string CreateFileName(Design design) - => Path.Combine(DesignFolder, $"{design.Identifier}.json"); + return ret; + } - public void SaveDesign(Design design) - => _saveService.QueueSave(design); - - private void SaveDesignInternal(Design design) + public void LoadDesigns() + { + _designs.Clear(); + List<(Design, string)> invalidNames = new(); + var skipped = 0; + foreach (var file in new DirectoryInfo(DesignFolder).EnumerateFiles("*.json", SearchOption.TopDirectoryOnly)) { - var fileName = CreateFileName(design); try { - var data = design.JsonSerialize().ToString(Formatting.Indented); - File.WriteAllText(fileName, data); - Glamourer.Log.Debug($"Saved design {design.Identifier}."); + var text = File.ReadAllText(file.FullName); + var data = JObject.Parse(text); + var design = Design.LoadDesign(_items, data, out var changes); + 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."); + + // TODO something when changed? + design.Index = _designs.Count; + _designs.Add(design); } catch (Exception ex) { - Glamourer.Log.Error($"Could not save design {design.Identifier} to file:\n{ex}"); + Glamourer.Log.Error($"Could not load design, skipped:\n{ex}"); + ++skipped; } } - public void LoadDesigns() + var failed = 0; + foreach (var (design, name) in invalidNames) { - _designs.Clear(); - List<(Design, string)> invalidNames = new(); - var skipped = 0; - foreach (var file in new DirectoryInfo(DesignFolder).EnumerateFiles("*.json", SearchOption.TopDirectoryOnly)) + try { - try - { - var text = File.ReadAllText(file.FullName); - var data = JObject.Parse(text); - var design = LoadDesign(_items, data, out var changes); - 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."); - - // TODO something when changed? - design.Index = _designs.Count; - _designs.Add(design); - } - catch (Exception ex) - { - Glamourer.Log.Error($"Could not load design, skipped:\n{ex}"); - ++skipped; - } + 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)}."); } - - var failed = 0; - foreach (var (design, name) in invalidNames) + catch (Exception ex) { - try - { - var correctName = CreateFileName(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}"); - } + ++failed; + Glamourer.Log.Error($"Failed to move invalid design file from {Path.GetFileName(name)}:\n{ex}"); } + } - 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)}"); - + if (invalidNames.Count > 0) Glamourer.Log.Information( - $"Loaded {_designs.Count} designs.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}"); - DesignChange.Invoke(DesignChangeType.ReloadedAll, null!); - } + $"Moved {invalidNames.Count - failed} designs to correct names.{(failed > 0 ? $" Failed to move {failed} designs to correct names." : string.Empty)}"); - public Design Create(string name) - { - var design = new Design(_items) - { - CreationDate = DateTimeOffset.UtcNow, - Identifier = CreateNewGuid(), - Index = _designs.Count, - Name = name, - }; - _designs.Add(design); - Glamourer.Log.Debug($"Added new design {design.Identifier}."); - DesignChange.Invoke(DesignChangeType.Created, design); - return design; - } + Glamourer.Log.Information( + $"Loaded {_designs.Count} designs.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}"); + DesignChange?.Invoke(DesignChangeType.ReloadedAll, null!); + } - public void Delete(Design design) + public Design Create(string name) + { + var design = new Design(_items) { - _designs.RemoveAt(design.Index); - foreach (var d in _designs.Skip(design.Index + 1)) - --d.Index; - var fileName = CreateFileName(design); - try - { - File.Delete(fileName); - Glamourer.Log.Debug($"Deleted design {design.Identifier}."); - DesignChange.Invoke(DesignChangeType.Deleted, design); - } - catch (Exception ex) - { - Glamourer.Log.Error($"Could not delete design file for {design.Identifier}:\n{ex}"); - } - } + CreationDate = DateTimeOffset.UtcNow, + Identifier = CreateNewGuid(), + Index = _designs.Count, + Name = name, + }; + _designs.Add(design); + Glamourer.Log.Debug($"Added new design {design.Identifier}."); + _saveService.ImmediateSave(design); + DesignChange?.Invoke(DesignChangeType.Created, design); + return design; + } - public void Rename(Design design, string newName) + public void Delete(Design design) + { + _designs.RemoveAt(design.Index); + foreach (var d in _designs.Skip(design.Index + 1)) + --d.Index; + _saveService.ImmediateDelete(design); + } + + public void Rename(Design design, string newName) + { + var oldName = design.Name.Text; + _saveService.ImmediateDelete(design); + design.Name = newName; + Glamourer.Log.Debug($"Renamed design {design.Identifier}."); + _saveService.ImmediateSave(design); + DesignChange?.Invoke(DesignChangeType.Renamed, design, oldName); + } + + public void ChangeDescription(Design design, string description) + { + design.Description = description; + Glamourer.Log.Debug($"Changed description of design {design.Identifier}."); + _saveService.QueueSave(design); + DesignChange?.Invoke(DesignChangeType.ChangedDescription, design); + } + + public void AddTag(Design design, string tag) + { + if (design.Tags.Contains(tag)) + return; + + design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray(); + var idx = design.Tags.IndexOf(tag); + Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}."); + _saveService.QueueSave(design); + DesignChange?.Invoke(DesignChangeType.AddedTag, design); + } + + public void RemoveTag(Design design, string tag) + { + var idx = design.Tags.IndexOf(tag); + if (idx >= 0) + RemoveTag(design, idx); + } + + public void RemoveTag(Design design, int tagIdx) + { + var oldTag = design.Tags[tagIdx]; + design.Tags = design.Tags.Take(tagIdx).Concat(design.Tags.Skip(tagIdx + 1)).ToArray(); + Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}."); + _saveService.QueueSave(design); + DesignChange?.Invoke(DesignChangeType.RemovedTag, design); + } + + + 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); + Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags."); + _saveService.QueueSave(design); + DesignChange?.Invoke(DesignChangeType.ChangedTag, design); + } + + public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value) + { + var old = design.GetCustomize(idx); + if (!design.SetCustomize(idx, value)) + return; + + Glamourer.Log.Debug($"Changed customize {idx} in design {design.Identifier} from {old.Value} to {value.Value}"); + _saveService.QueueSave(design); + DesignChange?.Invoke(DesignChangeType.Customize, design, idx); + } + + public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value) + { + if (!design.SetApplyCustomize(idx, value)) + return; + + Glamourer.Log.Debug($"Set applying of customization {idx} to {value}."); + _saveService.QueueSave(design); + DesignChange?.Invoke(DesignChangeType.ApplyCustomize, design, idx); + } + + public void ChangeEquip(Design design, EquipSlot slot, uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null) + { + var old = design.Armor(slot); + if (!design.SetArmor(_items, slot, itemId, item)) + return; + + var n = design.Armor(slot); + Glamourer.Log.Debug( + $"Set {slot} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {n.Name} ({n.ItemId})."); + _saveService.QueueSave(design); + DesignChange?.Invoke(DesignChangeType.Equip, design, slot); + } + + public void ChangeWeapon(Design design, uint itemId, EquipSlot offhand, Lumina.Excel.GeneratedSheets.Item? item = null) + { + var (old, change, n) = offhand == EquipSlot.OffHand + ? (design.WeaponOff, design.SetOffhand(_items, itemId, item), design.WeaponOff) + : (design.WeaponMain, design.SetMainhand(_items, itemId, item), design.WeaponMain); + if (!change) + return; + + Glamourer.Log.Debug( + $"Set {offhand} weapon in design {design.Identifier} from {old.Name} ({old.ItemId}) to {n.Name} ({n.ItemId})."); + _saveService.QueueSave(design); + DesignChange?.Invoke(DesignChangeType.Weapon, design, offhand); + } + + public void ChangeApplyEquip(Design design, EquipSlot slot, bool value) + { + if (!design.SetApplyEquip(slot, value)) + return; + + Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}."); + _saveService.QueueSave(design); + DesignChange?.Invoke(DesignChangeType.ApplyEquip, design, slot); + } + + public void ChangeStain(Design design, EquipSlot slot, StainId stain) + { + if (!design.SetStain(slot, stain)) + return; + + Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Value}."); + _saveService.QueueSave(design); + DesignChange?.Invoke(DesignChangeType.Stain, design, slot); + } + + public void ChangeApplyStain(Design design, EquipSlot slot, bool value) + { + if (!design.SetApplyStain(slot, value)) + return; + + Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}."); + _saveService.QueueSave(design); + DesignChange?.Invoke(DesignChangeType.Stain, design, slot); + } + + private Guid CreateNewGuid() + { + while (true) { - var oldName = design.Name.Text; - var oldFileName = CreateFileName(design); - if (File.Exists(oldFileName)) + var guid = Guid.NewGuid(); + if (_designs.All(d => d.Identifier != guid)) + return guid; + } + } + + 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); + DesignChange?.Invoke(DesignChangeType.Created, design); + return true; + } + + private void MigrateOldDesigns(DalamudPluginInterface pi, string filePath) + { + if (!File.Exists(filePath)) + return; + + var errors = 0; + var successes = 0; + try + { + var text = File.ReadAllText(filePath); + var dict = JsonConvert.DeserializeObject>(text) ?? new Dictionary(); + var migratedFileSystemPaths = new Dictionary(dict.Count); + foreach (var (name, base64) in dict) + { try { - File.Delete(oldFileName); + var actualName = Path.GetFileName(name); + var design = new Design(_items) + { + CreationDate = DateTimeOffset.UtcNow, + Identifier = CreateNewGuid(), + Name = actualName, + }; + design.MigrateBase64(_items, base64); + Add(design, $"Migrated old design to {design.Identifier}."); + migratedFileSystemPaths.Add(design.Identifier.ToString(), name); + ++successes; } catch (Exception ex) { - Glamourer.Log.Error($"Could not delete old design file for rename from {design.Identifier}:\n{ex}"); - return; + Glamourer.Log.Error($"Could not migrate design {name}:\n{ex}"); + ++errors; } - - design.Name = newName; - Glamourer.Log.Debug($"Renamed design {design.Identifier}."); - DesignChange.Invoke(DesignChangeType.Renamed, design, oldName); - } - - public void ChangeDescription(Design design, string description) - { - design.Description = description; - Glamourer.Log.Debug($"Changed description of design {design.Identifier}."); - DesignChange.Invoke(DesignChangeType.ChangedDescription, design); - } - - public void AddTag(Design design, string tag) - { - if (design.Tags.Contains(tag)) - return; - - design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray(); - var idx = design.Tags.IndexOf(tag); - Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}."); - DesignChange.Invoke(DesignChangeType.AddedTag, design); - } - - public void RemoveTag(Design design, string tag) - { - var idx = design.Tags.IndexOf(tag); - if (idx >= 0) - RemoveTag(design, idx); - } - - public void RemoveTag(Design design, int tagIdx) - { - var oldTag = design.Tags[tagIdx]; - design.Tags = design.Tags.Take(tagIdx).Concat(design.Tags.Skip(tagIdx + 1)).ToArray(); - Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}."); - DesignChange.Invoke(DesignChangeType.RemovedTag, design); - } - - - 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); - SaveDesign(design); - Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags."); - DesignChange.Invoke(DesignChangeType.ChangedTag, design); - } - - public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value) - { - var old = design.GetCustomize(idx); - if (design.SetCustomize(idx, value)) - { - Glamourer.Log.Debug($"Changed customize {idx} in design {design.Identifier} from {old.Value} to {value.Value}"); - DesignChange.Invoke(DesignChangeType.Customize, design, idx); - } - } - - public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value) - { - if (design.SetApplyCustomize(idx, value)) - { - Glamourer.Log.Debug($"Set applying of customization {idx} to {value}."); - DesignChange.Invoke(DesignChangeType.ApplyCustomize, design, idx); - } - } - - public void ChangeEquip(Design design, EquipSlot slot, uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null) - { - var old = design.Armor(slot); - if (design.SetArmor(_items, slot, itemId, item)) - { - var n = design.Armor(slot); - Glamourer.Log.Debug( - $"Set {slot} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {n.Name} ({n.ItemId})."); - DesignChange.Invoke(DesignChangeType.Equip, design, slot); - } - } - - public void ChangeWeapon(Design design, uint itemId, EquipSlot offhand, Lumina.Excel.GeneratedSheets.Item? item = null) - { - var (old, change, n) = offhand == EquipSlot.OffHand - ? (design.WeaponOff, design.SetOffhand(_items, itemId, item), design.WeaponOff) - : (design.WeaponMain, design.SetMainhand(_items, itemId, item), design.WeaponMain); - if (change) - { - Glamourer.Log.Debug( - $"Set {offhand} weapon in design {design.Identifier} from {old.Name} ({old.ItemId}) to {n.Name} ({n.ItemId})."); - DesignChange.Invoke(DesignChangeType.Weapon, design, offhand); - } - } - - public void ChangeApplyEquip(Design design, EquipSlot slot, bool value) - { - if (design.SetApplyEquip(slot, value)) - { - Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}."); - DesignChange.Invoke(DesignChangeType.ApplyEquip, design, slot); - } - } - - public void ChangeStain(Design design, EquipSlot slot, StainId stain) - { - if (design.SetStain(slot, stain)) - { - Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Value}."); - DesignChange.Invoke(DesignChangeType.Stain, design, slot); - } - } - - public void ChangeApplyStain(Design design, EquipSlot slot, bool value) - { - if (design.SetApplyStain(slot, value)) - { - Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}."); - DesignChange.Invoke(DesignChangeType.Stain, design, slot); - } - } - - private Guid CreateNewGuid() - { - while (true) - { - var guid = Guid.NewGuid(); - if (_designs.All(d => d.Identifier != guid)) - return guid; - } - } - - 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); - DesignChange.Invoke(DesignChangeType.Created, design); - return true; - } - - private void MigrateOldDesigns(DalamudPluginInterface pi, string filePath) - { - if (!File.Exists(filePath)) - return; - - var errors = 0; - var successes = 0; - try - { - var text = File.ReadAllText(filePath); - 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 = DateTimeOffset.UtcNow, - Identifier = CreateNewGuid(), - Name = actualName, - }; - design.MigrateBase64(_items, base64); - Add(design, $"Migrated old design to {design.Identifier}."); - migratedFileSystemPaths.Add(design.Identifier.ToString(), name); - ++successes; - } - catch (Exception ex) - { - Glamourer.Log.Error($"Could not migrate design {name}:\n{ex}"); - ++errors; - } - } - - DesignFileSystem.MigrateOldPaths(pi, migratedFileSystemPaths); - Glamourer.Log.Information($"Successfully migrated {successes} old designs. Failed to migrate {errors} designs."); - } - catch (Exception e) - { - Glamourer.Log.Error($"Could not migrate old design file {filePath}:\n{e}"); } - try - { - File.Move(filePath, Path.ChangeExtension(filePath, ".json.bak")); - Glamourer.Log.Information($"Moved migrated design file {filePath} to backup file."); - } - catch (Exception ex) - { - Glamourer.Log.Error($"Could not move migrated design file {filePath} to backup file:\n{ex}"); - } + DesignFileSystem.MigrateOldPaths(pi, migratedFileSystemPaths); + Glamourer.Log.Information($"Successfully migrated {successes} old designs. Failed to migrate {errors} designs."); + } + catch (Exception e) + { + Glamourer.Log.Error($"Could not migrate old design file {filePath}:\n{e}"); + } + + try + { + File.Move(filePath, Path.ChangeExtension(filePath, ".json.bak")); + Glamourer.Log.Information($"Moved migrated design file {filePath} to backup file."); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not move migrated design file {filePath} to backup file:\n{ex}"); } } } diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index 5ec8ee4..efccd20 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -11,24 +11,24 @@ using Penumbra.GameData.Structs; namespace Glamourer.Designs; -public partial class Design : DesignBase, ISavable +public partial class Design : DesignData, ISavable { public const int FileVersion = 1; - public Guid Identifier { get; private init; } - public DateTimeOffset CreationDate { get; private init; } - public LowerString Name { get; private set; } = LowerString.Empty; - public string Description { get; private set; } = string.Empty; - public string[] Tags { get; private set; } = Array.Empty(); - public int Index { get; private set; } + public Guid Identifier { get; internal init; } + public DateTimeOffset CreationDate { get; internal init; } + public LowerString Name { get; internal set; } = LowerString.Empty; + public string Description { get; internal set; } = string.Empty; + public string[] Tags { get; internal set; } = Array.Empty(); + public int Index { get; internal set; } - public EquipFlag ApplyEquip { get; private set; } - public CustomizeFlag ApplyCustomize { get; private set; } - public QuadBool Wetness { get; private set; } = QuadBool.NullFalse; - public QuadBool Visor { get; private set; } = QuadBool.NullFalse; - public QuadBool Hat { get; private set; } = QuadBool.NullFalse; - public QuadBool Weapon { get; private set; } = QuadBool.NullFalse; - public bool WriteProtected { get; private set; } + public EquipFlag ApplyEquip { get; internal set; } + public CustomizeFlag ApplyCustomize { get; internal set; } + public QuadBool Wetness { get; internal set; } = QuadBool.NullFalse; + public QuadBool Visor { get; internal set; } = QuadBool.NullFalse; + public QuadBool Hat { get; internal set; } = QuadBool.NullFalse; + public QuadBool Weapon { get; internal set; } = QuadBool.NullFalse; + public bool WriteProtected { get; internal set; } public bool DoApplyEquip(EquipSlot slot) => ApplyEquip.HasFlag(slot.ToFlag()); @@ -39,7 +39,7 @@ public partial class Design : DesignBase, ISavable public bool DoApplyCustomize(CustomizeIndex idx) => ApplyCustomize.HasFlag(idx.ToFlag()); - private bool SetApplyEquip(EquipSlot slot, bool value) + internal bool SetApplyEquip(EquipSlot slot, bool value) { var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag(); if (newValue == ApplyEquip) @@ -49,7 +49,7 @@ public partial class Design : DesignBase, ISavable return true; } - private bool SetApplyStain(EquipSlot slot, bool value) + internal bool SetApplyStain(EquipSlot slot, bool value) { var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag(); if (newValue == ApplyEquip) @@ -59,7 +59,7 @@ public partial class Design : DesignBase, ISavable return true; } - private bool SetApplyCustomize(CustomizeIndex idx, bool value) + internal bool SetApplyCustomize(CustomizeIndex idx, bool value) { var newValue = value ? ApplyCustomize | idx.ToFlag() : ApplyCustomize & ~idx.ToFlag(); if (newValue == ApplyCustomize) @@ -70,7 +70,7 @@ public partial class Design : DesignBase, ISavable } - private Design(ItemManager items) + internal Design(ItemManager items) : base(items) { } @@ -78,15 +78,15 @@ public partial class Design : DesignBase, ISavable { var ret = new JObject { - [nameof(FileVersion)] = FileVersion, - [nameof(Identifier)] = Identifier, - [nameof(CreationDate)] = CreationDate, - [nameof(Name)] = Name.Text, - [nameof(Description)] = Description, - [nameof(Tags)] = JArray.FromObject(Tags), - [nameof(WriteProtected)] = WriteProtected, - [nameof(CharacterData.Equipment)] = SerializeEquipment(), - [nameof(CharacterData.Customize)] = SerializeCustomize(), + [nameof(FileVersion)] = FileVersion, + [nameof(Identifier)] = Identifier, + [nameof(CreationDate)] = CreationDate, + [nameof(Name)] = Name.Text, + [nameof(Description)] = Description, + [nameof(Tags)] = JArray.FromObject(Tags), + [nameof(WriteProtected)] = WriteProtected, + [nameof(ModelData.Equipment)] = SerializeEquipment(), + [nameof(ModelData.Customize)] = SerializeCustomize(), }; return ret; } @@ -104,9 +104,9 @@ public partial class Design : DesignBase, ISavable var ret = new JObject() { - [nameof(MainHand)] = - Serialize(MainHand, CharacterData.MainHand.Stain, DoApplyEquip(EquipSlot.MainHand), DoApplyStain(EquipSlot.MainHand)), - [nameof(OffHand)] = Serialize(OffHand, CharacterData.OffHand.Stain, DoApplyEquip(EquipSlot.OffHand), + [nameof(MainHandId)] = + Serialize(MainHandId, ModelData.MainHand.Stain, DoApplyEquip(EquipSlot.MainHand), DoApplyStain(EquipSlot.MainHand)), + [nameof(OffHandId)] = Serialize(OffHandId, ModelData.OffHand.Stain, DoApplyEquip(EquipSlot.OffHand), DoApplyStain(EquipSlot.OffHand)), }; @@ -129,7 +129,7 @@ public partial class Design : DesignBase, ISavable { [nameof(ModelId)] = ModelId, }; - var customize = CharacterData.Customize; + var customize = ModelData.Customize; foreach (var idx in Enum.GetValues()) { var data = customize[idx]; @@ -154,7 +154,7 @@ public partial class Design : DesignBase, ISavable }; } - private static Design LoadDesignV1(ItemManager items, JObject json, out bool changes) + internal static Design LoadDesignV1(ItemManager items, JObject json, out bool changes) { static string[] ParseTags(JObject json) { @@ -176,7 +176,7 @@ public partial class Design : DesignBase, ISavable return design; } - private static bool LoadEquip(ItemManager items, JToken? equip, Design design) + internal static bool LoadEquip(ItemManager items, JToken? equip, Design design) { if (equip == null) return true; @@ -241,12 +241,12 @@ public partial class Design : DesignBase, ISavable return changes; } - private static bool LoadCustomize(JToken? json, Design design) + internal static bool LoadCustomize(JToken? json, Design design) { if (json == null) return true; - var customize = design.CharacterData.Customize; + var customize = design.ModelData.Customize; foreach (var idx in Enum.GetValues()) { var tok = json[idx.ToString()]; @@ -263,20 +263,21 @@ public partial class Design : DesignBase, ISavable public void MigrateBase64(ItemManager items, string base64) { - var data = MigrateBase64(base64, out var applyEquip, out var applyCustomize, out var writeProtected, out var wet, out var hat, - out var visor, out var weapon); + var data = DesignBase64Migration.MigrateBase64(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.Equipment[slot], true); - CharacterData.CustomizeData = data.CustomizeData; - ApplyEquip = applyEquip; - ApplyCustomize = applyCustomize; - WriteProtected = writeProtected; - Wetness = wet; - Hat = hat; - Visor = visor; - Weapon = weapon; + ModelData.CustomizeData = data.CustomizeData; + ApplyEquip = applyEquip; + ApplyCustomize = applyCustomize; + WriteProtected = writeProtected; + Wetness = wet; + Hat = hat; + Visor = visor; + Weapon = weapon; } public static Design CreateTemporaryFromBase64(ItemManager items, string base64, bool customize, bool equip) @@ -296,13 +297,14 @@ public partial class Design : DesignBase, ISavable // Outdated. public string CreateOldBase64() - => CreateOldBase64(in CharacterData, ApplyEquip, ApplyCustomize, Wetness == QuadBool.True, Hat.ForcedValue, Hat.Enabled, - Visor.ForcedValue, Visor.Enabled, Weapon.ForcedValue, Weapon.Enabled, WriteProtected, 1f); + => 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) + public void Save(StreamWriter writer) { using var j = new JsonTextWriter(writer) { diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs deleted file mode 100644 index f027e49..0000000 --- a/Glamourer/Designs/DesignBase.cs +++ /dev/null @@ -1,453 +0,0 @@ -using System; -using Glamourer.Customization; -using Glamourer.Services; -using Glamourer.Util; -using OtterGui.Classes; -using OtterGui; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Glamourer.Designs; - -public class DesignBase -{ - protected CharacterData CharacterData = CharacterData.Default; - public FullEquipType MainhandType { get; protected set; } - - public uint Head { get; protected set; } = ItemManager.NothingId(EquipSlot.Head); - public uint Body { get; protected set; } = ItemManager.NothingId(EquipSlot.Body); - public uint Hands { get; protected set; } = ItemManager.NothingId(EquipSlot.Hands); - public uint Legs { get; protected set; } = ItemManager.NothingId(EquipSlot.Legs); - public uint Feet { get; protected set; } = ItemManager.NothingId(EquipSlot.Feet); - public uint Ears { get; protected set; } = ItemManager.NothingId(EquipSlot.Ears); - public uint Neck { get; protected set; } = ItemManager.NothingId(EquipSlot.Neck); - public uint Wrists { get; protected set; } = ItemManager.NothingId(EquipSlot.Wrists); - public uint RFinger { get; protected set; } = ItemManager.NothingId(EquipSlot.RFinger); - public uint LFinger { get; protected set; } = ItemManager.NothingId(EquipSlot.RFinger); - public uint MainHand { get; protected set; } - public uint OffHand { get; protected set; } - - public string HeadName { get; protected set; } = ItemManager.Nothing; - public string BodyName { get; protected set; } = ItemManager.Nothing; - public string HandsName { get; protected set; } = ItemManager.Nothing; - public string LegsName { get; protected set; } = ItemManager.Nothing; - public string FeetName { get; protected set; } = ItemManager.Nothing; - public string EarsName { get; protected set; } = ItemManager.Nothing; - public string NeckName { get; protected set; } = ItemManager.Nothing; - public string WristsName { get; protected set; } = ItemManager.Nothing; - public string RFingerName { get; protected set; } = ItemManager.Nothing; - public string LFingerName { get; protected set; } = ItemManager.Nothing; - public string MainhandName { get; protected set; } - public string OffhandName { get; protected set; } - - public Customize Customize() - => CharacterData.Customize; - - public CharacterEquip Equipment() - => CharacterData.Equipment; - - public DesignBase(ItemManager items) - { - MainHand = items.DefaultSword.RowId; - (_, CharacterData.MainHand.Set, CharacterData.MainHand.Type, CharacterData.MainHand.Variant, MainhandName, MainhandType) = - items.Resolve(MainHand, items.DefaultSword); - OffHand = ItemManager.NothingId(MainhandType.Offhand()); - (_, CharacterData.OffHand.Set, CharacterData.OffHand.Type, CharacterData.OffHand.Variant, OffhandName, _) = - items.Resolve(OffHand, MainhandType); - } - - public uint ModelId - => CharacterData.ModelId; - - public Item Armor(EquipSlot slot) - { - return slot switch - { - EquipSlot.Head => new Item(HeadName, Head, CharacterData.Head), - EquipSlot.Body => new Item(BodyName, Body, CharacterData.Body), - EquipSlot.Hands => new Item(HandsName, Hands, CharacterData.Hands), - EquipSlot.Legs => new Item(LegsName, Legs, CharacterData.Legs), - EquipSlot.Feet => new Item(FeetName, Feet, CharacterData.Feet), - EquipSlot.Ears => new Item(EarsName, Ears, CharacterData.Ears), - EquipSlot.Neck => new Item(NeckName, Neck, CharacterData.Neck), - EquipSlot.Wrists => new Item(WristsName, Wrists, CharacterData.Wrists), - EquipSlot.RFinger => new Item(RFingerName, RFinger, CharacterData.RFinger), - EquipSlot.LFinger => new Item(LFingerName, LFinger, CharacterData.LFinger), - _ => throw new Exception("Invalid equip slot for item."), - }; - } - - - public Weapon WeaponMain - => new(MainhandName, MainHand, CharacterData.MainHand, MainhandType); - - public Weapon WeaponOff - => Weapon.Offhand(OffhandName, OffHand, CharacterData.OffHand, MainhandType); - - public CustomizeValue GetCustomize(CustomizeIndex idx) - => Customize()[idx]; - - protected bool SetCustomize(CustomizeIndex idx, CustomizeValue value) - { - var c = Customize(); - if (c[idx] == value) - return false; - - c[idx] = value; - return true; - } - - protected bool SetArmor(ItemManager items, EquipSlot slot, uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null) - { - var (valid, set, variant, name) = items.Resolve(slot, itemId, item); - if (!valid) - return false; - - return SetArmor(slot, set, variant, name, itemId); - } - - protected bool SetArmor(EquipSlot slot, Item item) - => SetArmor(slot, item.ModelBase, item.Variant, item.Name, item.ItemId); - - protected bool UpdateArmor(ItemManager items, EquipSlot slot, CharacterArmor armor, bool force) - { - if (!force) - switch (slot) - { - case EquipSlot.Head when CharacterData.Head.Value == armor.Value: return false; - case EquipSlot.Body when CharacterData.Body.Value == armor.Value: return false; - case EquipSlot.Hands when CharacterData.Hands.Value == armor.Value: return false; - case EquipSlot.Legs when CharacterData.Legs.Value == armor.Value: return false; - case EquipSlot.Feet when CharacterData.Feet.Value == armor.Value: return false; - case EquipSlot.Ears when CharacterData.Ears.Value == armor.Value: return false; - case EquipSlot.Neck when CharacterData.Neck.Value == armor.Value: return false; - case EquipSlot.Wrists when CharacterData.Wrists.Value == armor.Value: return false; - case EquipSlot.RFinger when CharacterData.RFinger.Value == armor.Value: return false; - case EquipSlot.LFinger when CharacterData.LFinger.Value == armor.Value: return false; - } - - var (valid, id, name) = items.Identify(slot, armor.Set, armor.Variant); - if (!valid) - return false; - - return SetArmor(slot, armor.Set, armor.Variant, name, id); - } - - protected bool SetMainhand(ItemManager items, uint mainId, Lumina.Excel.GeneratedSheets.Item? main = null) - { - if (mainId == MainHand) - return false; - - var (valid, set, weapon, variant, name, type) = items.Resolve(mainId, main); - if (!valid) - return false; - - var fixOffhand = type.Offhand() != MainhandType.Offhand(); - - MainHand = mainId; - MainhandName = name; - MainhandType = type; - CharacterData.MainHand.Set = set; - CharacterData.MainHand.Type = weapon; - CharacterData.MainHand.Variant = variant; - if (fixOffhand) - SetOffhand(items, ItemManager.NothingId(type.Offhand())); - return true; - } - - protected bool SetOffhand(ItemManager items, uint offId, Lumina.Excel.GeneratedSheets.Item? off = null) - { - if (offId == OffHand) - return false; - - var (valid, set, weapon, variant, name, type) = items.Resolve(offId, MainhandType, off); - if (!valid) - return false; - - OffHand = offId; - OffhandName = name; - CharacterData.OffHand.Set = set; - CharacterData.OffHand.Type = weapon; - CharacterData.OffHand.Variant = variant; - return true; - } - - protected bool UpdateMainhand(ItemManager items, CharacterWeapon weapon) - { - if (weapon.Value == CharacterData.MainHand.Value) - return false; - - var (valid, id, name, type) = items.Identify(EquipSlot.MainHand, weapon.Set, weapon.Type, (byte)weapon.Variant); - if (!valid || id == MainHand) - return false; - - var fixOffhand = type.Offhand() != MainhandType.Offhand(); - - MainHand = id; - MainhandName = name; - MainhandType = type; - CharacterData.MainHand.Set = weapon.Set; - CharacterData.MainHand.Type = weapon.Type; - CharacterData.MainHand.Variant = weapon.Variant; - CharacterData.MainHand.Stain = weapon.Stain; - if (fixOffhand) - SetOffhand(items, ItemManager.NothingId(type.Offhand())); - return true; - } - - protected bool UpdateOffhand(ItemManager items, CharacterWeapon weapon) - { - if (weapon.Value == CharacterData.OffHand.Value) - return false; - - var (valid, id, name, _) = items.Identify(EquipSlot.OffHand, weapon.Set, weapon.Type, (byte)weapon.Variant, MainhandType); - if (!valid || id == OffHand) - return false; - - OffHand = id; - OffhandName = name; - CharacterData.OffHand.Set = weapon.Set; - CharacterData.OffHand.Type = weapon.Type; - CharacterData.OffHand.Variant = weapon.Variant; - CharacterData.OffHand.Stain = weapon.Stain; - return true; - } - - protected bool SetStain(EquipSlot slot, StainId id) - { - return slot switch - { - EquipSlot.MainHand => SetIfDifferent(ref CharacterData.MainHand.Stain, id), - EquipSlot.OffHand => SetIfDifferent(ref CharacterData.OffHand.Stain, id), - EquipSlot.Head => SetIfDifferent(ref CharacterData.Head.Stain, id), - EquipSlot.Body => SetIfDifferent(ref CharacterData.Body.Stain, id), - EquipSlot.Hands => SetIfDifferent(ref CharacterData.Hands.Stain, id), - EquipSlot.Legs => SetIfDifferent(ref CharacterData.Legs.Stain, id), - EquipSlot.Feet => SetIfDifferent(ref CharacterData.Feet.Stain, id), - EquipSlot.Ears => SetIfDifferent(ref CharacterData.Ears.Stain, id), - EquipSlot.Neck => SetIfDifferent(ref CharacterData.Neck.Stain, id), - EquipSlot.Wrists => SetIfDifferent(ref CharacterData.Wrists.Stain, id), - EquipSlot.RFinger => SetIfDifferent(ref CharacterData.RFinger.Stain, id), - EquipSlot.LFinger => SetIfDifferent(ref CharacterData.LFinger.Stain, id), - _ => false, - }; - } - - protected static bool SetIfDifferent(ref T old, T value) where T : IEquatable - { - if (old.Equals(value)) - return false; - - old = value; - return true; - } - - - private bool SetArmor(EquipSlot slot, SetId set, byte variant, string name, uint id) - { - var changes = false; - switch (slot) - { - case EquipSlot.Head: - changes |= SetIfDifferent(ref CharacterData.Head.Set, set); - changes |= SetIfDifferent(ref CharacterData.Head.Variant, variant); - changes |= HeadName != name; - HeadName = name; - changes |= Head != id; - Head = id; - return changes; - case EquipSlot.Body: - changes |= SetIfDifferent(ref CharacterData.Body.Set, set); - changes |= SetIfDifferent(ref CharacterData.Body.Variant, variant); - changes |= BodyName != name; - BodyName = name; - changes |= Body != id; - Body = id; - return changes; - case EquipSlot.Hands: - changes |= SetIfDifferent(ref CharacterData.Hands.Set, set); - changes |= SetIfDifferent(ref CharacterData.Hands.Variant, variant); - changes |= HandsName != name; - HandsName = name; - changes |= Hands != id; - Hands = id; - return changes; - case EquipSlot.Legs: - changes |= SetIfDifferent(ref CharacterData.Legs.Set, set); - changes |= SetIfDifferent(ref CharacterData.Legs.Variant, variant); - changes |= LegsName != name; - LegsName = name; - changes |= Legs != id; - Legs = id; - return changes; - case EquipSlot.Feet: - changes |= SetIfDifferent(ref CharacterData.Feet.Set, set); - changes |= SetIfDifferent(ref CharacterData.Feet.Variant, variant); - changes |= FeetName != name; - FeetName = name; - changes |= Feet != id; - Feet = id; - return changes; - case EquipSlot.Ears: - changes |= SetIfDifferent(ref CharacterData.Ears.Set, set); - changes |= SetIfDifferent(ref CharacterData.Ears.Variant, variant); - changes |= EarsName != name; - EarsName = name; - changes |= Ears != id; - Ears = id; - return changes; - case EquipSlot.Neck: - changes |= SetIfDifferent(ref CharacterData.Neck.Set, set); - changes |= SetIfDifferent(ref CharacterData.Neck.Variant, variant); - changes |= NeckName != name; - NeckName = name; - changes |= Neck != id; - Neck = id; - return changes; - case EquipSlot.Wrists: - changes |= SetIfDifferent(ref CharacterData.Wrists.Set, set); - changes |= SetIfDifferent(ref CharacterData.Wrists.Variant, variant); - changes |= WristsName != name; - WristsName = name; - changes |= Wrists != id; - Wrists = id; - return changes; - case EquipSlot.RFinger: - changes |= SetIfDifferent(ref CharacterData.RFinger.Set, set); - changes |= SetIfDifferent(ref CharacterData.RFinger.Variant, variant); - changes |= RFingerName != name; - RFingerName = name; - changes |= RFinger != id; - RFinger = id; - return changes; - case EquipSlot.LFinger: - changes |= SetIfDifferent(ref CharacterData.LFinger.Set, set); - changes |= SetIfDifferent(ref CharacterData.LFinger.Variant, variant); - changes |= LFingerName != name; - LFingerName = name; - changes |= LFinger != id; - LFinger = id; - return changes; - default: return false; - } - } - - protected const int Base64Size = 89; - - public static CharacterData MigrateBase64(string base64, out EquipFlag equipFlags, out CustomizeFlag customizeFlags, - out bool writeProtected, out QuadBool wet, out QuadBool hat, out QuadBool visor, out QuadBool weapon) - { - 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}."); - } - - byte applicationFlags; - ushort equipFlagsS; - var bytes = Convert.FromBase64String(base64); - hat = QuadBool.Null; - visor = QuadBool.Null; - weapon = QuadBool.Null; - switch (bytes[0]) - { - case 1: - { - CheckSize(bytes.Length, 86); - applicationFlags = bytes[1]; - equipFlagsS = BitConverter.ToUInt16(bytes, 2); - break; - } - case 2: - { - CheckSize(bytes.Length, Base64Size); - applicationFlags = bytes[1]; - equipFlagsS = BitConverter.ToUInt16(bytes, 2); - hat = hat.SetValue((bytes[90] & 0x01) == 0); - visor = visor.SetValue((bytes[90] & 0x10) != 0); - weapon = weapon.SetValue((bytes[90] & 0x02) == 0); - break; - } - default: throw new Exception($"Can not parse Base64 string into design for migration:\n\tInvalid Version {bytes[0]}."); - } - - customizeFlags = (applicationFlags & 0x01) != 0 ? CustomizeFlagExtensions.All : 0; - wet = (applicationFlags & 0x02) != 0 ? QuadBool.True : QuadBool.NullFalse; - hat = hat.SetEnabled((applicationFlags & 0x04) != 0); - weapon = weapon.SetEnabled((applicationFlags & 0x08) != 0); - visor = visor.SetEnabled((applicationFlags & 0x10) != 0); - writeProtected = (applicationFlags & 0x20) != 0; - - equipFlags = 0; - equipFlags |= (equipFlagsS & 0x0001) != 0 ? EquipFlag.Mainhand | EquipFlag.MainhandStain : 0; - equipFlags |= (equipFlagsS & 0x0002) != 0 ? EquipFlag.Offhand | EquipFlag.OffhandStain : 0; - var flag = 0x0002u; - foreach (var slot in EquipSlotExtensions.EqdpSlots) - { - flag <<= 1; - equipFlags |= (equipFlagsS & flag) != 0 ? slot.ToFlag() | slot.ToStainFlag() : 0; - } - - var data = new CharacterData(); - unsafe - { - fixed (byte* ptr = bytes) - { - data.CustomizeData.Read(ptr + 4); - var cur = (CharacterWeapon*)(ptr + 30); - data.MainHand = cur[0]; - data.OffHand = cur[1]; - var eq = (CharacterArmor*)(cur + 2); - foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex()) - data.Equipment[slot] = eq[idx]; - } - } - - return data; - } - - public static unsafe string CreateOldBase64(in CharacterData save, EquipFlag equipFlags, CustomizeFlag customizeFlags, bool wet, bool hat, - bool setHat, bool visor, bool setVisor, bool weapon, bool setWeapon, bool writeProtected, float alpha) - { - var data = stackalloc byte[Base64Size]; - data[0] = 2; - data[1] = (byte)((customizeFlags == CustomizeFlagExtensions.All ? 0x01 : 0) - | (wet ? 0x02 : 0) - | (setHat ? 0x04 : 0) - | (setWeapon ? 0x08 : 0) - | (setVisor ? 0x10 : 0) - | (writeProtected ? 0x20 : 0)); - data[2] = (byte)((equipFlags.HasFlag(EquipFlag.Mainhand) ? 0x01 : 0) - | (equipFlags.HasFlag(EquipFlag.Offhand) ? 0x02 : 0) - | (equipFlags.HasFlag(EquipFlag.Head) ? 0x04 : 0) - | (equipFlags.HasFlag(EquipFlag.Body) ? 0x08 : 0) - | (equipFlags.HasFlag(EquipFlag.Hands) ? 0x10 : 0) - | (equipFlags.HasFlag(EquipFlag.Legs) ? 0x20 : 0) - | (equipFlags.HasFlag(EquipFlag.Feet) ? 0x40 : 0) - | (equipFlags.HasFlag(EquipFlag.Ears) ? 0x80 : 0)); - data[3] = (byte)((equipFlags.HasFlag(EquipFlag.Neck) ? 0x01 : 0) - | (equipFlags.HasFlag(EquipFlag.Wrist) ? 0x02 : 0) - | (equipFlags.HasFlag(EquipFlag.RFinger) ? 0x04 : 0) - | (equipFlags.HasFlag(EquipFlag.LFinger) ? 0x08 : 0)); - save.CustomizeData.Write(data + 4); - ((CharacterWeapon*)(data + 30))[0] = save.MainHand; - ((CharacterWeapon*)(data + 30))[1] = save.OffHand; - ((CharacterArmor*)(data + 44))[0] = save.Head; - ((CharacterArmor*)(data + 44))[1] = save.Body; - ((CharacterArmor*)(data + 44))[2] = save.Hands; - ((CharacterArmor*)(data + 44))[3] = save.Legs; - ((CharacterArmor*)(data + 44))[4] = save.Feet; - ((CharacterArmor*)(data + 44))[5] = save.Ears; - ((CharacterArmor*)(data + 44))[6] = save.Neck; - ((CharacterArmor*)(data + 44))[7] = save.Wrists; - ((CharacterArmor*)(data + 44))[8] = save.RFinger; - ((CharacterArmor*)(data + 44))[9] = save.LFinger; - *(float*)(data + 84) = 1f; - data[88] = (byte)((hat ? 0x01 : 0) - | (visor ? 0x10 : 0) - | (weapon ? 0x02 : 0)); - - return Convert.ToBase64String(new Span(data, Base64Size)); - } -} diff --git a/Glamourer/Designs/DesignData.cs b/Glamourer/Designs/DesignData.cs new file mode 100644 index 0000000..a2ca078 --- /dev/null +++ b/Glamourer/Designs/DesignData.cs @@ -0,0 +1,432 @@ +using System; +using System.Runtime.InteropServices; +using Glamourer.Customization; +using Glamourer.Interop; +using Glamourer.Services; +using OtterGui.Classes; +using OtterGui; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; + +namespace Glamourer.Designs; + +public class DesignData +{ + internal ModelData ModelData; + public FullEquipType MainhandType { get; internal set; } + + public uint Head = ItemManager.NothingId(EquipSlot.Head); + public uint Body = ItemManager.NothingId(EquipSlot.Body); + public uint Hands = ItemManager.NothingId(EquipSlot.Hands); + public uint Legs = ItemManager.NothingId(EquipSlot.Legs); + public uint Feet = ItemManager.NothingId(EquipSlot.Feet); + public uint Ears = ItemManager.NothingId(EquipSlot.Ears); + public uint Neck = ItemManager.NothingId(EquipSlot.Neck); + public uint Wrists = ItemManager.NothingId(EquipSlot.Wrists); + public uint RFinger = ItemManager.NothingId(EquipSlot.RFinger); + public uint LFinger = ItemManager.NothingId(EquipSlot.RFinger); + public uint MainHandId; + public uint OffHandId; + + public string HeadName = ItemManager.Nothing; + public string BodyName = ItemManager.Nothing; + public string HandsName = ItemManager.Nothing; + public string LegsName = ItemManager.Nothing; + public string FeetName = ItemManager.Nothing; + public string EarsName = ItemManager.Nothing; + public string NeckName = ItemManager.Nothing; + public string WristsName = ItemManager.Nothing; + public string RFingerName = ItemManager.Nothing; + public string LFingerName = ItemManager.Nothing; + public string MainhandName; + public string OffhandName; + + public DesignData(ItemManager items) + { + MainHandId = items.DefaultSword.RowId; + (_, var set, var type, var variant, MainhandName, MainhandType) = items.Resolve(MainHandId, items.DefaultSword); + + ModelData = new ModelData(new CharacterWeapon(set, type, variant, 0)); + OffHandId = ItemManager.NothingId(MainhandType.Offhand()); + (_, ModelData.OffHand.Set, ModelData.OffHand.Type, ModelData.OffHand.Variant, OffhandName, _) = + items.Resolve(OffHandId, MainhandType); + } + + public uint ModelId + => ModelData.ModelId; + + public Item Armor(EquipSlot slot) + { + return slot switch + { + EquipSlot.Head => new Item(HeadName, Head, ModelData.Head), + EquipSlot.Body => new Item(BodyName, Body, ModelData.Body), + EquipSlot.Hands => new Item(HandsName, Hands, ModelData.Hands), + EquipSlot.Legs => new Item(LegsName, Legs, ModelData.Legs), + EquipSlot.Feet => new Item(FeetName, Feet, ModelData.Feet), + EquipSlot.Ears => new Item(EarsName, Ears, ModelData.Ears), + EquipSlot.Neck => new Item(NeckName, Neck, ModelData.Neck), + EquipSlot.Wrists => new Item(WristsName, Wrists, ModelData.Wrists), + EquipSlot.RFinger => new Item(RFingerName, RFinger, ModelData.RFinger), + EquipSlot.LFinger => new Item(LFingerName, LFinger, ModelData.LFinger), + _ => throw new Exception("Invalid equip slot for item."), + }; + } + + + public Weapon WeaponMain + => new(MainhandName, MainHandId, ModelData.MainHand, MainhandType); + + public Weapon WeaponOff + => Weapon.Offhand(OffhandName, OffHandId, ModelData.OffHand, MainhandType); + + public CustomizeValue GetCustomize(CustomizeIndex idx) + => ModelData.Customize[idx]; + + internal bool SetCustomize(CustomizeIndex idx, CustomizeValue value) + => ModelData.Customize.Set(idx, value); + + internal bool SetArmor(ItemManager items, EquipSlot slot, uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null) + { + var (valid, set, variant, name) = items.Resolve(slot, itemId, item); + if (!valid) + return false; + + return SetArmor(slot, set, variant, name, itemId); + } + + internal bool SetArmor(EquipSlot slot, Item item) + => SetArmor(slot, item.ModelBase, item.Variant, item.Name, item.ItemId); + + internal bool UpdateArmor(ItemManager items, EquipSlot slot, CharacterArmor armor, bool force) + { + var (valid, id, name) = items.Identify(slot, armor.Set, armor.Variant); + if (!valid) + return false; + + return SetArmor(slot, armor.Set, armor.Variant, name, id) | SetStain(slot, armor.Stain); + } + + internal bool SetMainhand(ItemManager items, uint mainId, Lumina.Excel.GeneratedSheets.Item? main = null) + { + if (mainId == MainHandId) + return false; + + var (valid, set, weapon, variant, name, type) = items.Resolve(mainId, main); + if (!valid) + return false; + + var fixOffhand = type.Offhand() != MainhandType.Offhand(); + + MainHandId = mainId; + MainhandName = name; + MainhandType = type; + ModelData.MainHand.Set = set; + ModelData.MainHand.Type = weapon; + ModelData.MainHand.Variant = variant; + if (fixOffhand) + SetOffhand(items, ItemManager.NothingId(type.Offhand())); + return true; + } + + internal bool SetOffhand(ItemManager items, uint offId, Lumina.Excel.GeneratedSheets.Item? off = null) + { + if (offId == OffHandId) + return false; + + var (valid, set, weapon, variant, name, type) = items.Resolve(offId, MainhandType, off); + if (!valid) + return false; + + OffHandId = offId; + OffhandName = name; + ModelData.OffHand.Set = set; + ModelData.OffHand.Type = weapon; + ModelData.OffHand.Variant = variant; + return true; + } + + internal bool UpdateMainhand(ItemManager items, CharacterWeapon weapon) + { + if (weapon.Value == ModelData.MainHand.Value) + return false; + + var (valid, id, name, type) = items.Identify(EquipSlot.MainHand, weapon.Set, weapon.Type, (byte)weapon.Variant); + if (!valid || id == MainHandId) + return false; + + var fixOffhand = type.Offhand() != MainhandType.Offhand(); + + MainHandId = id; + MainhandName = name; + MainhandType = type; + ModelData.MainHand.Set = weapon.Set; + ModelData.MainHand.Type = weapon.Type; + ModelData.MainHand.Variant = weapon.Variant; + ModelData.MainHand.Stain = weapon.Stain; + if (fixOffhand) + SetOffhand(items, ItemManager.NothingId(type.Offhand())); + return true; + } + + internal bool UpdateOffhand(ItemManager items, CharacterWeapon weapon) + { + if (weapon.Value == ModelData.OffHand.Value) + return false; + + var (valid, id, name, _) = items.Identify(EquipSlot.OffHand, weapon.Set, weapon.Type, (byte)weapon.Variant, MainhandType); + if (!valid || id == OffHandId) + return false; + + OffHandId = id; + OffhandName = name; + ModelData.OffHand.Set = weapon.Set; + ModelData.OffHand.Type = weapon.Type; + ModelData.OffHand.Variant = weapon.Variant; + ModelData.OffHand.Stain = weapon.Stain; + return true; + } + + internal bool SetStain(EquipSlot slot, StainId id) + { + return slot switch + { + EquipSlot.MainHand => SetIfDifferent(ref ModelData.MainHand.Stain, id), + EquipSlot.OffHand => SetIfDifferent(ref ModelData.OffHand.Stain, id), + EquipSlot.Head => SetIfDifferent(ref ModelData.Head.Stain, id), + EquipSlot.Body => SetIfDifferent(ref ModelData.Body.Stain, id), + EquipSlot.Hands => SetIfDifferent(ref ModelData.Hands.Stain, id), + EquipSlot.Legs => SetIfDifferent(ref ModelData.Legs.Stain, id), + EquipSlot.Feet => SetIfDifferent(ref ModelData.Feet.Stain, id), + EquipSlot.Ears => SetIfDifferent(ref ModelData.Ears.Stain, id), + EquipSlot.Neck => SetIfDifferent(ref ModelData.Neck.Stain, id), + EquipSlot.Wrists => SetIfDifferent(ref ModelData.Wrists.Stain, id), + EquipSlot.RFinger => SetIfDifferent(ref ModelData.RFinger.Stain, id), + EquipSlot.LFinger => SetIfDifferent(ref ModelData.LFinger.Stain, id), + _ => false, + }; + } + + internal static bool SetIfDifferent(ref T old, T value) where T : IEquatable + { + if (old.Equals(value)) + return false; + + old = value; + return true; + } + + + private bool SetArmor(EquipSlot slot, SetId set, byte variant, string name, uint id) + { + var changes = false; + switch (slot) + { + case EquipSlot.Head: + changes |= SetIfDifferent(ref ModelData.Head.Set, set); + changes |= SetIfDifferent(ref ModelData.Head.Variant, variant); + changes |= HeadName != name; + HeadName = name; + changes |= Head != id; + Head = id; + return changes; + case EquipSlot.Body: + changes |= SetIfDifferent(ref ModelData.Body.Set, set); + changes |= SetIfDifferent(ref ModelData.Body.Variant, variant); + changes |= BodyName != name; + BodyName = name; + changes |= Body != id; + Body = id; + return changes; + case EquipSlot.Hands: + changes |= SetIfDifferent(ref ModelData.Hands.Set, set); + changes |= SetIfDifferent(ref ModelData.Hands.Variant, variant); + changes |= HandsName != name; + HandsName = name; + changes |= Hands != id; + Hands = id; + return changes; + case EquipSlot.Legs: + changes |= SetIfDifferent(ref ModelData.Legs.Set, set); + changes |= SetIfDifferent(ref ModelData.Legs.Variant, variant); + changes |= LegsName != name; + LegsName = name; + changes |= Legs != id; + Legs = id; + return changes; + case EquipSlot.Feet: + changes |= SetIfDifferent(ref ModelData.Feet.Set, set); + changes |= SetIfDifferent(ref ModelData.Feet.Variant, variant); + changes |= FeetName != name; + FeetName = name; + changes |= Feet != id; + Feet = id; + return changes; + case EquipSlot.Ears: + changes |= SetIfDifferent(ref ModelData.Ears.Set, set); + changes |= SetIfDifferent(ref ModelData.Ears.Variant, variant); + changes |= EarsName != name; + EarsName = name; + changes |= Ears != id; + Ears = id; + return changes; + case EquipSlot.Neck: + changes |= SetIfDifferent(ref ModelData.Neck.Set, set); + changes |= SetIfDifferent(ref ModelData.Neck.Variant, variant); + changes |= NeckName != name; + NeckName = name; + changes |= Neck != id; + Neck = id; + return changes; + case EquipSlot.Wrists: + changes |= SetIfDifferent(ref ModelData.Wrists.Set, set); + changes |= SetIfDifferent(ref ModelData.Wrists.Variant, variant); + changes |= WristsName != name; + WristsName = name; + changes |= Wrists != id; + Wrists = id; + return changes; + case EquipSlot.RFinger: + changes |= SetIfDifferent(ref ModelData.RFinger.Set, set); + changes |= SetIfDifferent(ref ModelData.RFinger.Variant, variant); + changes |= RFingerName != name; + RFingerName = name; + changes |= RFinger != id; + RFinger = id; + return changes; + case EquipSlot.LFinger: + changes |= SetIfDifferent(ref ModelData.LFinger.Set, set); + changes |= SetIfDifferent(ref ModelData.LFinger.Variant, variant); + changes |= LFingerName != name; + LFingerName = name; + changes |= LFinger != id; + LFinger = id; + return changes; + default: return false; + } + } +} + +public static class DesignBase64Migration +{ + public const int Base64Size = 91; + + public static ModelData MigrateBase64(string base64, out EquipFlag equipFlags, out CustomizeFlag customizeFlags, + out bool writeinternal, out QuadBool wet, out QuadBool hat, out QuadBool visor, out QuadBool weapon) + { + 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}."); + } + + byte applicationFlags; + ushort equipFlagsS; + var bytes = Convert.FromBase64String(base64); + hat = QuadBool.Null; + visor = QuadBool.Null; + weapon = QuadBool.Null; + switch (bytes[0]) + { + case 1: + { + CheckSize(bytes.Length, 86); + applicationFlags = bytes[1]; + equipFlagsS = BitConverter.ToUInt16(bytes, 2); + break; + } + case 2: + { + CheckSize(bytes.Length, Base64Size); + applicationFlags = bytes[1]; + equipFlagsS = BitConverter.ToUInt16(bytes, 2); + hat = hat.SetValue((bytes[90] & 0x01) == 0); + visor = visor.SetValue((bytes[90] & 0x10) != 0); + weapon = weapon.SetValue((bytes[90] & 0x02) == 0); + break; + } + default: throw new Exception($"Can not parse Base64 string into design for migration:\n\tInvalid Version {bytes[0]}."); + } + + customizeFlags = (applicationFlags & 0x01) != 0 ? CustomizeFlagExtensions.All : 0; + wet = (applicationFlags & 0x02) != 0 ? QuadBool.True : QuadBool.NullFalse; + hat = hat.SetEnabled((applicationFlags & 0x04) != 0); + weapon = weapon.SetEnabled((applicationFlags & 0x08) != 0); + visor = visor.SetEnabled((applicationFlags & 0x10) != 0); + writeinternal = (applicationFlags & 0x20) != 0; + + equipFlags = 0; + equipFlags |= (equipFlagsS & 0x0001) != 0 ? EquipFlag.Mainhand | EquipFlag.MainhandStain : 0; + equipFlags |= (equipFlagsS & 0x0002) != 0 ? EquipFlag.Offhand | EquipFlag.OffhandStain : 0; + var flag = 0x0002u; + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + flag <<= 1; + equipFlags |= (equipFlagsS & flag) != 0 ? slot.ToFlag() | slot.ToStainFlag() : 0; + } + + var data = new ModelData(); + unsafe + { + fixed (byte* ptr = bytes) + { + data.CustomizeData.Read(ptr + 4); + var cur = (CharacterWeapon*)(ptr + 30); + data.MainHand = cur[0]; + data.OffHand = cur[1]; + var eq = (CharacterArmor*)(cur + 2); + foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex()) + data.Equipment[slot] = eq[idx]; + } + } + + return data; + } + + public static unsafe string CreateOldBase64(in ModelData save, EquipFlag equipFlags, CustomizeFlag customizeFlags, bool wet, bool hat, + bool setHat, bool visor, bool setVisor, bool weapon, bool setWeapon, bool writeinternal, float alpha) + { + var data = stackalloc byte[Base64Size]; + data[0] = 2; + data[1] = (byte)((customizeFlags == CustomizeFlagExtensions.All ? 0x01 : 0) + | (wet ? 0x02 : 0) + | (setHat ? 0x04 : 0) + | (setWeapon ? 0x08 : 0) + | (setVisor ? 0x10 : 0) + | (writeinternal ? 0x20 : 0)); + data[2] = (byte)((equipFlags.HasFlag(EquipFlag.Mainhand) ? 0x01 : 0) + | (equipFlags.HasFlag(EquipFlag.Offhand) ? 0x02 : 0) + | (equipFlags.HasFlag(EquipFlag.Head) ? 0x04 : 0) + | (equipFlags.HasFlag(EquipFlag.Body) ? 0x08 : 0) + | (equipFlags.HasFlag(EquipFlag.Hands) ? 0x10 : 0) + | (equipFlags.HasFlag(EquipFlag.Legs) ? 0x20 : 0) + | (equipFlags.HasFlag(EquipFlag.Feet) ? 0x40 : 0) + | (equipFlags.HasFlag(EquipFlag.Ears) ? 0x80 : 0)); + data[3] = (byte)((equipFlags.HasFlag(EquipFlag.Neck) ? 0x01 : 0) + | (equipFlags.HasFlag(EquipFlag.Wrist) ? 0x02 : 0) + | (equipFlags.HasFlag(EquipFlag.RFinger) ? 0x04 : 0) + | (equipFlags.HasFlag(EquipFlag.LFinger) ? 0x08 : 0)); + save.CustomizeData.Write(data + 4); + ((CharacterWeapon*)(data + 30))[0] = save.MainHand; + ((CharacterWeapon*)(data + 30))[1] = save.OffHand; + ((CharacterArmor*)(data + 44))[0] = save.Head; + ((CharacterArmor*)(data + 44))[1] = save.Body; + ((CharacterArmor*)(data + 44))[2] = save.Hands; + ((CharacterArmor*)(data + 44))[3] = save.Legs; + ((CharacterArmor*)(data + 44))[4] = save.Feet; + ((CharacterArmor*)(data + 44))[5] = save.Ears; + ((CharacterArmor*)(data + 44))[6] = save.Neck; + ((CharacterArmor*)(data + 44))[7] = save.Wrists; + ((CharacterArmor*)(data + 44))[8] = save.RFinger; + ((CharacterArmor*)(data + 44))[9] = save.LFinger; + *(float*)(data + 84) = 1f; + data[88] = (byte)((hat ? 0x01 : 0) + | (visor ? 0x10 : 0) + | (weapon ? 0x02 : 0)); + + return Convert.ToBase64String(new Span(data, Base64Size)); + } +} diff --git a/Glamourer/Designs/DesignFileSystem.cs b/Glamourer/Designs/DesignFileSystem.cs index b1f66aa..6ac8084 100644 --- a/Glamourer/Designs/DesignFileSystem.cs +++ b/Glamourer/Designs/DesignFileSystem.cs @@ -8,7 +8,6 @@ using Dalamud.Plugin; using Glamourer.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui.Classes; using OtterGui.Filesystem; namespace Glamourer.Designs; @@ -20,9 +19,9 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable public readonly string DesignFileSystemFile; private readonly SaveService _saveService; - private readonly Design.Manager _designManager; + private readonly DesignManager _designManager; - public DesignFileSystem(Design.Manager designManager, DalamudPluginInterface pi, SaveService saveService) + public DesignFileSystem(DesignManager designManager, DalamudPluginInterface pi, SaveService saveService) { DesignFileSystemFile = GetDesignFileSystemFile(pi); _designManager = designManager; @@ -75,11 +74,11 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable _saveService.QueueSave(this); } - private void OnDataChange(Design.Manager.DesignChangeType type, Design design, object? data) + private void OnDataChange(DesignManager.DesignChangeType type, Design design, object? data) { switch (type) { - case Design.Manager.DesignChangeType.Created: + case DesignManager.DesignChangeType.Created: var originalName = design.Name.Text.FixName(); var name = originalName; var counter = 1; @@ -88,14 +87,14 @@ public sealed class DesignFileSystem : FileSystem, IDisposable, ISavable CreateLeaf(Root, name, design); break; - case Design.Manager.DesignChangeType.Deleted: + case DesignManager.DesignChangeType.Deleted: if (FindLeaf(design, out var leaf)) Delete(leaf); break; - case Design.Manager.DesignChangeType.ReloadedAll: + case DesignManager.DesignChangeType.ReloadedAll: Reload(); break; - case Design.Manager.DesignChangeType.Renamed when data is string oldName: + case DesignManager.DesignChangeType.Renamed when data is string oldName: var old = oldName.FixName(); if (Find(old, out var child) && child is not Folder) Rename(child, design.Name); diff --git a/Glamourer/Designs/ModelData.cs b/Glamourer/Designs/ModelData.cs new file mode 100644 index 0000000..e9852dd --- /dev/null +++ b/Glamourer/Designs/ModelData.cs @@ -0,0 +1,489 @@ +using System; +using Glamourer.Customization; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.String.Functions; + +namespace Glamourer.Designs; + +[Flags] +public enum ModelFlags : ushort +{ + HatVisible = 0x01, + WeaponVisible = 0x02, + VisorToggled = 0x04, +} + +public struct ModelData +{ + public readonly unsafe ModelData Clone() + { + var data = new ModelData(MainHand); + fixed (void* ptr = &this) + { + MemoryUtility.MemCpyUnchecked(&data, ptr, sizeof(ModelData)); + } + + return data; + } + + public Customize Customize = Customize.Default; + public ModelFlags Flags = ModelFlags.HatVisible | ModelFlags.WeaponVisible; + public CharacterWeapon MainHand; + public CharacterWeapon OffHand = CharacterWeapon.Empty; + + public uint ModelId = 0; + public CharacterArmor Head = CharacterArmor.Empty; + public CharacterArmor Body = CharacterArmor.Empty; + public CharacterArmor Hands = CharacterArmor.Empty; + public CharacterArmor Legs = CharacterArmor.Empty; + public CharacterArmor Feet = CharacterArmor.Empty; + public CharacterArmor Ears = CharacterArmor.Empty; + public CharacterArmor Neck = CharacterArmor.Empty; + public CharacterArmor Wrists = CharacterArmor.Empty; + public CharacterArmor RFinger = CharacterArmor.Empty; + public CharacterArmor LFinger = CharacterArmor.Empty; + + public ModelData(CharacterWeapon mainHand) + => MainHand = mainHand; + + public CharacterArmor Armor(EquipSlot slot) + => slot switch + { + EquipSlot.MainHand => MainHand.ToArmor(), + EquipSlot.OffHand => OffHand.ToArmor(), + EquipSlot.Head => Head, + EquipSlot.Body => Body, + EquipSlot.Hands => Hands, + EquipSlot.Legs => Legs, + EquipSlot.Feet => Feet, + EquipSlot.Ears => Ears, + EquipSlot.Neck => Neck, + EquipSlot.Wrists => Wrists, + EquipSlot.RFinger => RFinger, + EquipSlot.LFinger => LFinger, + _ => CharacterArmor.Empty, + }; + + public CharacterWeapon Piece(EquipSlot slot) + => slot switch + { + EquipSlot.MainHand => MainHand, + EquipSlot.OffHand => OffHand, + EquipSlot.Head => Head.ToWeapon(), + EquipSlot.Body => Body.ToWeapon(), + EquipSlot.Hands => Hands.ToWeapon(), + EquipSlot.Legs => Legs.ToWeapon(), + EquipSlot.Feet => Feet.ToWeapon(), + EquipSlot.Ears => Ears.ToWeapon(), + EquipSlot.Neck => Neck.ToWeapon(), + EquipSlot.Wrists => Wrists.ToWeapon(), + EquipSlot.RFinger => RFinger.ToWeapon(), + EquipSlot.LFinger => LFinger.ToWeapon(), + _ => CharacterWeapon.Empty, + }; + + public bool SetPiece(EquipSlot slot, SetId model, byte variant, out CharacterWeapon ret) + { + var changes = false; + switch (slot) + { + case EquipSlot.MainHand: + changes |= SetIfDifferent(ref MainHand.Set, model); + changes |= SetIfDifferent(ref MainHand.Variant, variant); + ret = MainHand; + return changes; + case EquipSlot.OffHand: + changes |= SetIfDifferent(ref OffHand.Set, model); + changes |= SetIfDifferent(ref OffHand.Variant, variant); + ret = OffHand; + return changes; + case EquipSlot.Head: + changes |= SetIfDifferent(ref Head.Set, model); + changes |= SetIfDifferent(ref Head.Variant, variant); + ret = Head.ToWeapon(); + return changes; + case EquipSlot.Body: + changes |= SetIfDifferent(ref Body.Set, model); + changes |= SetIfDifferent(ref Body.Variant, variant); + ret = Body.ToWeapon(); + return changes; + case EquipSlot.Hands: + changes |= SetIfDifferent(ref Hands.Set, model); + changes |= SetIfDifferent(ref Hands.Variant, variant); + ret = Hands.ToWeapon(); + return changes; + case EquipSlot.Legs: + changes |= SetIfDifferent(ref Legs.Set, model); + changes |= SetIfDifferent(ref Legs.Variant, variant); + ret = Legs.ToWeapon(); + return changes; + case EquipSlot.Feet: + changes |= SetIfDifferent(ref Feet.Set, model); + changes |= SetIfDifferent(ref Feet.Variant, variant); + ret = Feet.ToWeapon(); + return changes; + case EquipSlot.Ears: + changes |= SetIfDifferent(ref Ears.Set, model); + changes |= SetIfDifferent(ref Ears.Variant, variant); + ret = Ears.ToWeapon(); + return changes; + case EquipSlot.Neck: + changes |= SetIfDifferent(ref Neck.Set, model); + changes |= SetIfDifferent(ref Neck.Variant, variant); + ret = Neck.ToWeapon(); + return changes; + case EquipSlot.Wrists: + changes |= SetIfDifferent(ref Wrists.Set, model); + changes |= SetIfDifferent(ref Wrists.Variant, variant); + ret = Wrists.ToWeapon(); + return changes; + case EquipSlot.RFinger: + changes |= SetIfDifferent(ref RFinger.Set, model); + changes |= SetIfDifferent(ref RFinger.Variant, variant); + ret = RFinger.ToWeapon(); + return changes; + case EquipSlot.LFinger: + changes |= SetIfDifferent(ref LFinger.Set, model); + changes |= SetIfDifferent(ref LFinger.Variant, variant); + ret = LFinger.ToWeapon(); + return changes; + default: + ret = CharacterWeapon.Empty; + return changes; + } + } + + public bool SetPiece(EquipSlot slot, SetId model, WeaponType type, byte variant, out CharacterWeapon ret) + { + var changes = false; + switch (slot) + { + case EquipSlot.MainHand: + changes |= SetIfDifferent(ref MainHand.Set, model); + changes |= SetIfDifferent(ref MainHand.Type, type); + changes |= SetIfDifferent(ref MainHand.Variant, variant); + ret = MainHand; + return changes; + case EquipSlot.OffHand: + changes |= SetIfDifferent(ref OffHand.Set, model); + changes |= SetIfDifferent(ref OffHand.Type, type); + changes |= SetIfDifferent(ref OffHand.Variant, variant); + ret = OffHand; + return changes; + case EquipSlot.Head: + changes |= SetIfDifferent(ref Head.Set, model); + changes |= SetIfDifferent(ref Head.Variant, variant); + ret = Head.ToWeapon(); + return changes; + case EquipSlot.Body: + changes |= SetIfDifferent(ref Body.Set, model); + changes |= SetIfDifferent(ref Body.Variant, variant); + ret = Body.ToWeapon(); + return changes; + case EquipSlot.Hands: + changes |= SetIfDifferent(ref Hands.Set, model); + changes |= SetIfDifferent(ref Hands.Variant, variant); + ret = Hands.ToWeapon(); + return changes; + case EquipSlot.Legs: + changes |= SetIfDifferent(ref Legs.Set, model); + changes |= SetIfDifferent(ref Legs.Variant, variant); + ret = Legs.ToWeapon(); + return changes; + case EquipSlot.Feet: + changes |= SetIfDifferent(ref Feet.Set, model); + changes |= SetIfDifferent(ref Feet.Variant, variant); + ret = Feet.ToWeapon(); + return changes; + case EquipSlot.Ears: + changes |= SetIfDifferent(ref Ears.Set, model); + changes |= SetIfDifferent(ref Ears.Variant, variant); + ret = Ears.ToWeapon(); + return changes; + case EquipSlot.Neck: + changes |= SetIfDifferent(ref Neck.Set, model); + changes |= SetIfDifferent(ref Neck.Variant, variant); + ret = Neck.ToWeapon(); + return changes; + case EquipSlot.Wrists: + changes |= SetIfDifferent(ref Wrists.Set, model); + changes |= SetIfDifferent(ref Wrists.Variant, variant); + ret = Wrists.ToWeapon(); + return changes; + case EquipSlot.RFinger: + changes |= SetIfDifferent(ref RFinger.Set, model); + changes |= SetIfDifferent(ref RFinger.Variant, variant); + ret = RFinger.ToWeapon(); + return changes; + case EquipSlot.LFinger: + changes |= SetIfDifferent(ref LFinger.Set, model); + changes |= SetIfDifferent(ref LFinger.Variant, variant); + ret = LFinger.ToWeapon(); + return changes; + default: + ret = CharacterWeapon.Empty; + return changes; + } + } + + public bool SetPiece(EquipSlot slot, SetId model, byte variant, StainId stain, out CharacterWeapon ret) + { + var changes = false; + switch (slot) + { + case EquipSlot.MainHand: + changes |= SetIfDifferent(ref MainHand.Set, model); + changes |= SetIfDifferent(ref MainHand.Variant, variant); + changes |= SetIfDifferent(ref MainHand.Stain, stain); + ret = MainHand; + return changes; + case EquipSlot.OffHand: + changes |= SetIfDifferent(ref OffHand.Set, model); + changes |= SetIfDifferent(ref OffHand.Variant, variant); + changes |= SetIfDifferent(ref OffHand.Stain, stain); + ret = OffHand; + return changes; + case EquipSlot.Head: + changes |= SetIfDifferent(ref Head.Set, model); + changes |= SetIfDifferent(ref Head.Variant, variant); + changes |= SetIfDifferent(ref Head.Stain, stain); + ret = Head.ToWeapon(); + return changes; + case EquipSlot.Body: + changes |= SetIfDifferent(ref Body.Set, model); + changes |= SetIfDifferent(ref Body.Variant, variant); + changes |= SetIfDifferent(ref Body.Stain, stain); + ret = Body.ToWeapon(); + return changes; + case EquipSlot.Hands: + changes |= SetIfDifferent(ref Hands.Set, model); + changes |= SetIfDifferent(ref Hands.Variant, variant); + changes |= SetIfDifferent(ref Hands.Stain, stain); + ret = Hands.ToWeapon(); + return changes; + case EquipSlot.Legs: + changes |= SetIfDifferent(ref Legs.Set, model); + changes |= SetIfDifferent(ref Legs.Variant, variant); + changes |= SetIfDifferent(ref Legs.Stain, stain); + ret = Legs.ToWeapon(); + return changes; + case EquipSlot.Feet: + changes |= SetIfDifferent(ref Feet.Set, model); + changes |= SetIfDifferent(ref Feet.Variant, variant); + changes |= SetIfDifferent(ref Feet.Stain, stain); + ret = Feet.ToWeapon(); + return changes; + case EquipSlot.Ears: + changes |= SetIfDifferent(ref Ears.Set, model); + changes |= SetIfDifferent(ref Ears.Variant, variant); + changes |= SetIfDifferent(ref Ears.Stain, stain); + ret = Ears.ToWeapon(); + return changes; + case EquipSlot.Neck: + changes |= SetIfDifferent(ref Neck.Set, model); + changes |= SetIfDifferent(ref Neck.Variant, variant); + changes |= SetIfDifferent(ref Neck.Stain, stain); + ret = Neck.ToWeapon(); + return changes; + case EquipSlot.Wrists: + changes |= SetIfDifferent(ref Wrists.Set, model); + changes |= SetIfDifferent(ref Wrists.Variant, variant); + changes |= SetIfDifferent(ref Wrists.Stain, stain); + ret = Wrists.ToWeapon(); + return changes; + case EquipSlot.RFinger: + changes |= SetIfDifferent(ref RFinger.Set, model); + changes |= SetIfDifferent(ref RFinger.Variant, variant); + changes |= SetIfDifferent(ref RFinger.Stain, stain); + ret = RFinger.ToWeapon(); + return changes; + case EquipSlot.LFinger: + changes |= SetIfDifferent(ref LFinger.Set, model); + changes |= SetIfDifferent(ref LFinger.Variant, variant); + changes |= SetIfDifferent(ref LFinger.Stain, stain); + ret = LFinger.ToWeapon(); + return changes; + default: + ret = CharacterWeapon.Empty; + return changes; + } + } + + public bool SetPiece(EquipSlot slot, CharacterWeapon weapon, out CharacterWeapon ret) + => SetPiece(slot, weapon.Set, weapon.Type, (byte)weapon.Variant, weapon.Stain, out ret); + + public bool SetPiece(EquipSlot slot, CharacterArmor armor, out CharacterWeapon ret) + => SetPiece(slot, armor.Set, armor.Variant, armor.Stain, out ret); + + public bool SetPiece(EquipSlot slot, SetId model, WeaponType type, byte variant, StainId stain, out CharacterWeapon ret) + { + var changes = false; + switch (slot) + { + case EquipSlot.MainHand: + changes |= SetIfDifferent(ref MainHand.Set, model); + changes |= SetIfDifferent(ref MainHand.Type, type); + changes |= SetIfDifferent(ref MainHand.Variant, variant); + changes |= SetIfDifferent(ref MainHand.Stain, stain); + ret = MainHand; + return changes; + case EquipSlot.OffHand: + changes |= SetIfDifferent(ref OffHand.Set, model); + changes |= SetIfDifferent(ref OffHand.Type, type); + changes |= SetIfDifferent(ref OffHand.Variant, variant); + changes |= SetIfDifferent(ref OffHand.Stain, stain); + ret = OffHand; + return changes; + case EquipSlot.Head: + changes |= SetIfDifferent(ref Head.Set, model); + changes |= SetIfDifferent(ref Head.Variant, variant); + changes |= SetIfDifferent(ref Head.Stain, stain); + ret = Head.ToWeapon(); + return changes; + case EquipSlot.Body: + changes |= SetIfDifferent(ref Body.Set, model); + changes |= SetIfDifferent(ref Body.Variant, variant); + changes |= SetIfDifferent(ref Body.Stain, stain); + ret = Body.ToWeapon(); + return changes; + case EquipSlot.Hands: + changes |= SetIfDifferent(ref Hands.Set, model); + changes |= SetIfDifferent(ref Hands.Variant, variant); + changes |= SetIfDifferent(ref Hands.Stain, stain); + ret = Hands.ToWeapon(); + return changes; + case EquipSlot.Legs: + changes |= SetIfDifferent(ref Legs.Set, model); + changes |= SetIfDifferent(ref Legs.Variant, variant); + changes |= SetIfDifferent(ref Legs.Stain, stain); + ret = Legs.ToWeapon(); + return changes; + case EquipSlot.Feet: + changes |= SetIfDifferent(ref Feet.Set, model); + changes |= SetIfDifferent(ref Feet.Variant, variant); + changes |= SetIfDifferent(ref Feet.Stain, stain); + ret = Feet.ToWeapon(); + return changes; + case EquipSlot.Ears: + changes |= SetIfDifferent(ref Ears.Set, model); + changes |= SetIfDifferent(ref Ears.Variant, variant); + changes |= SetIfDifferent(ref Ears.Stain, stain); + ret = Ears.ToWeapon(); + return changes; + case EquipSlot.Neck: + changes |= SetIfDifferent(ref Neck.Set, model); + changes |= SetIfDifferent(ref Neck.Variant, variant); + changes |= SetIfDifferent(ref Neck.Stain, stain); + ret = Neck.ToWeapon(); + return changes; + case EquipSlot.Wrists: + changes |= SetIfDifferent(ref Wrists.Set, model); + changes |= SetIfDifferent(ref Wrists.Variant, variant); + changes |= SetIfDifferent(ref Wrists.Stain, stain); + ret = Wrists.ToWeapon(); + return changes; + case EquipSlot.RFinger: + changes |= SetIfDifferent(ref RFinger.Set, model); + changes |= SetIfDifferent(ref RFinger.Variant, variant); + changes |= SetIfDifferent(ref RFinger.Stain, stain); + ret = RFinger.ToWeapon(); + return changes; + case EquipSlot.LFinger: + changes |= SetIfDifferent(ref LFinger.Set, model); + changes |= SetIfDifferent(ref LFinger.Variant, variant); + changes |= SetIfDifferent(ref LFinger.Stain, stain); + ret = LFinger.ToWeapon(); + return changes; + default: + ret = CharacterWeapon.Empty; + return changes; + } + } + + public StainId Stain(EquipSlot slot) + => slot switch + { + EquipSlot.MainHand => MainHand.Stain, + EquipSlot.OffHand => OffHand.Stain, + EquipSlot.Head => Head.Stain, + EquipSlot.Body => Body.Stain, + EquipSlot.Hands => Hands.Stain, + EquipSlot.Legs => Legs.Stain, + EquipSlot.Feet => Feet.Stain, + EquipSlot.Ears => Ears.Stain, + EquipSlot.Neck => Neck.Stain, + EquipSlot.Wrists => Wrists.Stain, + EquipSlot.RFinger => RFinger.Stain, + EquipSlot.LFinger => LFinger.Stain, + _ => 0, + }; + + public bool SetStain(EquipSlot slot, StainId stain, out CharacterWeapon ret) + { + var changes = false; + switch (slot) + { + case EquipSlot.MainHand: + changes = SetIfDifferent(ref MainHand.Stain, stain); + ret = MainHand; + return changes; + case EquipSlot.OffHand: + changes = SetIfDifferent(ref OffHand.Stain, stain); + ret = OffHand; + return changes; + case EquipSlot.Head: + changes = SetIfDifferent(ref Head.Stain, stain); + ret = Head.ToWeapon(); + return changes; + case EquipSlot.Body: + changes = SetIfDifferent(ref Body.Stain, stain); + ret = Body.ToWeapon(); + return changes; + case EquipSlot.Hands: + changes = SetIfDifferent(ref Hands.Stain, stain); + ret = Hands.ToWeapon(); + return changes; + case EquipSlot.Legs: + changes = SetIfDifferent(ref Legs.Stain, stain); + ret = Legs.ToWeapon(); + return changes; + case EquipSlot.Feet: + changes = SetIfDifferent(ref Feet.Stain, stain); + ret = Feet.ToWeapon(); + return changes; + case EquipSlot.Ears: + changes = SetIfDifferent(ref Ears.Stain, stain); + ret = Ears.ToWeapon(); + return changes; + case EquipSlot.Neck: + changes = SetIfDifferent(ref Neck.Stain, stain); + ret = Neck.ToWeapon(); + return changes; + case EquipSlot.Wrists: + changes = SetIfDifferent(ref Wrists.Stain, stain); + ret = Wrists.ToWeapon(); + return changes; + case EquipSlot.RFinger: + changes = SetIfDifferent(ref RFinger.Stain, stain); + ret = RFinger.ToWeapon(); + return changes; + case EquipSlot.LFinger: + changes = SetIfDifferent(ref LFinger.Stain, stain); + ret = LFinger.ToWeapon(); + return changes; + default: + ret = CharacterWeapon.Empty; + return false; + } + } + + private static bool SetIfDifferent(ref T old, T value) where T : IEquatable + { + if (old.Equals(value)) + return false; + + old = value; + return true; + } +} diff --git a/Glamourer/DrawObjectManager.cs b/Glamourer/DrawObjectManager.cs index 47478c7..3c25ad8 100644 --- a/Glamourer/DrawObjectManager.cs +++ b/Glamourer/DrawObjectManager.cs @@ -16,19 +16,17 @@ public class DrawObjectManager : IDisposable private readonly ItemManager _items; private readonly ActorService _actors; private readonly ActiveDesign.Manager _manager; - private readonly Interop.Interop _interop; private readonly PenumbraAttach _penumbra; - public DrawObjectManager(ItemManager items, ActorService actors, ActiveDesign.Manager manager, Interop.Interop interop, + public DrawObjectManager(ItemManager items, ActorService actors, ActiveDesign.Manager manager, PenumbraAttach penumbra) { _items = items; _actors = actors; _manager = manager; - _interop = interop; _penumbra = penumbra; - _interop.EquipUpdate += FixEquipment; + //_interop.EquipUpdate += FixEquipment; _penumbra.CreatingCharacterBase += ApplyActiveDesign; } @@ -62,7 +60,7 @@ public class DrawObjectManager : IDisposable var gameObjectCustomize = gameObject.Customize; var customize = new Customize((CustomizeData*)customizePtr); if (gameObjectCustomize.Equals(customize)) - customize.Load(design.Customize()); + customize.Load(design.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. @@ -70,7 +68,7 @@ public class DrawObjectManager : IDisposable var equip = new CharacterEquip((CharacterArmor*)equipDataPtr); if (gameObjectEquip.Equals(equip)) { - var saveEquip = design.Equipment(); + var saveEquip = design.Equipment; foreach (var slot in EquipSlotExtensions.EquipmentSlots) { (_, equip[slot]) = @@ -82,6 +80,6 @@ public class DrawObjectManager : IDisposable public void Dispose() { _penumbra.CreatingCharacterBase -= ApplyActiveDesign; - _interop.EquipUpdate -= FixEquipment; + //_interop.EquipUpdate -= FixEquipment; } } diff --git a/Glamourer/Glamourer.cs b/Glamourer/Glamourer.cs index 0179f46..f3f09c8 100644 --- a/Glamourer/Glamourer.cs +++ b/Glamourer/Glamourer.cs @@ -1,6 +1,7 @@ using System.Reflection; using Dalamud.Plugin; using Glamourer.Gui; +using Glamourer.Interop; using Glamourer.Services; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; @@ -33,6 +34,12 @@ public partial class Glamourer : IDalamudPlugin _services.GetRequiredService(); _services.GetRequiredService(); _services.GetRequiredService(); + _services.GetRequiredService(); + _services.GetRequiredService(); + _services.GetRequiredService(); + _services.GetRequiredService(); + _services.GetRequiredService(); + _services.GetRequiredService(); } catch { diff --git a/Glamourer/Gui/Designs/DesignFileSystemSelector.cs b/Glamourer/Gui/Designs/DesignFileSystemSelector.cs index 17c110b..2ac388d 100644 --- a/Glamourer/Gui/Designs/DesignFileSystemSelector.cs +++ b/Glamourer/Gui/Designs/DesignFileSystemSelector.cs @@ -9,34 +9,34 @@ namespace Glamourer.Gui.Designs; public sealed class DesignFileSystemSelector : FileSystemSelector { - private readonly Design.Manager _manager; + private readonly DesignManager _designManager; public struct DesignState { } - public DesignFileSystemSelector(Design.Manager manager, DesignFileSystem fileSystem, KeyState keyState) + public DesignFileSystemSelector(DesignManager designManager, DesignFileSystem fileSystem, KeyState keyState) : base(fileSystem, keyState) { - _manager = manager; - _manager.DesignChange += OnDesignChange; + _designManager = designManager; + _designManager.DesignChange += OnDesignChange; AddButton(DeleteButton, 1000); } public override void Dispose() { base.Dispose(); - _manager.DesignChange -= OnDesignChange; + _designManager.DesignChange -= OnDesignChange; } - private void OnDesignChange(Design.Manager.DesignChangeType type, Design design, object? oldData) + private void OnDesignChange(DesignManager.DesignChangeType type, Design design, object? oldData) { switch (type) { - case Design.Manager.DesignChangeType.ReloadedAll: - case Design.Manager.DesignChangeType.Renamed: - case Design.Manager.DesignChangeType.AddedTag: - case Design.Manager.DesignChangeType.ChangedTag: - case Design.Manager.DesignChangeType.RemovedTag: + case DesignManager.DesignChangeType.ReloadedAll: + case DesignManager.DesignChangeType.Renamed: + case DesignManager.DesignChangeType.AddedTag: + case DesignManager.DesignChangeType.ChangedTag: + case DesignManager.DesignChangeType.RemovedTag: SetFilterDirty(); break; } @@ -54,6 +54,6 @@ public sealed class DesignFileSystemSelector : FileSystemSelector pair) @@ -256,11 +260,12 @@ public partial class Interface private void DrawSelectable(KeyValuePair pair) { var equal = pair.Key.Equals(_identifier); - if (ImGui.Selectable(pair.Value.Label, equal) && !equal) + if (ImGui.Selectable(pair.Value.Label, equal) || equal) { _identifier = pair.Key.CreatePermanent(); _currentData = pair.Value; - _currentSave = _currentData.Valid ? _activeDesigns.GetOrCreateSave(_currentData.Objects[0]) : null; + if (!_activeDesigns.TryGetValue(_identifier, out _currentSave)) + _currentSave = _currentData.Valid ? _activeDesigns.GetOrCreateSave(_currentData.Objects[0]) : null; } } diff --git a/Glamourer/Gui/Interface.DesignTab.cs b/Glamourer/Gui/Interface.DesignTab.cs index 97c7172..82927eb 100644 --- a/Glamourer/Gui/Interface.DesignTab.cs +++ b/Glamourer/Gui/Interface.DesignTab.cs @@ -2,10 +2,16 @@ using System.Numerics; using Dalamud.Game.ClientState.Keys; using Dalamud.Interface; +using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent; using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Gui.Designs; +using Glamourer.Interop; +using Glamourer.Services; +using Glamourer.State; using ImGuiNET; +using Microsoft.VisualBasic; +using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Enums; @@ -18,14 +24,19 @@ public partial class Interface public readonly DesignFileSystemSelector Selector; private readonly Interface _main; private readonly DesignFileSystem _fileSystem; - private readonly Design.Manager _manager; + private readonly DesignManager _designManager; + private readonly ActiveDesign.Manager _activeDesignManager; + private readonly ObjectManager _objects; - public DesignTab(Interface main, Design.Manager manager, DesignFileSystem fileSystem, KeyState keyState) + public DesignTab(Interface main, DesignManager designManager, DesignFileSystem fileSystem, KeyState keyState, + ActiveDesign.Manager activeDesignManager, ObjectManager objects) { - _main = main; - _manager = manager; - _fileSystem = fileSystem; - Selector = new DesignFileSystemSelector(manager, fileSystem, keyState); + _main = main; + _designManager = designManager; + _fileSystem = fileSystem; + _activeDesignManager = activeDesignManager; + _objects = objects; + Selector = new DesignFileSystemSelector(designManager, fileSystem, keyState); } public void Dispose() @@ -45,13 +56,30 @@ public partial class Interface public float GetDesignSelectorSize() => 200f * ImGuiHelpers.GlobalScale; - public void DrawDesignPanel() + private void ApplySelfButton() { - using var child = ImRaii.Child("##DesignPanel", new Vector2(-0.001f), true, ImGuiWindowFlags.HorizontalScrollbar); - if (!child || Selector.Selected == null) + var self = _objects.Player; + if (!ImGuiUtil.DrawDisabledButton("Apply to Self", Vector2.Zero, string.Empty, !self.Valid)) return; - _main._customizationDrawer.Draw(Selector.Selected.Customize(), CustomizeFlagExtensions.All, true); + var design = _activeDesignManager.GetOrCreateSave(self); + _activeDesignManager.ApplyDesign(design, Selector.Selected!, false); + } + + public void DrawDesignPanel() + { + if (Selector.Selected == null) + return; + + using var group = ImRaii.Group(); + + ApplySelfButton(); + + using var child = ImRaii.Child("##DesignPanel", new Vector2(-0.001f), true, ImGuiWindowFlags.HorizontalScrollbar); + if (!child) + return; + + _main._customizationDrawer.Draw(Selector.Selected.Customize, CustomizeFlagExtensions.All, true); foreach (var slot in EquipSlotExtensions.EqdpSlots) { var current = Selector.Selected.Armor(slot); diff --git a/Glamourer/Gui/Interface.cs b/Glamourer/Gui/Interface.cs index 41fdbe5..243a38a 100644 --- a/Glamourer/Gui/Interface.cs +++ b/Glamourer/Gui/Interface.cs @@ -29,7 +29,7 @@ public partial class Interface : Window, IDisposable private readonly DebugStateTab _debugStateTab; private readonly DebugDataTab _debugDataTab; - public Interface(DalamudPluginInterface pi, ItemManager items, ActiveDesign.Manager activeDesigns, Design.Manager manager, + public Interface(DalamudPluginInterface pi, ItemManager items, ActiveDesign.Manager activeDesigns, DesignManager designManager, DesignFileSystem fileSystem, ObjectManager objects, CustomizationService customization, Configuration config, DataManager gameData, TargetManager targets, ActorService actors, KeyState keyState) : base(GetLabel()) { @@ -46,7 +46,7 @@ public partial class Interface : Window, IDisposable _actorTab = new ActorTab(this, activeDesigns, objects, targets, actors, items); _debugStateTab = new DebugStateTab(activeDesigns); _debugDataTab = new DebugDataTab(customization); - _designTab = new DesignTab(this, manager, fileSystem, keyState); + _designTab = new DesignTab(this, designManager, fileSystem, keyState, activeDesigns, objects); } public override void Draw() diff --git a/Glamourer/Interop/Actor.cs b/Glamourer/Interop/Actor.cs index b3126fe..b3a875d 100644 --- a/Glamourer/Interop/Actor.cs +++ b/Glamourer/Interop/Actor.cs @@ -39,6 +39,9 @@ public unsafe partial struct Actor : IEquatable, IDesignable return false; } + public override string ToString() + => Pointer != null ? Utf8Name.ToString() : "Invalid"; + public bool IsAvailable => Pointer->GameObject.GetIsTargetable(); diff --git a/Glamourer/Interop/ChangeCustomizeService.cs b/Glamourer/Interop/ChangeCustomizeService.cs new file mode 100644 index 0000000..e770025 --- /dev/null +++ b/Glamourer/Interop/ChangeCustomizeService.cs @@ -0,0 +1,24 @@ +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop; + +public unsafe class ChangeCustomizeService +{ + public ChangeCustomizeService() + => SignatureHelper.Initialise(this); + + public delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment); + + [Signature(Sigs.ChangeCustomize)] + private readonly ChangeCustomizeDelegate _changeCustomize = null!; + + public bool UpdateCustomize(Actor actor, CustomizeData customize) + { + if (customize.Data == null || !actor.Valid || !actor.DrawObject.Valid) + return false; + + return _changeCustomize(actor.DrawObject.Pointer, customize.Data, 1); + } +} diff --git a/Glamourer/Interop/JobService.cs b/Glamourer/Interop/JobService.cs new file mode 100644 index 0000000..7cd1e40 --- /dev/null +++ b/Glamourer/Interop/JobService.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using Dalamud.Data; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using Glamourer.Structs; + +namespace Glamourer.Interop; + +public class JobService : IDisposable +{ + public readonly IReadOnlyDictionary Jobs; + public readonly IReadOnlyDictionary JobGroups; + + public event Action? JobChanged; + + public JobService(DataManager gameData) + { + SignatureHelper.Initialise(this); + Jobs = GameData.Jobs(gameData); + JobGroups = GameData.JobGroups(gameData); + _changeJobHook.Enable(); + } + + public void Dispose() + { + _changeJobHook.Dispose(); + } + + private delegate void ChangeJobDelegate(nint data, uint job); + + [Signature(Sigs.ChangeJob, DetourName = nameof(ChangeJobDetour))] + private readonly Hook _changeJobHook = null!; + + private void ChangeJobDetour(nint data, uint jobIndex) + { + _changeJobHook.Original(data, jobIndex); + var actor = (Actor)(data - Offsets.Character.ClassJobContainer); + var job = Jobs[(byte)jobIndex]; + Glamourer.Log.Excessive($"{actor} changed job to {job}"); + JobChanged?.Invoke(actor, job); + } +} diff --git a/Glamourer/Interop/RedrawManager.Customize.cs b/Glamourer/Interop/RedrawManager.Customize.cs deleted file mode 100644 index 68dcdd2..0000000 --- a/Glamourer/Interop/RedrawManager.Customize.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Glamourer.Customization; -using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; - -namespace Glamourer.Interop; - -public unsafe partial class RedrawManager -{ - - - -} diff --git a/Glamourer/Interop/RedrawManager.Equipment.cs b/Glamourer/Interop/RedrawManager.Equipment.cs deleted file mode 100644 index 3bf2b39..0000000 --- a/Glamourer/Interop/RedrawManager.Equipment.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using Dalamud.Hooking; -using Dalamud.Utility.Signatures; -using Glamourer.Designs; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Glamourer.Interop; - -public unsafe partial class RedrawManager -{ - private delegate ulong FlagSlotForUpdateDelegate(nint drawObject, uint slot, CharacterArmor* data); - - // This gets called when one of the ten equip items of an existing draw object gets changed. - [Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))] - private readonly Hook _flagSlotForUpdateHook = null!; - - public void UpdateSlot(DrawObject drawObject, EquipSlot slot, CharacterArmor data) - => FlagSlotForUpdateDetourBase(drawObject.Address, slot.ToIndex(), &data, true); - - public void UpdateStain(DrawObject drawObject, EquipSlot slot, StainId stain) - { - var armor = drawObject.Equip[slot] with { Stain = stain}; - UpdateSlot(drawObject, slot, armor); - } - - private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) - => FlagSlotForUpdateDetourBase(drawObject, slotIdx, data, false); - - private ulong FlagSlotForUpdateDetourBase(nint drawObject, uint slotIdx, CharacterArmor* data, bool manual) - { -// try -// { -// var slot = slotIdx.ToEquipSlot(); -// Glamourer.Log.Verbose( -// $"Flagged slot {slot} of 0x{(ulong)drawObject:X} for update with {data->Set.Value}-{data->Variant} (Stain {data->Stain.Value})."); -// HandleEquipUpdate(drawObject, slot, ref *data, manual); -// } -// catch (Exception ex) -// { -// Glamourer.Log.Error($"Error invoking SlotUpdate:\n{ex}"); -// } -// - return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data); - - //try - //{ - // var actor = Glamourer.Penumbra.GameObjectFromDrawObject((IntPtr)drawObject); - // var identifier = actor.GetIdentifier(); - // - // if (_fixedDesigns.TryGetDesign(identifier, out var design)) - // { - // PluginLog.Information($"Loaded {slot} from fixed design for {identifier}."); - // (var replaced, *data) = - // Glamourer.Items.RestrictedGear.ResolveRestricted(design.Armor(slot).Model, 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.Items.RestrictedGear.ResolveRestricted(*data, slot, (Race)drawObject->Race, (Gender)(drawObject->Sex + 1)); - // save2.Data.Equipment[slot] = *data; - // } - //} - //catch (Exception e) - //{ - // PluginLog.Error($"Error on loading new gear:\n{e}"); - //} - // - //return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data); - } -} diff --git a/Glamourer/Interop/RedrawManager.Weapons.cs b/Glamourer/Interop/RedrawManager.Weapons.cs deleted file mode 100644 index ae6b25c..0000000 --- a/Glamourer/Interop/RedrawManager.Weapons.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Dalamud.Hooking; -using Dalamud.Logging; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; - -namespace Glamourer.Interop; - -public unsafe partial class RedrawManager -{ - public static readonly int CharacterWeaponOffset = (int)Marshal.OffsetOf("DrawData"); - - 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(Penumbra.GameData.Sigs.WeaponReload, 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(_actors.AwaitedService); - if (_fixedDesignManager.TryGetDesign(identifier, out var save)) - { - PluginLog.Information($"Loaded weapon from fixed design for {identifier}."); - weapon = slot switch - { - 0 => save.WeaponMain.Model.Value, - 1 => save.WeaponOff.Model.Value, - _ => weapon, - }; - } - else if (redrawOnEquality == 1 && _stateManager.TryGetValue(identifier, out var save2)) - { - PluginLog.Information($"Loaded weapon from current design for {identifier}."); - //switch (slot) - //{ - // case 0: - // save2.MainHand = new CharacterWeapon(weapon); - // break; - // case 1: - // save2.Data.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, 1, 0, 1, 0); - LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, off.Value, 1, 0, 1, 0); - } -} diff --git a/Glamourer/Interop/RedrawManager.cs b/Glamourer/Interop/RedrawManager.cs index 453f984..c6ba3e7 100644 --- a/Glamourer/Interop/RedrawManager.cs +++ b/Glamourer/Interop/RedrawManager.cs @@ -1,224 +1,45 @@ using System; -using System.Diagnostics; +using System.Collections.Generic; using System.Linq; -using Dalamud.Hooking; +using Dalamud.Game.ClientState; using Dalamud.Logging; using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Glamourer.Api; using Glamourer.Customization; using Glamourer.Services; using Glamourer.State; -using Glamourer.Structs; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using CustomizeData = Penumbra.GameData.Structs.CustomizeData; namespace Glamourer.Interop; -public partial class Interop : IDisposable -{ - private readonly JobService _jobService; - - public Interop(JobService jobService) - { - _jobService = jobService; - SignatureHelper.Initialise(this); - _changeJobHook.Enable(); - _flagSlotForUpdateHook.Enable(); - _setupVisorHook.Enable(); - } - - public void Dispose() - { - _changeJobHook.Dispose(); - _flagSlotForUpdateHook.Dispose(); - _setupVisorHook.Dispose(); - } - - public static unsafe bool GetVisorState(nint humanPtr) - { - if (humanPtr == IntPtr.Zero) - return false; - - var data = (Human*)humanPtr; - var flags = &data->CharacterBase.UnkFlags_01; - return (*flags & Offsets.DrawObjectVisorStateFlag) != 0; - } - - public static unsafe void SetVisorState(nint humanPtr, bool on) - { - if (humanPtr == IntPtr.Zero) - return; - - var data = (Human*)humanPtr; - var flags = &data->CharacterBase.UnkFlags_01; - var state = (*flags & Offsets.DrawObjectVisorStateFlag) != 0; - if (state == on) - return; - - var newFlag = (byte)(on ? *flags | Offsets.DrawObjectVisorStateFlag : *flags & ~Offsets.DrawObjectVisorStateFlag); - *flags = (byte)(newFlag | Offsets.DrawObjectVisorToggleFlag); - } -} - -public partial class Interop -{ - private delegate void UpdateVisorDelegateInternal(nint humanPtr, ushort modelId, bool on); - public delegate void UpdateVisorDelegate(DrawObject human, SetId modelId, ref bool on); - - [Signature(Penumbra.GameData.Sigs.SetupVisor, DetourName = nameof(SetupVisorDetour))] - private readonly Hook _setupVisorHook = null!; - - public event UpdateVisorDelegate? VisorUpdate; - - private void SetupVisorDetour(nint humanPtr, ushort modelId, bool on) - { - InvokeVisorEvent(humanPtr, modelId, ref on); - _setupVisorHook.Original(humanPtr, modelId, on); - } - - private void InvokeVisorEvent(DrawObject drawObject, SetId modelId, ref bool on) - { - if (VisorUpdate == null) - { - Glamourer.Log.Verbose($"Visor setup on 0x{drawObject.Address:X} with {modelId.Value}, setting to {on}."); - return; - } - - var initialValue = on; - foreach (var del in VisorUpdate.GetInvocationList().OfType()) - { - try - { - del(drawObject, modelId, ref on); - } - catch (Exception ex) - { - Glamourer.Log.Error($"Could not invoke {nameof(VisorUpdate)} Subscriber:\n{ex}"); - } - } - - Glamourer.Log.Verbose( - $"Visor setup on 0x{drawObject.Address:X} with {modelId.Value}, setting to {on}, initial call was {initialValue}."); - } -} - -public unsafe partial class Interop -{ - private delegate ulong FlagSlotForUpdateDelegateIntern(nint drawObject, uint slot, CharacterArmor* data); - public delegate void FlagSlotForUpdateDelegate(DrawObject drawObject, EquipSlot slot, ref CharacterArmor item); - - // This gets called when one of the ten equip items of an existing draw object gets changed. - [Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))] - private readonly Hook _flagSlotForUpdateHook = null!; - - public event FlagSlotForUpdateDelegate? EquipUpdate; - - public ulong FlagSlotForUpdateInterop(DrawObject drawObject, EquipSlot slot, CharacterArmor armor) - => _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); - - public void UpdateSlot(DrawObject drawObject, EquipSlot slot, CharacterArmor data) - { - InvokeFlagSlotEvent(drawObject, slot, ref data); - FlagSlotForUpdateInterop(drawObject, slot, data); - } - - public void UpdateStain(DrawObject drawObject, EquipSlot slot, StainId stain) - { - var armor = drawObject.Equip[slot] with { Stain = stain }; - UpdateSlot(drawObject, slot, armor); - } - - private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) - { - var slot = slotIdx.ToEquipSlot(); - InvokeFlagSlotEvent(drawObject, slot, ref *data); - return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data); - } - - private void InvokeFlagSlotEvent(DrawObject drawObject, EquipSlot slot, ref CharacterArmor armor) - { - if (EquipUpdate == null) - { - Glamourer.Log.Verbose( - $"{slot} updated on 0x{drawObject.Address:X} to {armor.Set.Value}-{armor.Variant} with stain {armor.Stain.Value}."); - return; - } - - var iv = armor; - foreach (var del in EquipUpdate.GetInvocationList().OfType()) - { - try - { - del(drawObject, slot, ref armor); - } - catch (Exception ex) - { - Glamourer.Log.Error($"Could not invoke {nameof(EquipUpdate)} Subscriber:\n{ex}"); - } - } - - Glamourer.Log.Verbose( - $"{slot} updated on 0x{drawObject.Address:X} to {armor.Set.Value}-{armor.Variant} with stain {armor.Stain.Value}, initial armor was {iv.Set.Value}-{iv.Variant} with stain {iv.Stain.Value}."); - } -} - -public unsafe partial class Interop -{ - public delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment); - - [Signature(Sigs.ChangeCustomize)] - private readonly ChangeCustomizeDelegate _changeCustomize = null!; - - public bool UpdateCustomize(Actor actor, CustomizeData customize) - { - Debug.Assert(customize.Data != null, "Customize was invalid."); - - if (!actor.Valid || !actor.DrawObject.Valid) - return false; - - return _changeCustomize(actor.DrawObject.Pointer, customize.Data, 1); - } -} - -public partial class Interop -{ - private delegate void ChangeJobDelegate(IntPtr data, uint job); - - [Signature(Sigs.ChangeJob, DetourName = nameof(ChangeJobDetour))] - private readonly Hook _changeJobHook = null!; - - private void ChangeJobDetour(IntPtr data, uint job) - { - _changeJobHook.Original(data, job); - JobChanged?.Invoke(data - Offsets.Character.ClassJobContainer, _jobService.Jobs[(byte)job]); - } - - public event Action? JobChanged; -} - public unsafe partial class RedrawManager : IDisposable { private readonly ItemManager _items; private readonly ActorService _actors; private readonly FixedDesignManager _fixedDesignManager; private readonly ActiveDesign.Manager _stateManager; + private readonly PenumbraAttach _penumbra; + private readonly WeaponService _weapons; - public RedrawManager(FixedDesignManager fixedDesignManager, ActiveDesign.Manager stateManager, ItemManager items, ActorService actors) + public RedrawManager(FixedDesignManager fixedDesignManager, ActiveDesign.Manager stateManager, ItemManager items, ActorService actors, + PenumbraAttach penumbra, WeaponService weapons) { SignatureHelper.Initialise(this); _fixedDesignManager = fixedDesignManager; _stateManager = stateManager; _items = items; _actors = actors; - _flagSlotForUpdateHook.Enable(); - _loadWeaponHook.Enable(); + _penumbra = penumbra; + _weapons = weapons; + + _penumbra.CreatingCharacterBase += OnCharacterRedraw; + _penumbra.CreatedCharacterBase += OnCharacterRedrawFinished; } public void Dispose() { - _flagSlotForUpdateHook.Dispose(); - _loadWeaponHook.Dispose(); } private void OnCharacterRedraw(Actor actor, uint* modelId, Customize customize, CharacterEquip equip) @@ -230,21 +51,21 @@ public unsafe partial class RedrawManager : IDisposable // Check if we have a current design in use, or if not if the actor has a fixed design. var identifier = actor.GetIdentifier(_actors.AwaitedService); - if (!(_stateManager.TryGetValue(identifier, out var save) || _fixedDesignManager.TryGetDesign(identifier, out var save2))) + if (!_stateManager.TryGetValue(identifier, out var save)) 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()); + 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(); + var saveEquip = save!.Equipment; foreach (var slot in EquipSlotExtensions.EqdpSlots) { (_, equip[slot]) = diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs new file mode 100644 index 0000000..0b34f58 --- /dev/null +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop; + +public unsafe class UpdateSlotService : IDisposable +{ + public UpdateSlotService() + { + SignatureHelper.Initialise(this); + _flagSlotForUpdateHook.Enable(); + } + + public void Dispose() + => _flagSlotForUpdateHook.Dispose(); + + private delegate ulong FlagSlotForUpdateDelegateIntern(nint drawObject, uint slot, CharacterArmor* data); + public delegate void FlagSlotForUpdateDelegate(DrawObject drawObject, EquipSlot slot, ref CharacterArmor item); + + // This gets called when one of the ten equip items of an existing draw object gets changed. + [Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))] + private readonly Hook _flagSlotForUpdateHook = null!; + + public event FlagSlotForUpdateDelegate? EquipUpdate; + + public ulong FlagSlotForUpdateInterop(DrawObject drawObject, EquipSlot slot, CharacterArmor armor) + => _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); + + public void UpdateSlot(DrawObject drawObject, EquipSlot slot, CharacterArmor data) + { + InvokeFlagSlotEvent(drawObject, slot, ref data); + FlagSlotForUpdateInterop(drawObject, slot, data); + } + + public void UpdateStain(DrawObject drawObject, EquipSlot slot, StainId stain) + { + var armor = drawObject.Equip[slot] with { Stain = stain }; + UpdateSlot(drawObject, slot, armor); + } + + private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) + { + var slot = slotIdx.ToEquipSlot(); + InvokeFlagSlotEvent(drawObject, slot, ref *data); + return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data); + } + + private void InvokeFlagSlotEvent(DrawObject drawObject, EquipSlot slot, ref CharacterArmor armor) + { + if (EquipUpdate == null) + { + Glamourer.Log.Excessive( + $"{slot} updated on 0x{drawObject.Address:X} to {armor.Set.Value}-{armor.Variant} with stain {armor.Stain.Value}."); + return; + } + + var iv = armor; + foreach (var del in EquipUpdate.GetInvocationList().OfType()) + { + try + { + del(drawObject, slot, ref armor); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not invoke {nameof(EquipUpdate)} Subscriber:\n{ex}"); + } + } + + Glamourer.Log.Excessive( + $"{slot} updated on 0x{drawObject.Address:X} to {armor.Set.Value}-{armor.Variant} with stain {armor.Stain.Value}, initial armor was {iv.Set.Value}-{iv.Variant} with stain {iv.Stain.Value}."); + } +} diff --git a/Glamourer/Interop/VisorService.cs b/Glamourer/Interop/VisorService.cs new file mode 100644 index 0000000..250ee9f --- /dev/null +++ b/Glamourer/Interop/VisorService.cs @@ -0,0 +1,78 @@ +using System; +using System.Linq; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop; + +public class VisorService : IDisposable +{ + public VisorService() + { + SignatureHelper.Initialise(this); + _setupVisorHook.Enable(); + } + + public void Dispose() + => _setupVisorHook.Dispose(); + + public static unsafe bool GetVisorState(nint humanPtr) + { + if (humanPtr == IntPtr.Zero) + return false; + + var data = (Human*)humanPtr; + var flags = &data->CharacterBase.UnkFlags_01; + return (*flags & Offsets.DrawObjectVisorStateFlag) != 0; + } + + public unsafe void SetVisorState(nint humanPtr, bool on) + { + if (humanPtr == IntPtr.Zero) + return; + + var data = (Human*)humanPtr; + _setupVisorHook.Original(humanPtr, (ushort) data->HeadSetID, on); + } + + private delegate void UpdateVisorDelegateInternal(nint humanPtr, ushort modelId, bool on); + public delegate void UpdateVisorDelegate(DrawObject human, SetId modelId, ref bool on); + + [Signature(Penumbra.GameData.Sigs.SetupVisor, DetourName = nameof(SetupVisorDetour))] + private readonly Hook _setupVisorHook = null!; + + public event UpdateVisorDelegate? VisorUpdate; + + private void SetupVisorDetour(nint humanPtr, ushort modelId, bool on) + { + InvokeVisorEvent(humanPtr, modelId, ref on); + _setupVisorHook.Original(humanPtr, modelId, on); + } + + private void InvokeVisorEvent(DrawObject drawObject, SetId modelId, ref bool on) + { + if (VisorUpdate == null) + { + Glamourer.Log.Excessive($"Visor setup on 0x{drawObject.Address:X} with {modelId.Value}, setting to {on}."); + return; + } + + var initialValue = on; + foreach (var del in VisorUpdate.GetInvocationList().OfType()) + { + try + { + del(drawObject, modelId, ref on); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Could not invoke {nameof(VisorUpdate)} Subscriber:\n{ex}"); + } + } + + Glamourer.Log.Excessive( + $"Visor setup on 0x{drawObject.Address:X} with {modelId.Value}, setting to {on}, initial call was {initialValue}."); + } +} diff --git a/Glamourer/Interop/WeaponService.cs b/Glamourer/Interop/WeaponService.cs new file mode 100644 index 0000000..15024b8 --- /dev/null +++ b/Glamourer/Interop/WeaponService.cs @@ -0,0 +1,120 @@ +using System; +using System.Runtime.InteropServices; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop; + +public unsafe class WeaponService : IDisposable +{ + public WeaponService() + { + SignatureHelper.Initialise(this); + _loadWeaponHook.Enable(); + } + + public void Dispose() + { + _loadWeaponHook.Dispose(); + } + + public static readonly int CharacterWeaponOffset = (int)Marshal.OffsetOf("DrawData"); + + public delegate void LoadWeaponDelegate(nint 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(Penumbra.GameData.Sigs.WeaponReload, DetourName = nameof(LoadWeaponDetour))] + private readonly Hook _loadWeaponHook = null!; + + private void LoadWeaponDetour(nint 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(_actors.AwaitedService); + // if (_fixedDesignManager.TryGetDesign(identifier, out var save)) + // { + // PluginLog.Information($"Loaded weapon from fixed design for {identifier}."); + // weapon = slot switch + // { + // 0 => save.WeaponMain.Model.Value, + // 1 => save.WeaponOff.Model.Value, + // _ => weapon, + // }; + // } + // else if (redrawOnEquality == 1 && _stateManager.TryGetValue(identifier, out var save2)) + // { + // PluginLog.Information($"Loaded weapon from current design for {identifier}."); + // //switch (slot) + // //{ + // // case 0: + // // save2.MainHand = new CharacterWeapon(weapon); + // // break; + // // case 1: + // // save2.Data.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, weapon, redrawOnEquality, unk2, skipGameObject, unk4); + Glamourer.Log.Excessive($"Weapon reloaded for {(Actor)(characterOffset - CharacterWeaponOffset)} with attributes {slot} {weapon:X14}, {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, 1, 0, 1, 0); + LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, off.Value, 1, 0, 1, 0); + } + + public void LoadStain(Actor character, EquipSlot slot, StainId stain) + { + var weapon = slot == EquipSlot.OffHand ? character.OffHand : character.MainHand; + weapon.Stain = stain; + LoadWeapon(character, slot, weapon); + } +} diff --git a/Glamourer/Services/JobService.cs b/Glamourer/Services/JobService.cs deleted file mode 100644 index 9b91adb..0000000 --- a/Glamourer/Services/JobService.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using Dalamud.Data; - -namespace Glamourer.Services; - -public class JobService -{ - public readonly IReadOnlyDictionary Jobs; - public readonly IReadOnlyDictionary JobGroups; - - public JobService(DataManager gameData) - { - Jobs = GameData.Jobs(gameData); - JobGroups = GameData.JobGroups(gameData); - } -} diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index e6d28bc..0baacef 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -53,18 +53,22 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton(); private static IServiceCollection AddInterop(this IServiceCollection services) - => services.AddSingleton() + => services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton(); private static IServiceCollection AddDesigns(this IServiceCollection services) - => services.AddSingleton() + => services.AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddInterface(this IServiceCollection services) => services.AddSingleton() diff --git a/Glamourer/State/ActiveDesign.Manager.cs b/Glamourer/State/ActiveDesign.Manager.cs index b273559..4946e30 100644 --- a/Glamourer/State/ActiveDesign.Manager.cs +++ b/Glamourer/State/ActiveDesign.Manager.cs @@ -14,28 +14,44 @@ using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using CustomizeData = Penumbra.GameData.Structs.CustomizeData; +using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; namespace Glamourer.State; public sealed partial class ActiveDesign { - public partial class Manager : IReadOnlyDictionary + [Flags] + public enum ChangeType { - private readonly ActorService _actors; - private readonly ObjectManager _objects; - private readonly Interop.Interop _interop; - private readonly PenumbraAttach _penumbra; - private readonly ItemManager _items; + Default = 0x00, + Changed = 0x01, + Fixed = 0x02, + } + + public class Manager : IReadOnlyDictionary + { + private readonly ActorService _actors; + private readonly ObjectManager _objects; + private readonly PenumbraAttach _penumbra; + private readonly ItemManager _items; + private readonly VisorService _visor; + private readonly ChangeCustomizeService _customize; + private readonly UpdateSlotService _updateSlot; + private readonly WeaponService _weaponService; private readonly Dictionary _characterSaves = new(); - public Manager(ActorService actors, ObjectManager objects, Interop.Interop interop, PenumbraAttach penumbra, ItemManager items) + public Manager(ActorService actors, ObjectManager objects, PenumbraAttach penumbra, ItemManager items, VisorService visor, + ChangeCustomizeService customize, UpdateSlotService updateSlot, WeaponService weaponService) { - _actors = actors; - _objects = objects; - _interop = interop; - _penumbra = penumbra; - _items = items; + _actors = actors; + _objects = objects; + _penumbra = penumbra; + _items = items; + _visor = visor; + _customize = customize; + _updateSlot = updateSlot; + _weaponService = weaponService; } public IEnumerator> GetEnumerator() @@ -99,15 +115,25 @@ public sealed partial class ActiveDesign return; if (from.DoApplyEquip(EquipSlot.MainHand)) - ChangeMainHand(to, from.MainHand, fromFixed); + ChangeMainHand(to, from.MainHandId, fromFixed); + if (from.DoApplyStain(EquipSlot.MainHand)) + ChangeStain(to, EquipSlot.MainHand, from.WeaponMain.Stain, fromFixed); if (from.DoApplyEquip(EquipSlot.OffHand)) - ChangeOffHand(to, from.OffHand, fromFixed); + ChangeOffHand(to, from.OffHandId, fromFixed); + if (from.DoApplyStain(EquipSlot.OffHand)) + ChangeStain(to, EquipSlot.OffHand, from.WeaponOff.Stain, fromFixed); - foreach (var slot in EquipSlotExtensions.EqdpSlots.Where(from.DoApplyEquip)) - ChangeEquipment(to, slot, from.Armor(slot), fromFixed); + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var armor = from.Armor(slot); + if (from.DoApplyEquip(slot)) + ChangeEquipment(to, slot, armor, fromFixed); + if (from.DoApplyStain(slot)) + ChangeStain(to, slot, armor.Stain, fromFixed); + } - ChangeCustomize(to, from.ApplyCustomize, *from.Customize().Data, fromFixed); + ChangeCustomize(to, from.ApplyCustomize, *from.Customize.Data, fromFixed); if (from.Wetness.Enabled) SetWetness(to, from.Wetness.ForcedValue, fromFixed); @@ -177,7 +203,7 @@ public sealed partial class ActiveDesign if (redraw) _penumbra.RedrawObject(obj, RedrawType.Redraw); else - _interop.UpdateCustomize(obj, design.CharacterData.CustomizeData); + _customize.UpdateCustomize(obj, design.ModelData.CustomizeData); } } @@ -205,7 +231,7 @@ public sealed partial class ActiveDesign return; foreach (var obj in data.Objects) - _interop.UpdateSlot(obj.DrawObject, slot, item); + _updateSlot.UpdateSlot(obj.DrawObject, slot, item); } public void ChangeEquipment(ActiveDesign design, EquipSlot slot, Item item, bool fromFixed) @@ -228,16 +254,20 @@ public sealed partial class ActiveDesign return; foreach (var obj in data.Objects) - _interop.UpdateSlot(obj.DrawObject, slot, item.Model); + _updateSlot.UpdateSlot(obj.DrawObject, slot, item.Model); } public void ChangeStain(ActiveDesign design, EquipSlot slot, StainId stain, bool fromFixed) { var flag = slot.ToStainFlag(); design.SetStain(slot, stain); - var current = design.Armor(slot); - var initial = design._initialData.Equipment[slot]; - if (current.Stain.Value != initial.Stain.Value) + var (current, initial, weapon) = slot switch + { + EquipSlot.MainHand => (design.WeaponMain.Stain, design._initialData.MainHand.Stain, true), + EquipSlot.OffHand => (design.WeaponOff.Stain, design._initialData.OffHand.Stain, true), + _ => (design.Armor(slot).Stain, design._initialData.Equipment[slot].Stain, false), + }; + if (current.Value != initial.Value) design.ChangedEquip |= flag; else design.ChangedEquip &= ~flag; @@ -251,7 +281,12 @@ public sealed partial class ActiveDesign return; foreach (var obj in data.Objects) - _interop.UpdateStain(obj.DrawObject, slot, stain); + { + if (weapon) + _weaponService.LoadStain(obj, EquipSlot.MainHand, stain); + else + _updateSlot.UpdateStain(obj.DrawObject, slot, stain); + } } public void ChangeVisor(ActiveDesign design, bool on, bool fromFixed) @@ -266,7 +301,7 @@ public sealed partial class ActiveDesign return; foreach (var obj in data.Objects) - Interop.Interop.SetVisorState(obj.DrawObject, on); + _visor.SetVisorState(obj.DrawObject, on); } } } diff --git a/Glamourer/State/ActiveDesignData.cs b/Glamourer/State/ActiveDesign.cs similarity index 88% rename from Glamourer/State/ActiveDesignData.cs rename to Glamourer/State/ActiveDesign.cs index 4346b1a..f488738 100644 --- a/Glamourer/State/ActiveDesignData.cs +++ b/Glamourer/State/ActiveDesign.cs @@ -7,11 +7,11 @@ using Penumbra.GameData.Enums; namespace Glamourer.State; -public sealed partial class ActiveDesign : DesignBase +public sealed partial class ActiveDesign : DesignData { public readonly ActorIdentifier Identifier; - private CharacterData _initialData = new(); + private ModelData _initialData = new(); public CustomizeFlag ChangedCustomize { get; private set; } = 0; public CustomizeFlag FixedCustomize { get; private set; } = 0; @@ -19,6 +19,7 @@ public sealed partial class ActiveDesign : DesignBase public EquipFlag ChangedEquip { get; private set; } = 0; public EquipFlag FixedEquip { get; private set; } = 0; + public bool IsHatVisible { get; private set; } = false; public bool IsWeaponVisible { get; private set; } = false; public bool IsVisorToggled { get; private set; } = false; @@ -73,7 +74,7 @@ public sealed partial class ActiveDesign : DesignBase if (!_initialData.Customize.Equals(actor.Customize)) { _initialData.Customize.Load(actor.Customize); - Customize().Load(actor.Customize); + Customize.Load(actor.Customize); } var initialEquip = _initialData.Equipment; @@ -103,13 +104,13 @@ public sealed partial class ActiveDesign : DesignBase SetStain(EquipSlot.OffHand, actor.OffHand.Stain); } - var visor = Interop.Interop.GetVisorState(actor.DrawObject); + var visor = VisorService.GetVisorState(actor.DrawObject); if (IsVisorToggled != visor) IsVisorToggled = visor; } public string CreateOldBase64() - => CreateOldBase64(in CharacterData, EquipFlagExtensions.All, CustomizeFlagExtensions.All, IsWet, IsHatVisible, true, + => DesignBase64Migration.CreateOldBase64(in ModelData, EquipFlagExtensions.All, CustomizeFlagExtensions.All, IsWet, IsHatVisible, true, IsVisorToggled, true, IsWeaponVisible, true, false, 1f); } diff --git a/Glamourer/State/FixedDesignManager.cs b/Glamourer/State/FixedDesignManager.cs index 1d7c6e8..d7706e4 100644 --- a/Glamourer/State/FixedDesignManager.cs +++ b/Glamourer/State/FixedDesignManager.cs @@ -1,15 +1,29 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent; using Glamourer.Designs; using Glamourer.Interop; +using Glamourer.Structs; using Penumbra.GameData.Actors; namespace Glamourer.State; public class FixedDesignManager { - public bool TryGetDesign(ActorIdentifier actor, [NotNullWhen(true)] out Design? save) + public class FixedDesign { - save = null; - return false; + public Design Design; + public byte? JobCondition; + public ushort? TerritoryCondition; + + public bool Applies(byte job, ushort territoryType) + => (!JobCondition.HasValue || JobCondition.Value == job) + && (!TerritoryCondition.HasValue || TerritoryCondition.Value == territoryType); + } + + public IReadOnlyList GetDesigns(ActorIdentifier actor) + { + return Array.Empty(); } }