From 2d6fd6015d04b91161d31ae40de0258f59118664 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Jun 2023 17:57:40 +0200 Subject: [PATCH] . --- Glamourer.GameData/Offsets.cs | 16 - Glamourer.sln | 8 +- Glamourer/Events/UpdatedSlot.cs | 35 ++ Glamourer/Events/VisorStateChanged.cs | 32 ++ Glamourer/Glamourer.cs | 163 +----- Glamourer/Glamourer.csproj | 5 + Glamourer/Gui/GlamourerWindowSystem.cs | 7 +- Glamourer/Gui/MainWindow.cs | 39 ++ Glamourer/Gui/Tabs/DebugTab.cs | 491 ++++++++++++++++++ Glamourer/Interop/ChangeCustomizeService.cs | 18 +- Glamourer/Interop/Penumbra/PenumbraService.cs | 142 +++++ Glamourer/Interop/Structs/Actor.cs | 97 ++++ Glamourer/Interop/Structs/ActorData.cs | 26 + Glamourer/Interop/Structs/Model.cs | 92 ++++ Glamourer/Interop/UpdateSlotService.cs | 70 +-- Glamourer/Interop/VisorService.cs | 95 ++-- Glamourer/Interop/WeaponService.cs | 83 +-- Glamourer/Services/CommandService.cs | 11 +- Glamourer/Services/DalamudServices.cs | 49 ++ Glamourer/Services/FilenameService.cs | 5 - Glamourer/Services/ItemManager.cs | 12 +- Glamourer/Services/ServiceManager.cs | 56 +- Glamourer/Services/ServiceWrapper.cs | 10 +- .../Api/GlamourerIpc.cs | 0 .../Api/PenumbraAttach.cs | 0 {Glamourer => GlamourerOld}/Configuration.cs | 0 {Glamourer => GlamourerOld}/Dalamud.cs | 0 {Glamourer => GlamourerOld}/Designs/Design.cs | 0 .../Designs/DesignData.cs | 0 .../Designs/DesignFileSystem.cs | 0 .../Designs/DesignManager.cs | 0 .../Designs/EquipFlag.cs | 0 .../Designs/ModelData.cs | 0 .../Designs/Structs.cs | 0 .../DrawObjectManager.cs | 0 .../Fixed/FixedCondition.cs | 0 .../Fixed/FixedDesigns.cs | 0 GlamourerOld/Glamourer.cs | 190 +++++++ GlamourerOld/GlamourerOld.csproj | 126 +++++ {Glamourer => GlamourerOld}/Gui/ActorDebug.cs | 0 .../CustomizationDrawer.Color.cs | 0 .../CustomizationDrawer.GenderRace.cs | 0 .../Customization/CustomizationDrawer.Icon.cs | 0 .../CustomizationDrawer.Simple.cs | 0 .../Gui/Customization/CustomizationDrawer.cs | 0 .../Gui/Designs/DesignFileSystemSelector.cs | 0 .../Gui/Designs/InterfaceDesigns.cs | 0 .../Gui/Equipment/EquipmentDrawer.cs | 0 .../Gui/Equipment/ItemCombo.cs | 0 .../Gui/Equipment/WeaponCombo.cs | 0 GlamourerOld/Gui/GlamourerWindowSystem.cs | 30 ++ .../Gui/Interface.Actors.cs | 0 .../Gui/Interface.DebugDataTab.cs | 0 .../Gui/Interface.DebugStateTab.cs | 0 .../Gui/Interface.DesignTab.cs | 0 .../Gui/Interface.SettingsTab.cs | 0 .../Gui/Interface.State.cs | 0 {Glamourer => GlamourerOld}/Gui/Interface.cs | 0 .../Gui/InterfaceActorPanel.cs | 0 .../Gui/InterfaceEquipment.cs | 0 .../Gui/InterfaceFixedDesigns.cs | 0 .../Gui/InterfaceHelpers.cs | 0 .../Gui/InterfaceInitialization.cs | 0 .../Gui/InterfaceMiscellaneous.cs | 0 .../Gui/InterfaceRevertables.cs | 0 {Glamourer => GlamourerOld}/Interop/Actor.cs | 0 .../Interop/ChangeCustomizeService.cs | 24 + .../Interop/DrawObject.cs | 0 .../Interop/IDesignable.cs | 0 .../Interop/JobService.cs | 0 .../Interop/ObjectManager.cs | 0 .../Interop/RedrawManager.cs | 0 GlamourerOld/Interop/UpdateSlotService.cs | 77 +++ GlamourerOld/Interop/VisorService.cs | 78 +++ GlamourerOld/Interop/WeaponService.cs | 120 +++++ GlamourerOld/LegacyTattoo.raw | Bin 0 -> 147456 bytes GlamourerOld/Services/BackupService.cs | 30 ++ GlamourerOld/Services/CommandService.cs | 36 ++ GlamourerOld/Services/FilenameService.cs | 40 ++ GlamourerOld/Services/ItemManager.cs | 189 +++++++ .../Services/SaveService.cs | 0 GlamourerOld/Services/ServiceManager.cs | 80 +++ GlamourerOld/Services/ServiceWrapper.cs | 105 ++++ .../State/ActiveDesign.Manager.cs | 0 .../State/ActiveDesign.cs | 0 .../State/FixedDesignManager.cs | 0 .../Util/CharacterExtensions.cs | 0 .../Util/CustomizeExtensions.cs | 0 88 files changed, 2304 insertions(+), 383 deletions(-) create mode 100644 Glamourer/Events/UpdatedSlot.cs create mode 100644 Glamourer/Events/VisorStateChanged.cs create mode 100644 Glamourer/Gui/MainWindow.cs create mode 100644 Glamourer/Gui/Tabs/DebugTab.cs create mode 100644 Glamourer/Interop/Penumbra/PenumbraService.cs create mode 100644 Glamourer/Interop/Structs/Actor.cs create mode 100644 Glamourer/Interop/Structs/ActorData.cs create mode 100644 Glamourer/Interop/Structs/Model.cs create mode 100644 Glamourer/Services/DalamudServices.cs rename {Glamourer => GlamourerOld}/Api/GlamourerIpc.cs (100%) rename {Glamourer => GlamourerOld}/Api/PenumbraAttach.cs (100%) rename {Glamourer => GlamourerOld}/Configuration.cs (100%) rename {Glamourer => GlamourerOld}/Dalamud.cs (100%) rename {Glamourer => GlamourerOld}/Designs/Design.cs (100%) rename {Glamourer => GlamourerOld}/Designs/DesignData.cs (100%) rename {Glamourer => GlamourerOld}/Designs/DesignFileSystem.cs (100%) rename {Glamourer => GlamourerOld}/Designs/DesignManager.cs (100%) rename {Glamourer => GlamourerOld}/Designs/EquipFlag.cs (100%) rename {Glamourer => GlamourerOld}/Designs/ModelData.cs (100%) rename {Glamourer => GlamourerOld}/Designs/Structs.cs (100%) rename {Glamourer => GlamourerOld}/DrawObjectManager.cs (100%) rename {Glamourer => GlamourerOld}/Fixed/FixedCondition.cs (100%) rename {Glamourer => GlamourerOld}/Fixed/FixedDesigns.cs (100%) create mode 100644 GlamourerOld/Glamourer.cs create mode 100644 GlamourerOld/GlamourerOld.csproj rename {Glamourer => GlamourerOld}/Gui/ActorDebug.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Customization/CustomizationDrawer.Color.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Customization/CustomizationDrawer.GenderRace.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Customization/CustomizationDrawer.Icon.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Customization/CustomizationDrawer.Simple.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Customization/CustomizationDrawer.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Designs/DesignFileSystemSelector.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Designs/InterfaceDesigns.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Equipment/EquipmentDrawer.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Equipment/ItemCombo.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Equipment/WeaponCombo.cs (100%) create mode 100644 GlamourerOld/Gui/GlamourerWindowSystem.cs rename {Glamourer => GlamourerOld}/Gui/Interface.Actors.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Interface.DebugDataTab.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Interface.DebugStateTab.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Interface.DesignTab.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Interface.SettingsTab.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Interface.State.cs (100%) rename {Glamourer => GlamourerOld}/Gui/Interface.cs (100%) rename {Glamourer => GlamourerOld}/Gui/InterfaceActorPanel.cs (100%) rename {Glamourer => GlamourerOld}/Gui/InterfaceEquipment.cs (100%) rename {Glamourer => GlamourerOld}/Gui/InterfaceFixedDesigns.cs (100%) rename {Glamourer => GlamourerOld}/Gui/InterfaceHelpers.cs (100%) rename {Glamourer => GlamourerOld}/Gui/InterfaceInitialization.cs (100%) rename {Glamourer => GlamourerOld}/Gui/InterfaceMiscellaneous.cs (100%) rename {Glamourer => GlamourerOld}/Gui/InterfaceRevertables.cs (100%) rename {Glamourer => GlamourerOld}/Interop/Actor.cs (100%) create mode 100644 GlamourerOld/Interop/ChangeCustomizeService.cs rename {Glamourer => GlamourerOld}/Interop/DrawObject.cs (100%) rename {Glamourer => GlamourerOld}/Interop/IDesignable.cs (100%) rename {Glamourer => GlamourerOld}/Interop/JobService.cs (100%) rename {Glamourer => GlamourerOld}/Interop/ObjectManager.cs (100%) rename {Glamourer => GlamourerOld}/Interop/RedrawManager.cs (100%) create mode 100644 GlamourerOld/Interop/UpdateSlotService.cs create mode 100644 GlamourerOld/Interop/VisorService.cs create mode 100644 GlamourerOld/Interop/WeaponService.cs create mode 100644 GlamourerOld/LegacyTattoo.raw create mode 100644 GlamourerOld/Services/BackupService.cs create mode 100644 GlamourerOld/Services/CommandService.cs create mode 100644 GlamourerOld/Services/FilenameService.cs create mode 100644 GlamourerOld/Services/ItemManager.cs rename {Glamourer => GlamourerOld}/Services/SaveService.cs (100%) create mode 100644 GlamourerOld/Services/ServiceManager.cs create mode 100644 GlamourerOld/Services/ServiceWrapper.cs rename {Glamourer => GlamourerOld}/State/ActiveDesign.Manager.cs (100%) rename {Glamourer => GlamourerOld}/State/ActiveDesign.cs (100%) rename {Glamourer => GlamourerOld}/State/FixedDesignManager.cs (100%) rename {Glamourer => GlamourerOld}/Util/CharacterExtensions.cs (100%) rename {Glamourer => GlamourerOld}/Util/CustomizeExtensions.cs (100%) diff --git a/Glamourer.GameData/Offsets.cs b/Glamourer.GameData/Offsets.cs index e652860..347ced8 100644 --- a/Glamourer.GameData/Offsets.cs +++ b/Glamourer.GameData/Offsets.cs @@ -5,22 +5,6 @@ public static class Offsets public static class Character { public const int ClassJobContainer = 0x1A8; - - public const int Wetness = 0x1ADA; - public const int HatVisible = 0x84E; - public const int VisorToggled = 0x84F; - public const int WeaponHidden1 = 0x84F; - public const int WeaponHidden2 = 0x72C; - public const int Alpha = 0x19E0; - - public static class Flags - { - public const byte IsHatHidden = 0x01; - public const byte IsVisorToggled = 0x08; - public const byte IsWet = 0x80; - public const byte IsWeaponHidden1 = 0x01; - public const byte IsWeaponHidden2 = 0x02; - } } public const byte DrawObjectVisorStateFlag = 0x40; diff --git a/Glamourer.sln b/Glamourer.sln index e573beb..7509c7a 100644 --- a/Glamourer.sln +++ b/Glamourer.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.2.32210.308 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer", "Glamourer\Glamourer.csproj", "{A5439F6B-83C1-4078-9371-354A147FF554}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GlamourerOld", "GlamourerOld\GlamourerOld.csproj", "{A5439F6B-83C1-4078-9371-354A147FF554}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{383AEE76-D423-431C-893A-7AB3DEA13630}" ProjectSection(SolutionItems) = preProject @@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "..\Pen EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "..\Penumbra\OtterGui\OtterGui.csproj", "{6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer", "Glamourer\Glamourer.csproj", "{01EB903D-871F-4285-A8CF-6486561D5B5B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Release|Any CPU.Build.0 = Release|Any CPU + {01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Glamourer/Events/UpdatedSlot.cs b/Glamourer/Events/UpdatedSlot.cs new file mode 100644 index 0000000..b967416 --- /dev/null +++ b/Glamourer/Events/UpdatedSlot.cs @@ -0,0 +1,35 @@ +using System; +using Glamourer.Interop.Structs; +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Events; + +/// +/// Triggered when a model flags an equipment slot for an update. +/// +/// Parameter is the model with a flagged slot. +/// Parameter is the equipment slot changed. +/// Parameter is the model values to change the equipment piece to. +/// Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. +/// +/// +public sealed class UpdatedSlot : EventWrapper, Ref>, UpdatedSlot.Priority> +{ + public enum Priority + { } + + public UpdatedSlot() + : base(nameof(UpdatedSlot)) + { } + + public void Invoke(Model model, EquipSlot slot, ref CharacterArmor armor, ref ulong returnValue) + { + var value = new Ref(armor); + var @return = new Ref(returnValue); + Invoke(this, model, slot, value, @return); + armor = value; + returnValue = @return; + } +} diff --git a/Glamourer/Events/VisorStateChanged.cs b/Glamourer/Events/VisorStateChanged.cs new file mode 100644 index 0000000..9872b9b --- /dev/null +++ b/Glamourer/Events/VisorStateChanged.cs @@ -0,0 +1,32 @@ +using System; +using Glamourer.Interop.Structs; +using OtterGui.Classes; + +namespace Glamourer.Events; + +/// +/// Triggered when the state of a visor for any draw object is changed. +/// +/// Parameter is the model with a changed visor state. +/// Parameter is the new state. +/// Parameter is whether to call the original function. +/// +/// +public sealed class VisorStateChanged : EventWrapper, Ref>, VisorStateChanged.Priority> +{ + public enum Priority + { } + + public VisorStateChanged() + : base(nameof(VisorStateChanged)) + { } + + public void Invoke(Model model, ref bool state, ref bool callOriginal) + { + var value = new Ref(state); + var original = new Ref(callOriginal); + Invoke(this, model, value, original); + state = value; + callOriginal = original; + } +} diff --git a/Glamourer/Glamourer.cs b/Glamourer/Glamourer.cs index f3f09c8..9e9b4f4 100644 --- a/Glamourer/Glamourer.cs +++ b/Glamourer/Glamourer.cs @@ -9,7 +9,7 @@ using OtterGui.Log; namespace Glamourer; -public partial class Glamourer : IDalamudPlugin +public class Item : IDalamudPlugin { public string Name => "Glamourer"; @@ -20,26 +20,22 @@ public partial class Glamourer : IDalamudPlugin Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion ?? "Unknown"; - public static readonly Logger Log = new(); - public static ChatService ChatService { get; private set; } = null!; - private readonly ServiceProvider _services; + public static readonly Logger Log = new(); + public static ChatService Chat { get; private set; } = null!; - public Glamourer(DalamudPluginInterface pluginInterface) + + private readonly ServiceProvider _services; + + public Item(DalamudPluginInterface pluginInterface) { try { - _services = ServiceManager.CreateProvider(pluginInterface, Log); - ChatService = _services.GetRequiredService(); - _services.GetRequiredService(); - _services.GetRequiredService(); - _services.GetRequiredService(); - _services.GetRequiredService(); - _services.GetRequiredService(); - _services.GetRequiredService(); - _services.GetRequiredService(); - _services.GetRequiredService(); - _services.GetRequiredService(); - _services.GetRequiredService(); + _services = ServiceManager.CreateProvider(pluginInterface, Log); + Chat = _services.GetRequiredService(); + _services.GetRequiredService(); // call backup service. + _services.GetRequiredService(); // initialize ui. + _services.GetRequiredService(); // initialize commands. + _services.GetRequiredService(); } catch { @@ -53,137 +49,4 @@ public partial class Glamourer : IDalamudPlugin { _services?.Dispose(); } - - //private static GameObject? GetPlayer(string name) - //{ - // var lowerName = name.ToLowerInvariant(); - // return lowerName switch - // { - // "" => null, - // "" => Dalamud.Objects[Interface.GPoseObjectId] ?? Dalamud.ClientState.LocalPlayer, - // "self" => Dalamud.Objects[Interface.GPoseObjectId] ?? Dalamud.ClientState.LocalPlayer, - // "" => Dalamud.Targets.Target, - // "target" => Dalamud.Targets.Target, - // "" => Dalamud.Targets.FocusTarget, - // "focus" => Dalamud.Targets.FocusTarget, - // "" => Dalamud.Targets.MouseOverTarget, - // "mouseover" => Dalamud.Targets.MouseOverTarget, - // _ => Dalamud.Objects.LastOrDefault( - // a => string.Equals(a.Name.ToString(), lowerName, StringComparison.InvariantCultureIgnoreCase)), - // }; - //} - // - //public void CopyToClipboard(Character player) - //{ - // var save = new CharacterSave(); - // save.LoadCharacter(player); - // ImGui.SetClipboardText(save.ToBase64()); - //} - // - //public void ApplyCommand(Character player, string target) - //{ - // CharacterSave? save = null; - // if (target.ToLowerInvariant() == "clipboard") - // try - // { - // save = CharacterSave.FromString(ImGui.GetClipboardText()); - // } - // catch (Exception) - // { - // Dalamud.Chat.PrintError("Clipboard does not contain a valid customization string."); - // } - // else if (!Designs.FileSystem.Find(target, out var child) || child is not Design d) - // Dalamud.Chat.PrintError("The given path to a saved design does not exist or does not point to a design."); - // else - // save = d.Data; - // - // save?.Apply(player); - // Penumbra.UpdateCharacters(player); - //} - // - //public void SaveCommand(Character player, string path) - //{ - // var save = new CharacterSave(); - // save.LoadCharacter(player); - // try - // { - // var (folder, name) = Designs.FileSystem.CreateAllFolders(path); - // var design = new Design(folder, name) { Data = save }; - // folder.FindOrAddChild(design); - // Designs.Designs.Add(design.FullName(), design.Data); - // Designs.SaveToFile(); - // } - // catch (Exception e) - // { - // Dalamud.Chat.PrintError("Could not save file:"); - // Dalamud.Chat.PrintError($" {e.Message}"); - // } - //} - // - public void OnGlamour(string command, string arguments) - { - //static void PrintHelp() - //{ - // Dalamud.Chat.Print("Usage:"); - // Dalamud.Chat.Print($" {HelpString}"); - //} - - //arguments = arguments.Trim(); - //if (!arguments.Any()) - //{ - // PrintHelp(); - // return; - //} - // - //var split = arguments.Split(new[] - //{ - // ',', - //}, 3, StringSplitOptions.RemoveEmptyEntries); - // - //if (split.Length < 2) - //{ - // PrintHelp(); - // return; - //} - // - //var player = GetPlayer(split[1]) as Character; - //if (player == null) - //{ - // Dalamud.Chat.Print($"Could not find object for {split[1]} or it was not a Character."); - // return; - //} - // - //switch (split[0].ToLowerInvariant()) - //{ - // case "copy": - // CopyToClipboard(player); - // return; - // case "apply": - // { - // if (split.Length < 3) - // { - // Dalamud.Chat.Print("Applying requires a name for the save to be applied or 'clipboard'."); - // return; - // } - // - // ApplyCommand(player, split[2]); - // - // return; - // } - // case "save": - // { - // if (split.Length < 3) - // { - // Dalamud.Chat.Print("Saving requires a name for the save."); - // return; - // } - // - // SaveCommand(player, split[2]); - // return; - // } - // default: - // PrintHelp(); - // return; - //} - } } diff --git a/Glamourer/Glamourer.csproj b/Glamourer/Glamourer.csproj index 5532cb6..cea86c5 100644 --- a/Glamourer/Glamourer.csproj +++ b/Glamourer/Glamourer.csproj @@ -83,6 +83,7 @@ + @@ -119,6 +120,10 @@ PreserveNewest + + + + diff --git a/Glamourer/Gui/GlamourerWindowSystem.cs b/Glamourer/Gui/GlamourerWindowSystem.cs index dbe1c8c..0413b50 100644 --- a/Glamourer/Gui/GlamourerWindowSystem.cs +++ b/Glamourer/Gui/GlamourerWindowSystem.cs @@ -8,9 +8,9 @@ public class GlamourerWindowSystem : IDisposable { private readonly WindowSystem _windowSystem = new("Glamourer"); private readonly UiBuilder _uiBuilder; - private readonly Interface _ui; + private readonly MainWindow _ui; - public GlamourerWindowSystem(UiBuilder uiBuilder, Interface ui) + public GlamourerWindowSystem(UiBuilder uiBuilder, MainWindow ui) { _uiBuilder = uiBuilder; _ui = ui; @@ -24,7 +24,4 @@ public class GlamourerWindowSystem : IDisposable _uiBuilder.Draw -= _windowSystem.Draw; _uiBuilder.OpenConfigUi -= _ui.Toggle; } - - public void Toggle() - => _ui.Toggle(); } diff --git a/Glamourer/Gui/MainWindow.cs b/Glamourer/Gui/MainWindow.cs new file mode 100644 index 0000000..9e4c4fc --- /dev/null +++ b/Glamourer/Gui/MainWindow.cs @@ -0,0 +1,39 @@ +using System; +using System.Numerics; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin; +using Glamourer.Gui.Tabs; +using ImGuiNET; +using OtterGui.Widgets; + +namespace Glamourer.Gui; + +public class MainWindow : Window +{ + private readonly ITab[] _tabs; + + public MainWindow(DalamudPluginInterface pi, DebugTab debugTab) + : base(GetLabel()) + { + pi.UiBuilder.DisableGposeUiHide = true; + SizeConstraints = new WindowSizeConstraints() + { + MinimumSize = new Vector2(675, 675), + MaximumSize = ImGui.GetIO().DisplaySize, + }; + _tabs = new ITab[] + { + debugTab, + }; + } + + public override void Draw() + { + TabBar.Draw("##tabs", ImGuiTabBarFlags.None, ReadOnlySpan.Empty, out var currentTab, () => { }, _tabs); + } + + private static string GetLabel() + => Item.Version.Length == 0 + ? "Glamourer###GlamourerMainWindow" + : $"Glamourer v{Item.Version}###GlamourerMainWindow"; +} diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs new file mode 100644 index 0000000..b94ab8f --- /dev/null +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -0,0 +1,491 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Interface; +using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Glamourer.Customization; +using Glamourer.Interop; +using Glamourer.Interop.Penumbra; +using Glamourer.Interop.Structs; +using Glamourer.Services; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Gui.Tabs; + +public unsafe class DebugTab : ITab +{ + private readonly VisorService _visorService; + private readonly ChangeCustomizeService _changeCustomizeService; + private readonly UpdateSlotService _updateSlotService; + private readonly WeaponService _weaponService; + private readonly PenumbraService _penumbra; + private readonly ObjectTable _objects; + + private readonly IdentifierService _identifier; + private readonly ActorService _actors; + private readonly ItemService _items; + private readonly CustomizationService _customization; + + private int _gameObjectIndex; + + public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, ObjectTable objects, + UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra, IdentifierService identifier, + ActorService actors, ItemService items, CustomizationService customization) + { + _changeCustomizeService = changeCustomizeService; + _visorService = visorService; + _objects = objects; + _updateSlotService = updateSlotService; + _weaponService = weaponService; + _penumbra = penumbra; + _identifier = identifier; + _actors = actors; + _items = items; + _customization = customization; + } + + public ReadOnlySpan Label + => "Debug"u8; + + public void DrawContent() + { + DrawInteropHeader(); + DrawGameDataHeader(); + DrawPenumbraHeader(); + } + + #region Interop + + private void DrawInteropHeader() + { + if (!ImGui.CollapsingHeader("Interop")) + return; + + ImGui.InputInt("Game Object Index", ref _gameObjectIndex, 0, 0); + var actor = (Actor)_objects.GetObjectAddress(_gameObjectIndex); + var model = actor.Model; + using var table = ImRaii.Table("##interopTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableHeader("Actor"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Model"); + ImGui.TableNextColumn(); + + ImGuiUtil.DrawTableColumn("Address"); + ImGui.TableNextColumn(); + if (ImGui.Selectable($"0x{model.Address:X}")) + ImGui.SetClipboardText($"0x{model.Address:X}"); + ImGui.TableNextColumn(); + if (ImGui.Selectable($"0x{model.Address:X}")) + ImGui.SetClipboardText($"0x{model.Address:X}"); + ImGui.TableNextColumn(); + + ImGuiUtil.DrawTableColumn("Mainhand"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetMainhand().ToString() : "No Character"); + ImGui.TableNextColumn(); + var weapon = model.AsDrawObject->Object.ChildObject; + if (ImGui.Selectable($"0x{(ulong)weapon:X}")) + ImGui.SetClipboardText($"0x{(ulong)weapon:X}"); + ImGuiUtil.DrawTableColumn("Offhand"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetOffhand().ToString() : "No Character"); + if (weapon != null && ImGui.Selectable($"0x{(ulong)weapon->NextSiblingObject:X}")) + ImGui.SetClipboardText($"0x{(ulong)weapon->NextSiblingObject:X}"); + DrawVisor(actor, model); + DrawHatState(actor, model); + DrawWeaponState(actor, model); + DrawWetness(actor, model); + DrawEquip(actor, model); + DrawCustomize(actor, model); + } + + private void DrawVisor(Actor actor, Model model) + { + using var id = ImRaii.PushId("Visor"); + ImGuiUtil.DrawTableColumn("Visor State"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->DrawData.IsVisorToggled.ToString() : "No Character"); + ImGuiUtil.DrawTableColumn(model.IsHuman ? _visorService.GetVisorState(model).ToString() : "No Human"); + ImGui.TableNextColumn(); + if (!model.IsHuman) + return; + + if (ImGui.SmallButton("Set True")) + _visorService.SetVisorState(model, true); + ImGui.SameLine(); + if (ImGui.SmallButton("Set False")) + _visorService.SetVisorState(model, false); + ImGui.SameLine(); + if (ImGui.SmallButton("Toggle")) + _visorService.SetVisorState(model, !_visorService.GetVisorState(model)); + } + + private void DrawHatState(Actor actor, Model model) + { + using var id = ImRaii.PushId("HatState"); + ImGuiUtil.DrawTableColumn("Hat State"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter + ? actor.AsCharacter->DrawData.IsHatHidden ? "Hidden" : actor.GetArmor(EquipSlot.Head).ToString() + : "No Character"); + ImGuiUtil.DrawTableColumn(model.IsHuman + ? model.AsHuman->Head.Value == 0 ? "No Hat" : model.GetArmor(EquipSlot.Head).ToString() + : "No Human"); + ImGui.TableNextColumn(); + if (!model.IsHuman) + return; + + if (ImGui.SmallButton("Hide")) + _updateSlotService.UpdateSlot(model, EquipSlot.Head, CharacterArmor.Empty); + ImGui.SameLine(); + if (ImGui.SmallButton("Show")) + _updateSlotService.UpdateSlot(model, EquipSlot.Head, actor.GetArmor(EquipSlot.Head)); + ImGui.SameLine(); + if (ImGui.SmallButton("Toggle")) + _updateSlotService.UpdateSlot(model, EquipSlot.Head, + model.AsHuman->Head.Value == 0 ? actor.GetArmor(EquipSlot.Head) : CharacterArmor.Empty); + } + + private void DrawWeaponState(Actor actor, Model model) + { + using var id = ImRaii.PushId("WeaponState"); + ImGuiUtil.DrawTableColumn("Weapon State"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter + ? actor.AsCharacter->DrawData.IsWeaponHidden ? "Hidden" : "Visible" + : "No Character"); + var text = string.Empty; + // TODO + if (!model.IsHuman) + { + text = "No Model"; + } + else if (model.AsDrawObject->Object.ChildObject == null) + { + text = "No Weapon"; + } + else + { + var weapon = (DrawObject*)model.AsDrawObject->Object.ChildObject; + if ((weapon->Flags & 0x09) == 0x09) + text = "Visible"; + else + text = "Hidden"; + } + + ImGuiUtil.DrawTableColumn(text); + ImGui.TableNextColumn(); + if (!model.IsHuman) + return; + } + + private void DrawWetness(Actor actor, Model model) + { + using var id = ImRaii.PushId("Wetness"); + ImGuiUtil.DrawTableColumn("Wetness"); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->IsGPoseWet ? "GPose" : "None" : "No Character"); + var modelString = model.IsCharacterBase + ? $"{model.AsCharacterBase->SwimmingWetness:F4} Swimming\n" + + $"{model.AsCharacterBase->WeatherWetness:F4} Weather\n" + + $"{model.AsCharacterBase->ForcedWetness:F4} Forced\n" + + $"{model.AsCharacterBase->WetnessDepth:F4} Depth\n" + : "No CharacterBase"; + ImGuiUtil.DrawTableColumn(modelString); + ImGui.TableNextColumn(); + if (!actor.IsCharacter) + return; + + if (ImGui.SmallButton("GPose On")) + actor.AsCharacter->IsGPoseWet = true; + ImGui.SameLine(); + if (ImGui.SmallButton("GPose Off")) + actor.AsCharacter->IsGPoseWet = false; + ImGui.SameLine(); + if (ImGui.SmallButton("GPose Toggle")) + actor.AsCharacter->IsGPoseWet = !actor.AsCharacter->IsGPoseWet; + } + + private void DrawEquip(Actor actor, Model model) + { + using var id = ImRaii.PushId("Equipment"); + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + using var id2 = ImRaii.PushId((int)slot); + ImGuiUtil.DrawTableColumn(slot.ToName()); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetArmor(slot).ToString() : "No Character"); + ImGuiUtil.DrawTableColumn(model.IsHuman ? model.GetArmor(slot).ToString() : "No Human"); + ImGui.TableNextColumn(); + if (!model.IsHuman) + continue; + + if (ImGui.SmallButton("Change Piece")) + _updateSlotService.UpdateArmor(model, slot, + new CharacterArmor((SetId)(slot == EquipSlot.Hands ? 6064 : slot == EquipSlot.Head ? 6072 : 1), 1, 0)); + ImGui.SameLine(); + if (ImGui.SmallButton("Change Stain")) + _updateSlotService.UpdateStain(model, slot, 5); + ImGui.SameLine(); + if (ImGui.SmallButton("Reset")) + _updateSlotService.UpdateSlot(model, slot, actor.GetArmor(slot)); + } + } + + private void DrawCustomize(Actor actor, Model model) + { + using var id = ImRaii.PushId("Customize"); + var actorCustomize = new Customize(actor.IsCharacter + ? *(Penumbra.GameData.Structs.CustomizeData*)&actor.AsCharacter->DrawData.CustomizeData + : new Penumbra.GameData.Structs.CustomizeData()); + var modelCustomize = new Customize(model.IsHuman + ? *(Penumbra.GameData.Structs.CustomizeData*)model.AsHuman->CustomizeData + : new Penumbra.GameData.Structs.CustomizeData()); + foreach (var type in Enum.GetValues()) + { + using var id2 = ImRaii.PushId((int)type); + ImGuiUtil.DrawTableColumn(type.ToDefaultName()); + ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actorCustomize[type].Value.ToString("X2") : "No Character"); + ImGuiUtil.DrawTableColumn(model.IsHuman ? modelCustomize[type].Value.ToString("X2") : "No Human"); + ImGui.TableNextColumn(); + if (!model.IsHuman || type.ToFlag().RequiresRedraw()) + continue; + + if (ImGui.SmallButton("++")) + { + modelCustomize.Set(type, (CustomizeValue)(modelCustomize[type].Value + 1)); + _changeCustomizeService.UpdateCustomize(model, modelCustomize.Data); + } + + ImGui.SameLine(); + if (ImGui.SmallButton("--")) + { + modelCustomize.Set(type, (CustomizeValue)(modelCustomize[type].Value - 1)); + _changeCustomizeService.UpdateCustomize(model, modelCustomize.Data); + } + + ImGui.SameLine(); + if (ImGui.SmallButton("Reset")) + { + modelCustomize.Set(type, actorCustomize[type]); + _changeCustomizeService.UpdateCustomize(model, modelCustomize.Data); + } + } + } + + #endregion + + #region Penumbra + + private Model _drawObject = Model.Null; + + private void DrawPenumbraHeader() + { + if (!ImGui.CollapsingHeader("Penumbra")) + return; + + using var table = ImRaii.Table("##PenumbraTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + ImGuiUtil.DrawTableColumn("Available"); + ImGuiUtil.DrawTableColumn(_penumbra.Available.ToString()); + ImGui.TableNextColumn(); + if (ImGui.SmallButton("Unattach")) + _penumbra.Unattach(); + ImGui.SameLine(); + if (ImGui.SmallButton("Reattach")) + _penumbra.Reattach(); + + ImGuiUtil.DrawTableColumn("Draw Object"); + ImGui.TableNextColumn(); + var address = _drawObject.Address; + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + if (ImGui.InputScalar("##drawObjectPtr", ImGuiDataType.U64, (nint)(&address), IntPtr.Zero, IntPtr.Zero, "%llx", + ImGuiInputTextFlags.CharsHexadecimal)) + _drawObject = address; + ImGuiUtil.DrawTableColumn(_penumbra.Available + ? $"0x{_penumbra.GameObjectFromDrawObject(_drawObject).Address:X}" + : "Penumbra Unavailable"); + + ImGuiUtil.DrawTableColumn("Cutscene Object"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + ImGui.InputInt("##CutsceneIndex", ref _gameObjectIndex, 0, 0); + ImGuiUtil.DrawTableColumn(_penumbra.Available + ? _penumbra.CutsceneParent(_gameObjectIndex).ToString() + : "Penumbra Unavailable"); + + ImGuiUtil.DrawTableColumn("Redraw Object"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + ImGui.InputInt("##redrawObject", ref _gameObjectIndex, 0, 0); + ImGui.TableNextColumn(); + using (var disabled = ImRaii.Disabled(!_penumbra.Available)) + { + if (ImGui.SmallButton("Redraw")) + _penumbra.RedrawObject(_objects.GetObjectAddress(_gameObjectIndex), RedrawType.Redraw); + } + } + + #endregion + + #region GameData + + private void DrawGameDataHeader() + { + if (!ImGui.CollapsingHeader("Game Data")) + return; + + DrawIdentifierService(); + DrawActorService(); + DrawItemService(); + DrawCustomizationService(); + } + + private string _gamePath = string.Empty; + private int _setId; + private int _secondaryId; + private int _variant; + + private void DrawIdentifierService() + { + using var disabled = ImRaii.Disabled(!_identifier.Valid); + using var tree = ImRaii.TreeNode("Identifier Service"); + if (!tree || !_identifier.Valid) + return; + + disabled.Dispose(); + + + static void Text(string text) + { + if (text.Length > 0) + ImGui.TextUnformatted(text); + } + + ImGui.TextUnformatted("Parse Game Path"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); + ImGui.InputTextWithHint("##gamePath", "Enter game path...", ref _gamePath, 256); + var fileInfo = _identifier.AwaitedService.GamePathParser.GetFileInfo(_gamePath); + ImGui.TextUnformatted( + $"{fileInfo.ObjectType} {fileInfo.EquipSlot} {fileInfo.PrimaryId} {fileInfo.SecondaryId} {fileInfo.Variant} {fileInfo.BodySlot} {fileInfo.CustomizationType}"); + Text(string.Join("\n", _identifier.AwaitedService.Identify(_gamePath).Keys)); + + ImGui.Separator(); + ImGui.TextUnformatted("Identify Model"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + ImGui.InputInt("##SetId", ref _setId, 0, 0); + ImGui.SameLine(); + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + ImGui.InputInt("##TypeId", ref _secondaryId, 0, 0); + ImGui.SameLine(); + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + ImGui.InputInt("##Variant", ref _variant, 0, 0); + + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var identified = _identifier.AwaitedService.Identify((SetId)_setId, (ushort)_variant, slot); + Text(string.Join("\n", identified.Select(i => i.Name.ToDalamudString().TextValue))); + } + + var main = _identifier.AwaitedService.Identify((SetId)_setId, (WeaponType)_secondaryId, (ushort)_variant, EquipSlot.MainHand); + Text(string.Join("\n", main.Select(i => i.Name.ToDalamudString().TextValue))); + var off = _identifier.AwaitedService.Identify((SetId)_setId, (WeaponType)_secondaryId, (ushort)_variant, EquipSlot.OffHand); + Text(string.Join("\n", off.Select(i => i.Name.ToDalamudString().TextValue))); + } + + private string _bnpcFilter = string.Empty; + private string _enpcFilter = string.Empty; + private string _companionFilter = string.Empty; + private string _mountFilter = string.Empty; + private string _ornamentFilter = string.Empty; + private string _worldFilter = string.Empty; + + private void DrawActorService() + { + using var disabled = ImRaii.Disabled(!_actors.Valid); + using var tree = ImRaii.TreeNode("Actor Service"); + if (!tree || !_actors.Valid) + return; + + disabled.Dispose(); + + DrawNameTable("BNPCs", ref _bnpcFilter, _actors.AwaitedService.Data.BNpcs.Select(kvp => (kvp.Key, kvp.Value))); + DrawNameTable("ENPCs", ref _enpcFilter, _actors.AwaitedService.Data.ENpcs.Select(kvp => (kvp.Key, kvp.Value))); + DrawNameTable("Companions", ref _companionFilter, _actors.AwaitedService.Data.Companions.Select(kvp => (kvp.Key, kvp.Value))); + DrawNameTable("Mounts", ref _mountFilter, _actors.AwaitedService.Data.Mounts.Select(kvp => (kvp.Key, kvp.Value))); + DrawNameTable("Ornaments", ref _ornamentFilter, _actors.AwaitedService.Data.Ornaments.Select(kvp => (kvp.Key, kvp.Value))); + DrawNameTable("Worlds", ref _worldFilter, _actors.AwaitedService.Data.Worlds.Select(kvp => ((uint)kvp.Key, kvp.Value))); + } + + private static void DrawNameTable(string label, ref string filter, IEnumerable<(uint, string)> names) + { + using var _ = ImRaii.PushId(label); + using var tree = ImRaii.TreeNode(label); + if (!tree) + return; + + var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref filter, 256); + var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; + using var table = ImRaii.Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter, + new Vector2(-1, 10 * height)); + if (!table) + return; + + if (resetScroll) + ImGui.SetScrollY(0); + ImGui.TableSetupColumn("1", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("2", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableNextColumn(); + var skips = ImGuiClip.GetNecessarySkips(height); + ImGui.TableNextColumn(); + var f = filter; + var remainder = ImGuiClip.FilteredClippedDraw(names.Select(p => (p.Item1.ToString("D5"), p.Item2)), skips, + p => p.Item1.Contains(f) || p.Item2.Contains(f, StringComparison.OrdinalIgnoreCase), + p => + { + ImGuiUtil.DrawTableColumn(p.Item1); + ImGuiUtil.DrawTableColumn(p.Item2); + }); + ImGuiClip.DrawEndDummy(remainder, height); + } + + private void DrawItemService() + { + using var disabled = ImRaii.Disabled(!_items.Valid); + using var tree = ImRaii.TreeNode("Item Manager"); + if (!tree || !_items.Valid) + return; + + disabled.Dispose(); + } + + private void DrawCustomizationService() + { + using var id = ImRaii.PushId("Customization"); + ImGuiUtil.DrawTableColumn("Customization Service"); + ImGui.TableNextColumn(); + if (!_customization.Valid) + { + ImGui.TextUnformatted("Unavailable"); + ImGui.TableNextColumn(); + return; + } + + using var tree = ImRaii.TreeNode("Available###Customization", ImGuiTreeNodeFlags.NoTreePushOnOpen); + ImGui.TableNextColumn(); + + if (!tree) + return; + } + + #endregion +} diff --git a/Glamourer/Interop/ChangeCustomizeService.cs b/Glamourer/Interop/ChangeCustomizeService.cs index e770025..da81490 100644 --- a/Glamourer/Interop/ChangeCustomizeService.cs +++ b/Glamourer/Interop/ChangeCustomizeService.cs @@ -1,24 +1,34 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Glamourer.Interop.Structs; using Penumbra.GameData.Structs; namespace Glamourer.Interop; +/// +/// Access the function the game uses to update customize data on the character screen. +/// Changes in Race, body type or Gender are probably ignored. +/// This operates on draw objects, not game objects. +/// public unsafe class ChangeCustomizeService { public ChangeCustomizeService() => SignatureHelper.Initialise(this); - public delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment); + private delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment); [Signature(Sigs.ChangeCustomize)] private readonly ChangeCustomizeDelegate _changeCustomize = null!; - public bool UpdateCustomize(Actor actor, CustomizeData customize) + public bool UpdateCustomize(Model model, CustomizeData customize) { - if (customize.Data == null || !actor.Valid || !actor.DrawObject.Valid) + if (!model.IsHuman) return false; - return _changeCustomize(actor.DrawObject.Pointer, customize.Data, 1); + Item.Log.Verbose($"[ChangeCustomize] Invoked on 0x{model.Address:X} with {customize}."); + return _changeCustomize(model.AsHuman, customize.Data, 1); } + + public bool UpdateCustomize(Actor actor, CustomizeData customize) + => UpdateCustomize(actor.Model, customize); } diff --git a/Glamourer/Interop/Penumbra/PenumbraService.cs b/Glamourer/Interop/Penumbra/PenumbraService.cs new file mode 100644 index 0000000..d547a7c --- /dev/null +++ b/Glamourer/Interop/Penumbra/PenumbraService.cs @@ -0,0 +1,142 @@ +using System; +using Dalamud.Logging; +using Dalamud.Plugin; +using Glamourer.Interop.Structs; +using Penumbra.Api; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Glamourer.Interop.Penumbra; + +public unsafe class PenumbraService : IDisposable +{ + public const int RequiredPenumbraBreakingVersion = 4; + public const int RequiredPenumbraFeatureVersion = 15; + + private readonly DalamudPluginInterface _pluginInterface; + private readonly EventSubscriber _tooltipSubscriber; + private readonly EventSubscriber _clickSubscriber; + private readonly EventSubscriber _creatingCharacterBase; + private readonly EventSubscriber _createdCharacterBase; + private ActionSubscriber _redrawSubscriber; + private FuncSubscriber _drawObjectInfo; + private FuncSubscriber _cutsceneParent; + + private readonly EventSubscriber _initializedEvent; + private readonly EventSubscriber _disposedEvent; + public bool Available { get; private set; } + + public PenumbraService(DalamudPluginInterface pi) + { + _pluginInterface = pi; + _initializedEvent = Ipc.Initialized.Subscriber(pi, Reattach); + _disposedEvent = Ipc.Disposed.Subscriber(pi, Unattach); + _tooltipSubscriber = Ipc.ChangedItemTooltip.Subscriber(pi); + _clickSubscriber = Ipc.ChangedItemClick.Subscriber(pi); + _createdCharacterBase = Ipc.CreatedCharacterBase.Subscriber(pi); + _creatingCharacterBase = Ipc.CreatingCharacterBase.Subscriber(pi); + Reattach(); + } + + public event Action Click + { + add => _clickSubscriber.Event += value; + remove => _clickSubscriber.Event -= value; + } + + public event Action Tooltip + { + add => _tooltipSubscriber.Event += value; + remove => _tooltipSubscriber.Event -= value; + } + + + public event Action CreatingCharacterBase + { + add => _creatingCharacterBase.Event += value; + remove => _creatingCharacterBase.Event -= value; + } + + public event Action CreatedCharacterBase + { + add => _createdCharacterBase.Event += value; + remove => _createdCharacterBase.Event -= value; + } + + /// Obtain the game object corresponding to a draw object. + public Actor GameObjectFromDrawObject(Model drawObject) + => Available ? _drawObjectInfo.Invoke(drawObject.Address).Item1 : Actor.Null; + + /// Obtain the parent of a cutscene actor if it is known. + public int CutsceneParent(int idx) + => Available ? _cutsceneParent.Invoke(idx) : -1; + + /// Try to redraw the given actor. + public void RedrawObject(Actor actor, RedrawType settings) + { + if (!actor || !Available) + return; + + try + { + _redrawSubscriber.Invoke(actor.AsObject->ObjectIndex, settings); + } + catch (Exception e) + { + PluginLog.Debug($"Failure redrawing object:\n{e}"); + } + } + + /// Reattach to the currently running Penumbra IPC provider. Unattaches before if necessary. + public void Reattach() + { + try + { + Unattach(); + + var (breaking, feature) = Ipc.ApiVersions.Subscriber(_pluginInterface).Invoke(); + if (breaking != RequiredPenumbraBreakingVersion || feature < RequiredPenumbraFeatureVersion) + throw new Exception( + $"Invalid Version {breaking}.{feature:D4}, required major Version {RequiredPenumbraBreakingVersion} with feature greater or equal to {RequiredPenumbraFeatureVersion}."); + + _tooltipSubscriber.Enable(); + _clickSubscriber.Enable(); + _creatingCharacterBase.Enable(); + _createdCharacterBase.Enable(); + _drawObjectInfo = Ipc.GetDrawObjectInfo.Subscriber(_pluginInterface); + _cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface); + _redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface); + Available = true; + Item.Log.Debug("Glamourer attached to Penumbra."); + } + catch (Exception e) + { + Item.Log.Debug($"Could not attach to Penumbra:\n{e}"); + } + } + + /// Unattach from the currently running Penumbra IPC provider. + public void Unattach() + { + _tooltipSubscriber.Disable(); + _clickSubscriber.Disable(); + _creatingCharacterBase.Disable(); + _createdCharacterBase.Disable(); + if (Available) + { + Available = false; + Item.Log.Debug("Glamourer detached from Penumbra."); + } + } + + public void Dispose() + { + Unattach(); + _tooltipSubscriber.Dispose(); + _clickSubscriber.Dispose(); + _creatingCharacterBase.Dispose(); + _createdCharacterBase.Dispose(); + _initializedEvent.Dispose(); + _disposedEvent.Dispose(); + } +} diff --git a/Glamourer/Interop/Structs/Actor.cs b/Glamourer/Interop/Structs/Actor.cs new file mode 100644 index 0000000..e54d28b --- /dev/null +++ b/Glamourer/Interop/Structs/Actor.cs @@ -0,0 +1,97 @@ +using Penumbra.GameData.Actors; +using System; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop.Structs; + +public readonly unsafe struct Actor : IEquatable +{ + private Actor(nint address) + => Address = address; + + public static readonly Actor Null = new(nint.Zero); + + public readonly nint Address; + + public GameObject* AsObject + => (GameObject*)Address; + + public Character* AsCharacter + => (Character*)Address; + + public bool Valid + => Address != nint.Zero; + + public bool IsCharacter + => Valid && AsObject->IsCharacter(); + + public static implicit operator Actor(nint? pointer) + => new(pointer ?? nint.Zero); + + public static implicit operator Actor(GameObject* pointer) + => new((nint)pointer); + + public static implicit operator Actor(Character* pointer) + => new((nint)pointer); + + public static implicit operator nint(Actor actor) + => actor.Address; + + public ActorIdentifier GetIdentifier(ActorManager actors) + => actors.FromObject(AsObject, out _, true, true, false); + + public bool Identifier(ActorManager actors, out ActorIdentifier ident) + { + if (Valid) + { + ident = GetIdentifier(actors); + return ident.IsValid; + } + + ident = ActorIdentifier.Invalid; + return false; + } + + public Model Model + => Valid ? AsObject->DrawObject : null; + + public static implicit operator bool(Actor actor) + => actor.Address != nint.Zero; + + public static bool operator true(Actor actor) + => actor.Address != nint.Zero; + + public static bool operator false(Actor actor) + => actor.Address == nint.Zero; + + public static bool operator !(Actor actor) + => actor.Address == nint.Zero; + + public bool Equals(Actor other) + => Address == other.Address; + + public override bool Equals(object? obj) + => obj is Actor other && Equals(other); + + public override int GetHashCode() + => Address.GetHashCode(); + + public static bool operator ==(Actor lhs, Actor rhs) + => lhs.Address == rhs.Address; + + public static bool operator !=(Actor lhs, Actor rhs) + => lhs.Address != rhs.Address; + + /// Only valid for characters. + public CharacterArmor GetArmor(EquipSlot slot) + => ((CharacterArmor*)&AsCharacter->DrawData.Head)[slot.ToIndex()]; + + public CharacterWeapon GetMainhand() + => *(CharacterWeapon*)&AsCharacter->DrawData.MainHandModel; + + public CharacterWeapon GetOffhand() + => *(CharacterWeapon*)&AsCharacter->DrawData.OffHandModel; +} diff --git a/Glamourer/Interop/Structs/ActorData.cs b/Glamourer/Interop/Structs/ActorData.cs new file mode 100644 index 0000000..953260b --- /dev/null +++ b/Glamourer/Interop/Structs/ActorData.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Glamourer.Interop.Structs; + +public readonly struct ActorData +{ + public readonly List Objects; + public readonly string Label; + + public bool Valid + => Objects.Count > 0; + + public ActorData(Actor actor, string label) + { + Objects = new List { actor }; + Label = label; + } + + public static readonly ActorData Invalid = new(false); + + private ActorData(bool _) + { + Objects = new List(0); + Label = string.Empty; + } +} diff --git a/Glamourer/Interop/Structs/Model.cs b/Glamourer/Interop/Structs/Model.cs new file mode 100644 index 0000000..a7f04d5 --- /dev/null +++ b/Glamourer/Interop/Structs/Model.cs @@ -0,0 +1,92 @@ +using System; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; + +namespace Glamourer.Interop.Structs; + +public readonly unsafe struct Model : IEquatable +{ + private Model(nint address) + => Address = address; + + public readonly nint Address; + + public static readonly Model Null = new(0); + + public DrawObject* AsDrawObject + => (DrawObject*)Address; + + public CharacterBase* AsCharacterBase + => (CharacterBase*)Address; + + public Human* AsHuman + => (Human*)Address; + + public static implicit operator Model(nint? pointer) + => new(pointer ?? nint.Zero); + + public static implicit operator Model(DrawObject* pointer) + => new((nint)pointer); + + public static implicit operator Model(Human* pointer) + => new((nint)pointer); + + public static implicit operator Model(CharacterBase* pointer) + => new((nint)pointer); + + public static implicit operator nint(Model model) + => model.Address; + + public bool Valid + => Address != nint.Zero; + + public bool IsCharacterBase + => Valid && AsDrawObject->Object.GetObjectType() == ObjectType.CharacterBase; + + public bool IsHuman + => IsCharacterBase && AsCharacterBase->GetModelType() == CharacterBase.ModelType.Human; + + public static implicit operator bool(Model actor) + => actor.Address != nint.Zero; + + public static bool operator true(Model actor) + => actor.Address != nint.Zero; + + public static bool operator false(Model actor) + => actor.Address == nint.Zero; + + public static bool operator !(Model actor) + => actor.Address == nint.Zero; + + public bool Equals(Model other) + => Address == other.Address; + + public override bool Equals(object? obj) + => obj is Model other && Equals(other); + + public override int GetHashCode() + => Address.GetHashCode(); + + public static bool operator ==(Model lhs, Model rhs) + => lhs.Address == rhs.Address; + + public static bool operator !=(Model lhs, Model rhs) + => lhs.Address != rhs.Address; + + /// Only valid for humans. + public CharacterArmor GetArmor(EquipSlot slot) + => ((CharacterArmor*)AsHuman->EquipSlotData)[slot.ToIndex()]; + + public CharacterWeapon GetMainhand() + { + var weapon = AsDrawObject->Object.ChildObject; + if (weapon == null) + return CharacterWeapon.Empty; + weapon + } + + public CharacterWeapon GetOffhand() + => *(CharacterWeapon*)&AsCharacter->DrawData.OffHandModel; +} diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index 0b34f58..b4aaa6a 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -1,7 +1,8 @@ using System; -using System.Linq; using Dalamud.Hooking; using Dalamud.Utility.Signatures; +using Glamourer.Events; +using Glamourer.Interop.Structs; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -9,8 +10,11 @@ namespace Glamourer.Interop; public unsafe class UpdateSlotService : IDisposable { - public UpdateSlotService() + public readonly UpdatedSlot Event; + + public UpdateSlotService(UpdatedSlot updatedSlot) { + Event = updatedSlot; SignatureHelper.Initialise(this); _flagSlotForUpdateHook.Enable(); } @@ -19,59 +23,41 @@ public unsafe class UpdateSlotService : IDisposable => _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) + public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data) { - InvokeFlagSlotEvent(drawObject, slot, ref data); + if (!drawObject.IsCharacterBase) + return; FlagSlotForUpdateInterop(drawObject, slot, data); } - public void UpdateStain(DrawObject drawObject, EquipSlot slot, StainId stain) + public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor data) { - var armor = drawObject.Equip[slot] with { Stain = stain }; - UpdateSlot(drawObject, slot, armor); + if (!drawObject.IsCharacterBase) + return; + + FlagSlotForUpdateInterop(drawObject, slot, data.With(drawObject.GetArmor(slot).Stain)); + } + + public void UpdateStain(Model drawObject, EquipSlot slot, StainId stain) + { + if (!drawObject.IsHuman) + return; + + FlagSlotForUpdateInterop(drawObject, slot, drawObject.GetArmor(slot).With(stain)); } private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { - var slot = slotIdx.ToEquipSlot(); - InvokeFlagSlotEvent(drawObject, slot, ref *data); - return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data); + var slot = slotIdx.ToEquipSlot(); + var returnValue = ulong.MaxValue; + Event.Invoke(drawObject, slot, ref *data, ref returnValue); + return returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; } - 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}."); - } + private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor) + => _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); } diff --git a/Glamourer/Interop/VisorService.cs b/Glamourer/Interop/VisorService.cs index 250ee9f..3679b66 100644 --- a/Glamourer/Interop/VisorService.cs +++ b/Glamourer/Interop/VisorService.cs @@ -1,16 +1,19 @@ using System; -using System.Linq; +using System.Runtime.CompilerServices; using Dalamud.Hooking; using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Penumbra.GameData.Structs; +using Glamourer.Events; +using Glamourer.Interop.Structs; namespace Glamourer.Interop; public class VisorService : IDisposable { - public VisorService() + public readonly VisorStateChanged Event; + + public VisorService(VisorStateChanged visorStateChanged) { + Event = visorStateChanged; SignatureHelper.Initialise(this); _setupVisorHook.Enable(); } @@ -18,61 +21,67 @@ public class VisorService : IDisposable public void Dispose() => _setupVisorHook.Dispose(); - public static unsafe bool GetVisorState(nint humanPtr) + /// Obtain the current state of the Visor for the given draw object (true: toggled). + public unsafe bool GetVisorState(Model characterBase) { - if (humanPtr == IntPtr.Zero) + if (!characterBase.IsCharacterBase) return false; - var data = (Human*)humanPtr; - var flags = &data->CharacterBase.UnkFlags_01; - return (*flags & Offsets.DrawObjectVisorStateFlag) != 0; + // TODO: use client structs. + return (characterBase.AsCharacterBase->UnkFlags_01 & Offsets.DrawObjectVisorStateFlag) != 0; } - public unsafe void SetVisorState(nint humanPtr, bool on) + /// Manually set the state of the Visor for the given draw object. + /// The draw object. + /// The desired state (true: toggled). + /// Whether the state was changed. + public unsafe bool SetVisorState(Model human, bool on) { - if (humanPtr == IntPtr.Zero) - return; + if (!human.IsHuman) + return false; - var data = (Human*)humanPtr; - _setupVisorHook.Original(humanPtr, (ushort) data->HeadSetID, on); + var oldState = GetVisorState(human); + Item.Log.Verbose($"[SetVisorState] Invoked manually on 0x{human.Address:X} switching from {oldState} to {on}."); + if (oldState == on) + return false; + + + SetupVisorHook(human, (ushort)human.AsHuman->HeadSetID, on); + return true; } 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))] + [Signature(global::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) + private void SetupVisorDetour(nint human, ushort modelId, bool on) { - InvokeVisorEvent(humanPtr, modelId, ref on); - _setupVisorHook.Original(humanPtr, modelId, on); + var callOriginal = true; + var originalOn = on; + // Invoke an event that can change the requested value + // and also control whether the function should be called at all. + Event.Invoke(human, ref on, ref callOriginal); + + Item.Log.Excessive( + $"[SetVisorState] Invoked from game on 0x{human:X} switching to {on} (original {originalOn}, call original {callOriginal})."); + + if (callOriginal) + SetupVisorHook(human, modelId, on); } - private void InvokeVisorEvent(DrawObject drawObject, SetId modelId, ref bool on) + /// + /// The SetupVisor function does not set the visor state for the draw object itself, + /// it only sets the "visor is changing" state to false. + /// So we wrap a manual change of that flag with the function call. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private unsafe void SetupVisorHook(Model human, ushort modelId, 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}."); + // TODO: use client structs. + human.AsCharacterBase->UnkFlags_01 = (byte)(on + ? human.AsCharacterBase->UnkFlags_01 | Offsets.DrawObjectVisorStateFlag + : human.AsCharacterBase->UnkFlags_01 & ~Offsets.DrawObjectVisorStateFlag); + _setupVisorHook.Original(human.Address, modelId, on); } } diff --git a/Glamourer/Interop/WeaponService.cs b/Glamourer/Interop/WeaponService.cs index 15024b8..14004fd 100644 --- a/Glamourer/Interop/WeaponService.cs +++ b/Glamourer/Interop/WeaponService.cs @@ -1,8 +1,8 @@ using System; -using System.Runtime.InteropServices; using Dalamud.Hooking; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; +using Glamourer.Interop.Structs; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -13,6 +13,7 @@ public unsafe class WeaponService : IDisposable public WeaponService() { SignatureHelper.Initialise(this); + _loadWeaponHook = Hook.FromAddress((nint) DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour); _loadWeaponHook.Enable(); } @@ -21,68 +22,18 @@ public unsafe class WeaponService : IDisposable _loadWeaponHook.Dispose(); } - public static readonly int CharacterWeaponOffset = (int)Marshal.OffsetOf("DrawData"); + private delegate void LoadWeaponDelegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject, byte unk4); - public delegate void LoadWeaponDelegate(nint offsetCharacter, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, - byte skipGameObject, - byte unk4); + private readonly Hook _loadWeaponHook; - // 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, + private void LoadWeaponDetour(DrawDataContainer* drawData, 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}"); - //} - + var actor = (Actor) (nint)drawData->Unk8; + // 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); + _loadWeaponHook.Original(drawData, slot, weapon, redrawOnEquality, unk2, skipGameObject, unk4); + Item.Log.Information($"Weapon reloaded for 0x{actor.Address:X} with attributes {slot} {weapon:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}"); } // Load a specific weapon for a character by its data and slot. @@ -91,14 +42,14 @@ public unsafe class WeaponService : IDisposable switch (slot) { case EquipSlot.MainHand: - LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, weapon.Value, 0, 0, 1, 0); + LoadWeaponDetour(&character.AsCharacter->DrawData, 0, weapon.Value, 0, 0, 1, 0); return; case EquipSlot.OffHand: - LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, weapon.Value, 0, 0, 1, 0); + LoadWeaponDetour(&character.AsCharacter->DrawData, 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); + LoadWeaponDetour(&character.AsCharacter->DrawData, 0, weapon.Value, 0, 0, 1, 0); + LoadWeaponDetour(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 0, 0, 1, 0); return; // function can also be called with '2', but does not seem to ever be. } @@ -107,14 +58,14 @@ public unsafe class WeaponService : IDisposable // 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); + LoadWeaponDetour(&character.AsCharacter->DrawData, 0, main.Value, 1, 0, 1, 0); + LoadWeaponDetour(&character.AsCharacter->DrawData, 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; + var value = slot == EquipSlot.OffHand ? character.AsCharacter->DrawData.OffHandModel : character.AsCharacter->DrawData.MainHandModel; + var weapon = new CharacterWeapon(value.Value) { Stain = stain.Value }; LoadWeapon(character, slot, weapon); } } diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index 602a897..5595310 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -1,6 +1,7 @@ using System; using Dalamud.Game.Command; using Glamourer.Gui; +using Glamourer.Gui.Tabs; namespace Glamourer.Services; @@ -11,12 +12,12 @@ public class CommandService : IDisposable private const string ApplyCommandString = "/glamour"; private readonly CommandManager _commands; - private readonly Interface _interface; + private readonly MainWindow _mainWindow; - public CommandService(CommandManager commands, Interface ui) + public CommandService(CommandManager commands, MainWindow mainWindow) { - _commands = commands; - _interface = ui; + _commands = commands; + _mainWindow = mainWindow; _commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." }); _commands.AddHandler(ApplyCommandString, new CommandInfo(OnGlamour) { HelpMessage = $"Use Glamourer Functions: {HelpString}" }); @@ -29,7 +30,7 @@ public class CommandService : IDisposable } private void OnGlamourer(string command, string arguments) - => _interface.Toggle(); + => _mainWindow.Toggle(); private void OnGlamour(string command, string arguments) { } diff --git a/Glamourer/Services/DalamudServices.cs b/Glamourer/Services/DalamudServices.cs new file mode 100644 index 0000000..787ad12 --- /dev/null +++ b/Glamourer/Services/DalamudServices.cs @@ -0,0 +1,49 @@ +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Keys; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using Dalamud.IoC; +using Dalamud.Plugin; +using Microsoft.Extensions.DependencyInjection; + +namespace Glamourer.Services; + +public class DalamudServices +{ + public DalamudServices(DalamudPluginInterface pi) + { + pi.Inject(this); + } + + public void AddServices(IServiceCollection services) + { + services.AddSingleton(PluginInterface); + services.AddSingleton(Commands); + services.AddSingleton(GameData); + services.AddSingleton(ClientState); + services.AddSingleton(GameGui); + services.AddSingleton(Chat); + services.AddSingleton(Framework); + services.AddSingleton(Targets); + services.AddSingleton(Objects); + services.AddSingleton(KeyState); + services.AddSingleton(this); + services.AddSingleton(PluginInterface.UiBuilder); + } + + // @formatter:off + [PluginService][RequiredVersion("1.0")] public DalamudPluginInterface PluginInterface { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public CommandManager Commands { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public DataManager GameData { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ClientState ClientState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public GameGui GameGui { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ChatGui Chat { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public Framework Framework { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public TargetManager Targets { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public ObjectTable Objects { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public KeyState KeyState { get; private set; } = null!; + // @formatter:on +} diff --git a/Glamourer/Services/FilenameService.cs b/Glamourer/Services/FilenameService.cs index f4e1176..e67a531 100644 --- a/Glamourer/Services/FilenameService.cs +++ b/Glamourer/Services/FilenameService.cs @@ -1,8 +1,6 @@ -using System; using System.Collections.Generic; using System.IO; using Dalamud.Plugin; -using Glamourer.Designs; namespace Glamourer.Services; @@ -32,9 +30,6 @@ public class FilenameService yield return new FileInfo(file); } - public string DesignFile(Design design) - => DesignFile(design.Identifier.ToString()); - public string DesignFile(string identifier) => Path.Combine(DesignDirectory, $"{identifier}.json"); } diff --git a/Glamourer/Services/ItemManager.cs b/Glamourer/Services/ItemManager.cs index 65f4b13..a815c7b 100644 --- a/Glamourer/Services/ItemManager.cs +++ b/Glamourer/Services/ItemManager.cs @@ -22,7 +22,7 @@ public class ItemManager : IDisposable private readonly Configuration _config; public readonly IdentifierService IdentifierService; - public readonly ExcelSheet ItemSheet; + public readonly ExcelSheet ItemSheet; public readonly StainData Stains; public readonly ItemService ItemService; public readonly RestrictedGear RestrictedGear; @@ -30,7 +30,7 @@ public class ItemManager : IDisposable public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService, Configuration config) { _config = config; - ItemSheet = gameData.GetExcelSheet()!; + ItemSheet = gameData.GetExcelSheet()!; IdentifierService = identifierService; Stains = new StainData(pi, gameData, gameData.Language); ItemService = itemService; @@ -52,7 +52,7 @@ public class ItemManager : IDisposable return (false, armor); } - public readonly Item DefaultSword; + public readonly Lumina.Excel.GeneratedSheets.Item DefaultSword; public static uint NothingId(EquipSlot slot) => uint.MaxValue - 128 - (uint)slot.ToSlot(); @@ -81,7 +81,7 @@ public class ItemManager : IDisposable return new Designs.Item(SmallClothesNpc, SmallclothesId(slot), new CharacterArmor(SmallClothesNpcModel, 1, 0)); } - public (bool Valid, SetId Id, byte Variant, string ItemName) Resolve(EquipSlot slot, uint itemId, Item? item = null) + public (bool Valid, SetId Id, byte Variant, string ItemName) Resolve(EquipSlot slot, uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null) { slot = slot.ToSlot(); if (itemId == NothingId(slot)) @@ -100,7 +100,7 @@ public class ItemManager : IDisposable return (true, (SetId)item.ModelMain, (byte)(item.ModelMain >> 16), string.Intern(item.Name.ToDalamudString().TextValue)); } - public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, Item? item = null) + public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null) { if (item == null || item.RowId != itemId) item = ItemSheet.GetRow(itemId); @@ -117,7 +117,7 @@ public class ItemManager : IDisposable } public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, - FullEquipType mainType, Item? item = null) + FullEquipType mainType, Lumina.Excel.GeneratedSheets.Item? item = null) { var offType = mainType.Offhand(); if (itemId == NothingId(offType)) diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 0baacef..6ce8631 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -1,9 +1,9 @@ using Dalamud.Plugin; -using Glamourer.Api; -using Glamourer.Designs; +using Glamourer.Events; using Glamourer.Gui; +using Glamourer.Gui.Tabs; using Glamourer.Interop; -using Glamourer.State; +using Glamourer.Interop.Penumbra; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; using OtterGui.Log; @@ -18,12 +18,10 @@ public static class ServiceManager .AddSingleton(log) .AddDalamud(pi) .AddMeta() - .AddConfig() - .AddPenumbra() .AddInterop() - .AddGameData() - .AddDesigns() - .AddInterface() + .AddEvents() + .AddData() + .AddUi() .AddApi(); return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); @@ -36,45 +34,33 @@ public static class ServiceManager } private static IServiceCollection AddMeta(this IServiceCollection services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static IServiceCollection AddConfig(this IServiceCollection services) - => services.AddSingleton() + => services.AddSingleton() + .AddSingleton() .AddSingleton(); - private static IServiceCollection AddPenumbra(this IServiceCollection services) - => services.AddSingleton(); + private static IServiceCollection AddEvents(this IServiceCollection services) + => services.AddSingleton() + .AddSingleton(); - private static IServiceCollection AddGameData(this IServiceCollection services) + private static IServiceCollection AddData(this IServiceCollection services) => services.AddSingleton() - .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton(); private static IServiceCollection AddInterop(this IServiceCollection services) - => services.AddSingleton() - .AddSingleton() + => services.AddSingleton() + .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton(); - private static IServiceCollection AddDesigns(this IServiceCollection services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static IServiceCollection AddInterface(this IServiceCollection services) - => services.AddSingleton() + private static IServiceCollection AddUi(this IServiceCollection services) + => services + .AddSingleton() + .AddSingleton() .AddSingleton(); private static IServiceCollection AddApi(this IServiceCollection services) - => services.AddSingleton() - .AddSingleton(); + => services.AddSingleton(); } diff --git a/Glamourer/Services/ServiceWrapper.cs b/Glamourer/Services/ServiceWrapper.cs index 8cd16c2..b9220cc 100644 --- a/Glamourer/Services/ServiceWrapper.cs +++ b/Glamourer/Services/ServiceWrapper.cs @@ -7,8 +7,8 @@ using Penumbra.GameData.Actors; using System; using System.Threading.Tasks; using Dalamud.Game; -using Glamourer.Api; using Glamourer.Customization; +using Glamourer.Interop.Penumbra; using Penumbra.GameData.Data; using Penumbra.GameData; @@ -50,7 +50,7 @@ public abstract class AsyncServiceWrapper else { Service = service; - Glamourer.Log.Verbose($"[{Name}] Created."); + Item.Log.Verbose($"[{Name}] Created."); _task = null; } }); @@ -70,7 +70,7 @@ public abstract class AsyncServiceWrapper _task = null; if (Service is IDisposable d) d.Dispose(); - Glamourer.Log.Verbose($"[{Name}] Disposed."); + Item.Log.Verbose($"[{Name}] Disposed."); } } @@ -91,7 +91,7 @@ public sealed class ItemService : AsyncServiceWrapper public sealed class ActorService : AsyncServiceWrapper { public ActorService(DalamudPluginInterface pi, ObjectTable objects, ClientState clientState, Framework framework, DataManager gameData, - GameGui gui, PenumbraAttach penumbra) + GameGui gui, PenumbraService penumbra) : base(nameof(ActorService), () => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)penumbra.CutsceneParent(idx))) { } @@ -102,4 +102,4 @@ public sealed class CustomizationService : AsyncServiceWrapper CustomizationManager.Create(pi, gameData)) { } -} \ No newline at end of file +} diff --git a/Glamourer/Api/GlamourerIpc.cs b/GlamourerOld/Api/GlamourerIpc.cs similarity index 100% rename from Glamourer/Api/GlamourerIpc.cs rename to GlamourerOld/Api/GlamourerIpc.cs diff --git a/Glamourer/Api/PenumbraAttach.cs b/GlamourerOld/Api/PenumbraAttach.cs similarity index 100% rename from Glamourer/Api/PenumbraAttach.cs rename to GlamourerOld/Api/PenumbraAttach.cs diff --git a/Glamourer/Configuration.cs b/GlamourerOld/Configuration.cs similarity index 100% rename from Glamourer/Configuration.cs rename to GlamourerOld/Configuration.cs diff --git a/Glamourer/Dalamud.cs b/GlamourerOld/Dalamud.cs similarity index 100% rename from Glamourer/Dalamud.cs rename to GlamourerOld/Dalamud.cs diff --git a/Glamourer/Designs/Design.cs b/GlamourerOld/Designs/Design.cs similarity index 100% rename from Glamourer/Designs/Design.cs rename to GlamourerOld/Designs/Design.cs diff --git a/Glamourer/Designs/DesignData.cs b/GlamourerOld/Designs/DesignData.cs similarity index 100% rename from Glamourer/Designs/DesignData.cs rename to GlamourerOld/Designs/DesignData.cs diff --git a/Glamourer/Designs/DesignFileSystem.cs b/GlamourerOld/Designs/DesignFileSystem.cs similarity index 100% rename from Glamourer/Designs/DesignFileSystem.cs rename to GlamourerOld/Designs/DesignFileSystem.cs diff --git a/Glamourer/Designs/DesignManager.cs b/GlamourerOld/Designs/DesignManager.cs similarity index 100% rename from Glamourer/Designs/DesignManager.cs rename to GlamourerOld/Designs/DesignManager.cs diff --git a/Glamourer/Designs/EquipFlag.cs b/GlamourerOld/Designs/EquipFlag.cs similarity index 100% rename from Glamourer/Designs/EquipFlag.cs rename to GlamourerOld/Designs/EquipFlag.cs diff --git a/Glamourer/Designs/ModelData.cs b/GlamourerOld/Designs/ModelData.cs similarity index 100% rename from Glamourer/Designs/ModelData.cs rename to GlamourerOld/Designs/ModelData.cs diff --git a/Glamourer/Designs/Structs.cs b/GlamourerOld/Designs/Structs.cs similarity index 100% rename from Glamourer/Designs/Structs.cs rename to GlamourerOld/Designs/Structs.cs diff --git a/Glamourer/DrawObjectManager.cs b/GlamourerOld/DrawObjectManager.cs similarity index 100% rename from Glamourer/DrawObjectManager.cs rename to GlamourerOld/DrawObjectManager.cs diff --git a/Glamourer/Fixed/FixedCondition.cs b/GlamourerOld/Fixed/FixedCondition.cs similarity index 100% rename from Glamourer/Fixed/FixedCondition.cs rename to GlamourerOld/Fixed/FixedCondition.cs diff --git a/Glamourer/Fixed/FixedDesigns.cs b/GlamourerOld/Fixed/FixedDesigns.cs similarity index 100% rename from Glamourer/Fixed/FixedDesigns.cs rename to GlamourerOld/Fixed/FixedDesigns.cs diff --git a/GlamourerOld/Glamourer.cs b/GlamourerOld/Glamourer.cs new file mode 100644 index 0000000..aa84f00 --- /dev/null +++ b/GlamourerOld/Glamourer.cs @@ -0,0 +1,190 @@ +using System.Reflection; +using Dalamud.Plugin; +using Glamourer.Gui; +using Glamourer.Interop; +using Glamourer.Services; +using Microsoft.Extensions.DependencyInjection; +using OtterGui.Classes; +using OtterGui.Log; + +namespace Glamourer; + +public partial class Glamourer : IDalamudPlugin +{ + public string Name + => "Glamourer"; + + public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; + + public static readonly string CommitHash = + Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion ?? "Unknown"; + + + public static readonly Logger Log = new(); + public static ChatService ChatService { get; private set; } = null!; + private readonly ServiceProvider _services; + + public Glamourer(DalamudPluginInterface pluginInterface) + { + try + { + EventWrapper.ChangeLogger(Log); + _services = ServiceManager.CreateProvider(pluginInterface, Log); + ChatService = _services.GetRequiredService(); + _services.GetRequiredService(); + _services.GetRequiredService(); + _services.GetRequiredService(); + _services.GetRequiredService(); + _services.GetRequiredService(); + _services.GetRequiredService(); + _services.GetRequiredService(); + _services.GetRequiredService(); + _services.GetRequiredService(); + _services.GetRequiredService(); + } + catch + { + Dispose(); + throw; + } + } + + + public void Dispose() + { + _services?.Dispose(); + } + + //private static GameObject? GetPlayer(string name) + //{ + // var lowerName = name.ToLowerInvariant(); + // return lowerName switch + // { + // "" => null, + // "" => Dalamud.Objects[Interface.GPoseObjectId] ?? Dalamud.ClientState.LocalPlayer, + // "self" => Dalamud.Objects[Interface.GPoseObjectId] ?? Dalamud.ClientState.LocalPlayer, + // "" => Dalamud.Targets.Target, + // "target" => Dalamud.Targets.Target, + // "" => Dalamud.Targets.FocusTarget, + // "focus" => Dalamud.Targets.FocusTarget, + // "" => Dalamud.Targets.MouseOverTarget, + // "mouseover" => Dalamud.Targets.MouseOverTarget, + // _ => Dalamud.Objects.LastOrDefault( + // a => string.Equals(a.Name.ToString(), lowerName, StringComparison.InvariantCultureIgnoreCase)), + // }; + //} + // + //public void CopyToClipboard(Character player) + //{ + // var save = new CharacterSave(); + // save.LoadCharacter(player); + // ImGui.SetClipboardText(save.ToBase64()); + //} + // + //public void ApplyCommand(Character player, string target) + //{ + // CharacterSave? save = null; + // if (target.ToLowerInvariant() == "clipboard") + // try + // { + // save = CharacterSave.FromString(ImGui.GetClipboardText()); + // } + // catch (Exception) + // { + // Dalamud.Chat.PrintError("Clipboard does not contain a valid customization string."); + // } + // else if (!Designs.FileSystem.Find(target, out var child) || child is not Design d) + // Dalamud.Chat.PrintError("The given path to a saved design does not exist or does not point to a design."); + // else + // save = d.Data; + // + // save?.Apply(player); + // Penumbra.UpdateCharacters(player); + //} + // + //public void SaveCommand(Character player, string path) + //{ + // var save = new CharacterSave(); + // save.LoadCharacter(player); + // try + // { + // var (folder, name) = Designs.FileSystem.CreateAllFolders(path); + // var design = new Design(folder, name) { Data = save }; + // folder.FindOrAddChild(design); + // Designs.Designs.Add(design.FullName(), design.Data); + // Designs.SaveToFile(); + // } + // catch (Exception e) + // { + // Dalamud.Chat.PrintError("Could not save file:"); + // Dalamud.Chat.PrintError($" {e.Message}"); + // } + //} + // + public void OnGlamour(string command, string arguments) + { + //static void PrintHelp() + //{ + // Dalamud.Chat.Print("Usage:"); + // Dalamud.Chat.Print($" {HelpString}"); + //} + + //arguments = arguments.Trim(); + //if (!arguments.Any()) + //{ + // PrintHelp(); + // return; + //} + // + //var split = arguments.Split(new[] + //{ + // ',', + //}, 3, StringSplitOptions.RemoveEmptyEntries); + // + //if (split.Length < 2) + //{ + // PrintHelp(); + // return; + //} + // + //var player = GetPlayer(split[1]) as Character; + //if (player == null) + //{ + // Dalamud.Chat.Print($"Could not find object for {split[1]} or it was not a Character."); + // return; + //} + // + //switch (split[0].ToLowerInvariant()) + //{ + // case "copy": + // CopyToClipboard(player); + // return; + // case "apply": + // { + // if (split.Length < 3) + // { + // Dalamud.Chat.Print("Applying requires a name for the save to be applied or 'clipboard'."); + // return; + // } + // + // ApplyCommand(player, split[2]); + // + // return; + // } + // case "save": + // { + // if (split.Length < 3) + // { + // Dalamud.Chat.Print("Saving requires a name for the save."); + // return; + // } + // + // SaveCommand(player, split[2]); + // return; + // } + // default: + // PrintHelp(); + // return; + //} + } +} diff --git a/GlamourerOld/GlamourerOld.csproj b/GlamourerOld/GlamourerOld.csproj new file mode 100644 index 0000000..e351791 --- /dev/null +++ b/GlamourerOld/GlamourerOld.csproj @@ -0,0 +1,126 @@ + + + net7.0-windows + preview + x64 + Glamourer + GlamourerOld + 0.2.0.0 + 0.2.0.0 + SoftOtter + Glamourer + Copyright © 2020 + true + Library + 4 + true + enable + bin\$(Configuration)\ + $(MSBuildWarningsAsMessages);MSB3277 + true + false + false + + + + true + full + false + DEBUG;TRACE + + + + pdbonly + true + TRACE + + + + OnOutputUpdated + + + + + + + + + + + + $(AppData)\XIVLauncher\addon\Hooks\dev\ + + + + + $(DalamudLibPath)Dalamud.dll + False + + + $(DalamudLibPath)FFXIVClientStructs.dll + False + + + $(DalamudLibPath)ImGui.NET.dll + False + + + $(DalamudLibPath)ImGuiScene.dll + False + + + $(DalamudLibPath)Lumina.dll + False + + + $(DalamudLibPath)Lumina.Excel.dll + False + + + $(DalamudLibPath)Newtonsoft.Json.dll + False + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + + + $(GitCommitHash) + + + + + + PreserveNewest + + + + + + + \ No newline at end of file diff --git a/Glamourer/Gui/ActorDebug.cs b/GlamourerOld/Gui/ActorDebug.cs similarity index 100% rename from Glamourer/Gui/ActorDebug.cs rename to GlamourerOld/Gui/ActorDebug.cs diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Color.cs b/GlamourerOld/Gui/Customization/CustomizationDrawer.Color.cs similarity index 100% rename from Glamourer/Gui/Customization/CustomizationDrawer.Color.cs rename to GlamourerOld/Gui/Customization/CustomizationDrawer.Color.cs diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs b/GlamourerOld/Gui/Customization/CustomizationDrawer.GenderRace.cs similarity index 100% rename from Glamourer/Gui/Customization/CustomizationDrawer.GenderRace.cs rename to GlamourerOld/Gui/Customization/CustomizationDrawer.GenderRace.cs diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs b/GlamourerOld/Gui/Customization/CustomizationDrawer.Icon.cs similarity index 100% rename from Glamourer/Gui/Customization/CustomizationDrawer.Icon.cs rename to GlamourerOld/Gui/Customization/CustomizationDrawer.Icon.cs diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs b/GlamourerOld/Gui/Customization/CustomizationDrawer.Simple.cs similarity index 100% rename from Glamourer/Gui/Customization/CustomizationDrawer.Simple.cs rename to GlamourerOld/Gui/Customization/CustomizationDrawer.Simple.cs diff --git a/Glamourer/Gui/Customization/CustomizationDrawer.cs b/GlamourerOld/Gui/Customization/CustomizationDrawer.cs similarity index 100% rename from Glamourer/Gui/Customization/CustomizationDrawer.cs rename to GlamourerOld/Gui/Customization/CustomizationDrawer.cs diff --git a/Glamourer/Gui/Designs/DesignFileSystemSelector.cs b/GlamourerOld/Gui/Designs/DesignFileSystemSelector.cs similarity index 100% rename from Glamourer/Gui/Designs/DesignFileSystemSelector.cs rename to GlamourerOld/Gui/Designs/DesignFileSystemSelector.cs diff --git a/Glamourer/Gui/Designs/InterfaceDesigns.cs b/GlamourerOld/Gui/Designs/InterfaceDesigns.cs similarity index 100% rename from Glamourer/Gui/Designs/InterfaceDesigns.cs rename to GlamourerOld/Gui/Designs/InterfaceDesigns.cs diff --git a/Glamourer/Gui/Equipment/EquipmentDrawer.cs b/GlamourerOld/Gui/Equipment/EquipmentDrawer.cs similarity index 100% rename from Glamourer/Gui/Equipment/EquipmentDrawer.cs rename to GlamourerOld/Gui/Equipment/EquipmentDrawer.cs diff --git a/Glamourer/Gui/Equipment/ItemCombo.cs b/GlamourerOld/Gui/Equipment/ItemCombo.cs similarity index 100% rename from Glamourer/Gui/Equipment/ItemCombo.cs rename to GlamourerOld/Gui/Equipment/ItemCombo.cs diff --git a/Glamourer/Gui/Equipment/WeaponCombo.cs b/GlamourerOld/Gui/Equipment/WeaponCombo.cs similarity index 100% rename from Glamourer/Gui/Equipment/WeaponCombo.cs rename to GlamourerOld/Gui/Equipment/WeaponCombo.cs diff --git a/GlamourerOld/Gui/GlamourerWindowSystem.cs b/GlamourerOld/Gui/GlamourerWindowSystem.cs new file mode 100644 index 0000000..dbe1c8c --- /dev/null +++ b/GlamourerOld/Gui/GlamourerWindowSystem.cs @@ -0,0 +1,30 @@ +using System; +using Dalamud.Interface; +using Dalamud.Interface.Windowing; + +namespace Glamourer.Gui; + +public class GlamourerWindowSystem : IDisposable +{ + private readonly WindowSystem _windowSystem = new("Glamourer"); + private readonly UiBuilder _uiBuilder; + private readonly Interface _ui; + + public GlamourerWindowSystem(UiBuilder uiBuilder, Interface ui) + { + _uiBuilder = uiBuilder; + _ui = ui; + _windowSystem.AddWindow(ui); + _uiBuilder.Draw += _windowSystem.Draw; + _uiBuilder.OpenConfigUi += _ui.Toggle; + } + + public void Dispose() + { + _uiBuilder.Draw -= _windowSystem.Draw; + _uiBuilder.OpenConfigUi -= _ui.Toggle; + } + + public void Toggle() + => _ui.Toggle(); +} diff --git a/Glamourer/Gui/Interface.Actors.cs b/GlamourerOld/Gui/Interface.Actors.cs similarity index 100% rename from Glamourer/Gui/Interface.Actors.cs rename to GlamourerOld/Gui/Interface.Actors.cs diff --git a/Glamourer/Gui/Interface.DebugDataTab.cs b/GlamourerOld/Gui/Interface.DebugDataTab.cs similarity index 100% rename from Glamourer/Gui/Interface.DebugDataTab.cs rename to GlamourerOld/Gui/Interface.DebugDataTab.cs diff --git a/Glamourer/Gui/Interface.DebugStateTab.cs b/GlamourerOld/Gui/Interface.DebugStateTab.cs similarity index 100% rename from Glamourer/Gui/Interface.DebugStateTab.cs rename to GlamourerOld/Gui/Interface.DebugStateTab.cs diff --git a/Glamourer/Gui/Interface.DesignTab.cs b/GlamourerOld/Gui/Interface.DesignTab.cs similarity index 100% rename from Glamourer/Gui/Interface.DesignTab.cs rename to GlamourerOld/Gui/Interface.DesignTab.cs diff --git a/Glamourer/Gui/Interface.SettingsTab.cs b/GlamourerOld/Gui/Interface.SettingsTab.cs similarity index 100% rename from Glamourer/Gui/Interface.SettingsTab.cs rename to GlamourerOld/Gui/Interface.SettingsTab.cs diff --git a/Glamourer/Gui/Interface.State.cs b/GlamourerOld/Gui/Interface.State.cs similarity index 100% rename from Glamourer/Gui/Interface.State.cs rename to GlamourerOld/Gui/Interface.State.cs diff --git a/Glamourer/Gui/Interface.cs b/GlamourerOld/Gui/Interface.cs similarity index 100% rename from Glamourer/Gui/Interface.cs rename to GlamourerOld/Gui/Interface.cs diff --git a/Glamourer/Gui/InterfaceActorPanel.cs b/GlamourerOld/Gui/InterfaceActorPanel.cs similarity index 100% rename from Glamourer/Gui/InterfaceActorPanel.cs rename to GlamourerOld/Gui/InterfaceActorPanel.cs diff --git a/Glamourer/Gui/InterfaceEquipment.cs b/GlamourerOld/Gui/InterfaceEquipment.cs similarity index 100% rename from Glamourer/Gui/InterfaceEquipment.cs rename to GlamourerOld/Gui/InterfaceEquipment.cs diff --git a/Glamourer/Gui/InterfaceFixedDesigns.cs b/GlamourerOld/Gui/InterfaceFixedDesigns.cs similarity index 100% rename from Glamourer/Gui/InterfaceFixedDesigns.cs rename to GlamourerOld/Gui/InterfaceFixedDesigns.cs diff --git a/Glamourer/Gui/InterfaceHelpers.cs b/GlamourerOld/Gui/InterfaceHelpers.cs similarity index 100% rename from Glamourer/Gui/InterfaceHelpers.cs rename to GlamourerOld/Gui/InterfaceHelpers.cs diff --git a/Glamourer/Gui/InterfaceInitialization.cs b/GlamourerOld/Gui/InterfaceInitialization.cs similarity index 100% rename from Glamourer/Gui/InterfaceInitialization.cs rename to GlamourerOld/Gui/InterfaceInitialization.cs diff --git a/Glamourer/Gui/InterfaceMiscellaneous.cs b/GlamourerOld/Gui/InterfaceMiscellaneous.cs similarity index 100% rename from Glamourer/Gui/InterfaceMiscellaneous.cs rename to GlamourerOld/Gui/InterfaceMiscellaneous.cs diff --git a/Glamourer/Gui/InterfaceRevertables.cs b/GlamourerOld/Gui/InterfaceRevertables.cs similarity index 100% rename from Glamourer/Gui/InterfaceRevertables.cs rename to GlamourerOld/Gui/InterfaceRevertables.cs diff --git a/Glamourer/Interop/Actor.cs b/GlamourerOld/Interop/Actor.cs similarity index 100% rename from Glamourer/Interop/Actor.cs rename to GlamourerOld/Interop/Actor.cs diff --git a/GlamourerOld/Interop/ChangeCustomizeService.cs b/GlamourerOld/Interop/ChangeCustomizeService.cs new file mode 100644 index 0000000..e770025 --- /dev/null +++ b/GlamourerOld/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/DrawObject.cs b/GlamourerOld/Interop/DrawObject.cs similarity index 100% rename from Glamourer/Interop/DrawObject.cs rename to GlamourerOld/Interop/DrawObject.cs diff --git a/Glamourer/Interop/IDesignable.cs b/GlamourerOld/Interop/IDesignable.cs similarity index 100% rename from Glamourer/Interop/IDesignable.cs rename to GlamourerOld/Interop/IDesignable.cs diff --git a/Glamourer/Interop/JobService.cs b/GlamourerOld/Interop/JobService.cs similarity index 100% rename from Glamourer/Interop/JobService.cs rename to GlamourerOld/Interop/JobService.cs diff --git a/Glamourer/Interop/ObjectManager.cs b/GlamourerOld/Interop/ObjectManager.cs similarity index 100% rename from Glamourer/Interop/ObjectManager.cs rename to GlamourerOld/Interop/ObjectManager.cs diff --git a/Glamourer/Interop/RedrawManager.cs b/GlamourerOld/Interop/RedrawManager.cs similarity index 100% rename from Glamourer/Interop/RedrawManager.cs rename to GlamourerOld/Interop/RedrawManager.cs diff --git a/GlamourerOld/Interop/UpdateSlotService.cs b/GlamourerOld/Interop/UpdateSlotService.cs new file mode 100644 index 0000000..0b34f58 --- /dev/null +++ b/GlamourerOld/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/GlamourerOld/Interop/VisorService.cs b/GlamourerOld/Interop/VisorService.cs new file mode 100644 index 0000000..250ee9f --- /dev/null +++ b/GlamourerOld/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/GlamourerOld/Interop/WeaponService.cs b/GlamourerOld/Interop/WeaponService.cs new file mode 100644 index 0000000..15024b8 --- /dev/null +++ b/GlamourerOld/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/GlamourerOld/LegacyTattoo.raw b/GlamourerOld/LegacyTattoo.raw new file mode 100644 index 0000000000000000000000000000000000000000..c6a347badf9e7dc3a51441e3d27baa70ebb199ae GIT binary patch literal 147456 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≪WJ3q})6tjXUf1Av@*U+q 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 B literal 0 HcmV?d00001 diff --git a/GlamourerOld/Services/BackupService.cs b/GlamourerOld/Services/BackupService.cs new file mode 100644 index 0000000..b98de25 --- /dev/null +++ b/GlamourerOld/Services/BackupService.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.IO; +using OtterGui.Classes; +using OtterGui.Log; + +namespace Glamourer.Services; + +public class BackupService +{ + public BackupService(Logger logger, FilenameService fileNames) + { + var files = GlamourerFiles(fileNames); + Backup.CreateBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files); + } + + /// Collect all relevant files for glamourer configuration. + private static IReadOnlyList GlamourerFiles(FilenameService fileNames) + { + var list = new List(16) + { + new(fileNames.ConfigFile), + new(fileNames.DesignFileSystem), + new(fileNames.MigrationDesignFile), + }; + + list.AddRange(fileNames.Designs()); + + return list; + } +} diff --git a/GlamourerOld/Services/CommandService.cs b/GlamourerOld/Services/CommandService.cs new file mode 100644 index 0000000..602a897 --- /dev/null +++ b/GlamourerOld/Services/CommandService.cs @@ -0,0 +1,36 @@ +using System; +using Dalamud.Game.Command; +using Glamourer.Gui; + +namespace Glamourer.Services; + +public class CommandService : IDisposable +{ + private const string HelpString = "[Copy|Apply|Save],[Name or PlaceHolder],"; + private const string MainCommandString = "/glamourer"; + private const string ApplyCommandString = "/glamour"; + + private readonly CommandManager _commands; + private readonly Interface _interface; + + public CommandService(CommandManager commands, Interface ui) + { + _commands = commands; + _interface = ui; + + _commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." }); + _commands.AddHandler(ApplyCommandString, new CommandInfo(OnGlamour) { HelpMessage = $"Use Glamourer Functions: {HelpString}" }); + } + + public void Dispose() + { + _commands.RemoveHandler(MainCommandString); + _commands.RemoveHandler(ApplyCommandString); + } + + private void OnGlamourer(string command, string arguments) + => _interface.Toggle(); + + private void OnGlamour(string command, string arguments) + { } +} diff --git a/GlamourerOld/Services/FilenameService.cs b/GlamourerOld/Services/FilenameService.cs new file mode 100644 index 0000000..f4e1176 --- /dev/null +++ b/GlamourerOld/Services/FilenameService.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Dalamud.Plugin; +using Glamourer.Designs; + +namespace Glamourer.Services; + +public class FilenameService +{ + public readonly string ConfigDirectory; + public readonly string ConfigFile; + public readonly string DesignFileSystem; + public readonly string MigrationDesignFile; + public readonly string DesignDirectory; + + public FilenameService(DalamudPluginInterface pi) + { + ConfigDirectory = pi.ConfigDirectory.FullName; + ConfigFile = pi.ConfigFile.FullName; + DesignFileSystem = Path.Combine(ConfigDirectory, "sort_order.json"); + MigrationDesignFile = Path.Combine(ConfigDirectory, "Designs.json"); + DesignDirectory = Path.Combine(ConfigDirectory, "designs"); + } + + public IEnumerable Designs() + { + if (!Directory.Exists(DesignDirectory)) + yield break; + + foreach (var file in Directory.EnumerateFiles(DesignDirectory, "*.json", SearchOption.TopDirectoryOnly)) + yield return new FileInfo(file); + } + + public string DesignFile(Design design) + => DesignFile(design.Identifier.ToString()); + + public string DesignFile(string identifier) + => Path.Combine(DesignDirectory, $"{identifier}.json"); +} diff --git a/GlamourerOld/Services/ItemManager.cs b/GlamourerOld/Services/ItemManager.cs new file mode 100644 index 0000000..65f4b13 --- /dev/null +++ b/GlamourerOld/Services/ItemManager.cs @@ -0,0 +1,189 @@ +using System; +using System.Diagnostics; +using System.Linq; +using Dalamud.Data; +using Dalamud.Plugin; +using Dalamud.Utility; +using Lumina.Excel; +using Lumina.Excel.GeneratedSheets; +using Lumina.Text; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Race = Penumbra.GameData.Enums.Race; + +namespace Glamourer.Services; + +public class ItemManager : IDisposable +{ + public const string Nothing = "Nothing"; + public const string SmallClothesNpc = "Smallclothes (NPC)"; + public const ushort SmallClothesNpcModel = 9903; + + private readonly Configuration _config; + public readonly IdentifierService IdentifierService; + public readonly ExcelSheet ItemSheet; + public readonly StainData Stains; + public readonly ItemService ItemService; + public readonly RestrictedGear RestrictedGear; + + public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService, Configuration config) + { + _config = config; + ItemSheet = gameData.GetExcelSheet()!; + IdentifierService = identifierService; + Stains = new StainData(pi, gameData, gameData.Language); + ItemService = itemService; + RestrictedGear = new RestrictedGear(pi, gameData.Language, gameData); + DefaultSword = ItemSheet.GetRow(1601)!; // Weathered Shortsword + } + + public void Dispose() + { + Stains.Dispose(); + RestrictedGear.Dispose(); + } + + public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender) + { + if (_config.UseRestrictedGearProtection) + return RestrictedGear.ResolveRestricted(armor, slot, race, gender); + + return (false, armor); + } + + public readonly Item DefaultSword; + + public static uint NothingId(EquipSlot slot) + => uint.MaxValue - 128 - (uint)slot.ToSlot(); + + public static uint SmallclothesId(EquipSlot slot) + => uint.MaxValue - 256 - (uint)slot.ToSlot(); + + public static uint NothingId(FullEquipType type) + => uint.MaxValue - 384 - (uint)type; + + public static Designs.Item NothingItem(EquipSlot slot) + { + Debug.Assert(slot.IsEquipment() || slot.IsAccessory(), $"Called {nameof(NothingItem)} on {slot}."); + return new Designs.Item(Nothing, NothingId(slot), CharacterArmor.Empty); + } + + public static Designs.Weapon NothingItem(FullEquipType type) + { + Debug.Assert(type.ToSlot() == EquipSlot.OffHand, $"Called {nameof(NothingItem)} on {type}."); + return new Designs.Weapon(Nothing, NothingId(type), CharacterWeapon.Empty, type); + } + + public static Designs.Item SmallClothesItem(EquipSlot slot) + { + Debug.Assert(slot.IsEquipment(), $"Called {nameof(SmallClothesItem)} on {slot}."); + return new Designs.Item(SmallClothesNpc, SmallclothesId(slot), new CharacterArmor(SmallClothesNpcModel, 1, 0)); + } + + public (bool Valid, SetId Id, byte Variant, string ItemName) Resolve(EquipSlot slot, uint itemId, Item? item = null) + { + slot = slot.ToSlot(); + if (itemId == NothingId(slot)) + return (true, 0, 0, Nothing); + if (itemId == SmallclothesId(slot)) + return (true, SmallClothesNpcModel, 1, SmallClothesNpc); + + if (item == null || item.RowId != itemId) + item = ItemSheet.GetRow(itemId); + + if (item == null) + return (false, 0, 0, string.Intern($"Unknown #{itemId}")); + if (item.ToEquipType().ToSlot() != slot) + return (false, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})")); + + return (true, (SetId)item.ModelMain, (byte)(item.ModelMain >> 16), string.Intern(item.Name.ToDalamudString().TextValue)); + } + + public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, Item? item = null) + { + if (item == null || item.RowId != itemId) + item = ItemSheet.GetRow(itemId); + + if (item == null) + return (false, 0, 0, 0, string.Intern($"Unknown #{itemId}"), FullEquipType.Unknown); + + var type = item.ToEquipType(); + if (type.ToSlot() != EquipSlot.MainHand) + return (false, 0, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"), type); + + return (true, (SetId)item.ModelMain, (WeaponType)(item.ModelMain >> 16), (byte)(item.ModelMain >> 32), + string.Intern(item.Name.ToDalamudString().TextValue), type); + } + + public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, + FullEquipType mainType, Item? item = null) + { + var offType = mainType.Offhand(); + if (itemId == NothingId(offType)) + return (true, 0, 0, 0, Nothing, offType); + + if (item == null || item.RowId != itemId) + item = ItemSheet.GetRow(itemId); + + if (item == null) + return (false, 0, 0, 0, string.Intern($"Unknown #{itemId}"), FullEquipType.Unknown); + + + var type = item.ToEquipType(); + if (offType != type) + return (false, 0, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"), type); + + var (m, w, v) = offType.ToSlot() == EquipSlot.MainHand + ? ((SetId)item.ModelSub, (WeaponType)(item.ModelSub >> 16), (byte)(item.ModelSub >> 32)) + : ((SetId)item.ModelMain, (WeaponType)(item.ModelMain >> 16), (byte)(item.ModelMain >> 32)); + + return (true, m, w, v, string.Intern(item.Name.ToDalamudString().TextValue), type); + } + + public (bool Valid, uint ItemId, string ItemName) Identify(EquipSlot slot, SetId id, byte variant) + { + slot = slot.ToSlot(); + if (!slot.IsEquipmentPiece()) + return (false, 0, string.Intern($"Unknown ({id.Value}-{variant})")); + + switch (id.Value) + { + case 0: return (true, NothingId(slot), Nothing); + case SmallClothesNpcModel: return (true, SmallclothesId(slot), SmallClothesNpc); + default: + var item = IdentifierService.AwaitedService.Identify(id, variant, slot).FirstOrDefault(); + return item == null + ? (false, 0, string.Intern($"Unknown ({id.Value}-{variant})")) + : (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue)); + } + } + + public (bool Valid, uint ItemId, string ItemName, FullEquipType Type) Identify(EquipSlot slot, SetId id, WeaponType type, byte variant, + FullEquipType mainhandType = FullEquipType.Unknown) + { + switch (slot) + { + case EquipSlot.MainHand: + { + var item = IdentifierService.AwaitedService.Identify(id, type, variant, slot).FirstOrDefault(); + return item != null + ? (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue), item.ToEquipType()) + : (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), mainhandType); + } + case EquipSlot.OffHand: + { + var weaponType = mainhandType.Offhand(); + if (id.Value == 0) + return (true, NothingId(weaponType), Nothing, weaponType); + + var item = IdentifierService.AwaitedService.Identify(id, type, variant, slot).FirstOrDefault(); + return item != null + ? (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue), item.ToEquipType()) + : (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), + weaponType); + } + default: return (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), FullEquipType.Unknown); + } + } +} diff --git a/Glamourer/Services/SaveService.cs b/GlamourerOld/Services/SaveService.cs similarity index 100% rename from Glamourer/Services/SaveService.cs rename to GlamourerOld/Services/SaveService.cs diff --git a/GlamourerOld/Services/ServiceManager.cs b/GlamourerOld/Services/ServiceManager.cs new file mode 100644 index 0000000..0baacef --- /dev/null +++ b/GlamourerOld/Services/ServiceManager.cs @@ -0,0 +1,80 @@ +using Dalamud.Plugin; +using Glamourer.Api; +using Glamourer.Designs; +using Glamourer.Gui; +using Glamourer.Interop; +using Glamourer.State; +using Microsoft.Extensions.DependencyInjection; +using OtterGui.Classes; +using OtterGui.Log; + +namespace Glamourer.Services; + +public static class ServiceManager +{ + public static ServiceProvider CreateProvider(DalamudPluginInterface pi, Logger log) + { + var services = new ServiceCollection() + .AddSingleton(log) + .AddDalamud(pi) + .AddMeta() + .AddConfig() + .AddPenumbra() + .AddInterop() + .AddGameData() + .AddDesigns() + .AddInterface() + .AddApi(); + + return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); + } + + private static IServiceCollection AddDalamud(this IServiceCollection services, DalamudPluginInterface pi) + { + new DalamudServices(pi).AddServices(services); + return services; + } + + private static IServiceCollection AddMeta(this IServiceCollection services) + => services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + private static IServiceCollection AddConfig(this IServiceCollection services) + => services.AddSingleton() + .AddSingleton(); + + private static IServiceCollection AddPenumbra(this IServiceCollection services) + => services.AddSingleton(); + + private static IServiceCollection AddGameData(this IServiceCollection services) + => services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + private static IServiceCollection AddInterop(this IServiceCollection services) + => services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + private static IServiceCollection AddDesigns(this IServiceCollection services) + => services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + private static IServiceCollection AddInterface(this IServiceCollection services) + => services.AddSingleton() + .AddSingleton(); + + private static IServiceCollection AddApi(this IServiceCollection services) + => services.AddSingleton() + .AddSingleton(); +} diff --git a/GlamourerOld/Services/ServiceWrapper.cs b/GlamourerOld/Services/ServiceWrapper.cs new file mode 100644 index 0000000..8cd16c2 --- /dev/null +++ b/GlamourerOld/Services/ServiceWrapper.cs @@ -0,0 +1,105 @@ +using Dalamud.Data; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState; +using Dalamud.Game.Gui; +using Dalamud.Plugin; +using Penumbra.GameData.Actors; +using System; +using System.Threading.Tasks; +using Dalamud.Game; +using Glamourer.Api; +using Glamourer.Customization; +using Penumbra.GameData.Data; +using Penumbra.GameData; + +namespace Glamourer.Services; + +public abstract class AsyncServiceWrapper +{ + public string Name { get; } + public T? Service { get; private set; } + + public T AwaitedService + { + get + { + _task?.Wait(); + return Service!; + } + } + + public bool Valid + => Service != null && !_isDisposed; + + public event Action? FinishedCreation; + private Task? _task; + + private bool _isDisposed; + + protected AsyncServiceWrapper(string name, Func factory) + { + Name = name; + _task = Task.Run(() => + { + var service = factory(); + if (_isDisposed) + { + if (service is IDisposable d) + d.Dispose(); + } + else + { + Service = service; + Glamourer.Log.Verbose($"[{Name}] Created."); + _task = null; + } + }); + _task.ContinueWith((t, x) => + { + if (!_isDisposed) + FinishedCreation?.Invoke(); + }, null); + } + + public void Dispose() + { + if (_isDisposed) + return; + + _isDisposed = true; + _task = null; + if (Service is IDisposable d) + d.Dispose(); + Glamourer.Log.Verbose($"[{Name}] Disposed."); + } +} + +public sealed class IdentifierService : AsyncServiceWrapper +{ + public IdentifierService(DalamudPluginInterface pi, DataManager data) + : base(nameof(IdentifierService), () => Penumbra.GameData.GameData.GetIdentifier(pi, data)) + { } +} + +public sealed class ItemService : AsyncServiceWrapper +{ + public ItemService(DalamudPluginInterface pi, DataManager gameData) + : base(nameof(ItemService), () => new ItemData(pi, gameData, gameData.Language)) + { } +} + +public sealed class ActorService : AsyncServiceWrapper +{ + public ActorService(DalamudPluginInterface pi, ObjectTable objects, ClientState clientState, Framework framework, DataManager gameData, + GameGui gui, PenumbraAttach penumbra) + : base(nameof(ActorService), + () => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)penumbra.CutsceneParent(idx))) + { } +} + +public sealed class CustomizationService : AsyncServiceWrapper +{ + public CustomizationService(DalamudPluginInterface pi, DataManager gameData) + : base(nameof(CustomizationService), () => CustomizationManager.Create(pi, gameData)) + { } +} \ No newline at end of file diff --git a/Glamourer/State/ActiveDesign.Manager.cs b/GlamourerOld/State/ActiveDesign.Manager.cs similarity index 100% rename from Glamourer/State/ActiveDesign.Manager.cs rename to GlamourerOld/State/ActiveDesign.Manager.cs diff --git a/Glamourer/State/ActiveDesign.cs b/GlamourerOld/State/ActiveDesign.cs similarity index 100% rename from Glamourer/State/ActiveDesign.cs rename to GlamourerOld/State/ActiveDesign.cs diff --git a/Glamourer/State/FixedDesignManager.cs b/GlamourerOld/State/FixedDesignManager.cs similarity index 100% rename from Glamourer/State/FixedDesignManager.cs rename to GlamourerOld/State/FixedDesignManager.cs diff --git a/Glamourer/Util/CharacterExtensions.cs b/GlamourerOld/Util/CharacterExtensions.cs similarity index 100% rename from Glamourer/Util/CharacterExtensions.cs rename to GlamourerOld/Util/CharacterExtensions.cs diff --git a/Glamourer/Util/CustomizeExtensions.cs b/GlamourerOld/Util/CustomizeExtensions.cs similarity index 100% rename from Glamourer/Util/CustomizeExtensions.cs rename to GlamourerOld/Util/CustomizeExtensions.cs