diff --git a/Brio/Game/Actor/ActorService.cs b/Brio/Game/Actor/ActorService.cs index 982b9ad2..cca7745b 100644 --- a/Brio/Game/Actor/ActorService.cs +++ b/Brio/Game/Actor/ActorService.cs @@ -1,5 +1,6 @@ using Brio.Core; using Brio.Game.Actor.Extensions; +using Brio.Game.Actor.Interop; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Hooking; using System; @@ -23,6 +24,8 @@ public class ActorService : ServiceBase private List _gposeActors = new(); + public AttachmentInterop AttachmentInterop { get; } = new(); + public ActorService() { var destroyAddress = Dalamud.SigScanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D 05 ?? ?? ?? ?? 48 8B D9 48 89 01 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 81 C1"); diff --git a/Brio/Game/Actor/ActorSpawnService.cs b/Brio/Game/Actor/ActorSpawnService.cs index eb1ca29b..ca3ea7d2 100644 --- a/Brio/Game/Actor/ActorSpawnService.cs +++ b/Brio/Game/Actor/ActorSpawnService.cs @@ -64,7 +64,9 @@ private unsafe void ActorService_OnActorDestructing(DalamudGameObject gameObject Character* originalPlayer = (Character*)localPlayer.AsNative(); if(originalPlayer == null) return null; - uint idCheck = com->CreateBattleCharacter(); + bool reserveCompanionSlot = options.HasFlag(SpawnOptions.ReserveCompanionSlot); + + uint idCheck = com->CreateBattleCharacter(param: (byte)(reserveCompanionSlot ? 1 : 0)); if(idCheck == 0xffffffff) return null; ushort newId = (ushort)idCheck; @@ -159,5 +161,6 @@ public override void Dispose() public enum SpawnOptions { None = 0, - ApplyModelPosition = 1 + ApplyModelPosition = 1, + ReserveCompanionSlot = 2, } diff --git a/Brio/Game/Actor/Interop/AttachmentInterop.cs b/Brio/Game/Actor/Interop/AttachmentInterop.cs new file mode 100644 index 00000000..9449bff2 --- /dev/null +++ b/Brio/Game/Actor/Interop/AttachmentInterop.cs @@ -0,0 +1,24 @@ +using Dalamud.Utility.Signatures; +using static FFXIVClientStructs.FFXIV.Client.Game.Character.Character; + +namespace Brio.Game.Actor.Interop; +public unsafe class AttachmentInterop +{ + public delegate void CreateAndSetupMountDelegate(MountContainer* instance, short mountId, uint buddyModelTop, uint buddyModelBody, uint buddyModelLegs, byte buddyStain, byte unk6, byte unk7); + [Signature("E8 ?? ?? ?? ?? 8B 43 ?? 41 89 46", ScanType = ScanType.Text)] + public CreateAndSetupMountDelegate CreateAndSetupMount = null!; + + + public delegate void SetupOrnamentDelegate(OrnamentContainer* instance, short ornamentId, uint param); + [Signature("E8 ?? ?? ?? ?? 48 8B 7B ?? 0F B7 97", ScanType = ScanType.Text)] + public SetupOrnamentDelegate SetupOrnament = null!; + + public delegate void SetupCompanionDelegate(CompanionContainer* instance, short companionId, uint param); + [Signature("E8 ?? ?? ?? ?? 84 C0 74 ?? 66 44 89 7F", ScanType = ScanType.Text)] + public SetupCompanionDelegate SetupCompanion = null!; + + public AttachmentInterop() + { + Dalamud.GameInteropProvider.InitializeFromAttributes(this); + } +} diff --git a/Brio/UI/Components/Actor/ActorSelector.cs b/Brio/UI/Components/Actor/ActorSelector.cs index f63316a4..54d79e52 100644 --- a/Brio/UI/Components/Actor/ActorSelector.cs +++ b/Brio/UI/Components/Actor/ActorSelector.cs @@ -14,7 +14,7 @@ public unsafe void Draw() { var gposeObjects = ActorService.Instance.GPoseActors; - ImGui.Text($"Actors: {gposeObjects.Count}/{ActorService.GPoseActorCount}"); + ImGui.Text($"Actors: {gposeObjects.Count}"); ImGui.PushItemWidth(-1); if(ImGui.BeginListBox("###gpose_actor_list", new Vector2(-1, ImGui.GetTextLineHeight() * 9))) diff --git a/Brio/UI/Components/Actor/ActorTab.cs b/Brio/UI/Components/Actor/ActorTab.cs index 3357bdd6..801211de 100644 --- a/Brio/UI/Components/Actor/ActorTab.cs +++ b/Brio/UI/Components/Actor/ActorTab.cs @@ -11,6 +11,7 @@ namespace Brio.UI.Components.Actor; public static class ActorTab { private static ActorSelector _selector = new ActorSelector(); + private static bool _allowCompanions = true; public unsafe static void Draw() { @@ -30,7 +31,8 @@ public unsafe static void Draw() if(!canSpawn) ImGui.BeginDisabled(); if(ImGui.Button("Spawn###gpose_actor_spawn")) { - ushort? createdId = ActorSpawnService.Instance.Spawn(); + SpawnOptions options = _allowCompanions ? SpawnOptions.ReserveCompanionSlot : SpawnOptions.None; + ushort? createdId = ActorSpawnService.Instance.Spawn(options); if(createdId == null) Dalamud.ToastGui.ShowError("Failed to Create Actor."); } @@ -61,6 +63,10 @@ public unsafe static void Draw() { ActorSpawnService.Instance.DestroyAll(); } + + ImGui.Checkbox("Allow Attachments", ref _allowCompanions); + if(ImGui.IsItemHovered()) + ImGui.SetTooltip("Allow attachments to be attached to spawned actors.\nThis will take two slots instead of one so reduces the total actors you can spawn."); } if(ImGui.CollapsingHeader("Redraw")) @@ -102,6 +108,18 @@ public unsafe static void Draw() } } + if(ImGui.CollapsingHeader("Attachments")) + { + if(_selector.SelectedObject != null) + { + AttachmentControls.Draw(_selector.SelectedObject); + } + else + { + ImGui.Text("No actor selected."); + } + } + if(ImGui.CollapsingHeader("Status Effects")) { if(_selector.SelectedObject != null) diff --git a/Brio/UI/Components/Actor/AttachmentControls.cs b/Brio/UI/Components/Actor/AttachmentControls.cs new file mode 100644 index 00000000..2a3edcc8 --- /dev/null +++ b/Brio/UI/Components/Actor/AttachmentControls.cs @@ -0,0 +1,209 @@ +using Brio.Game.Actor; +using Brio.Game.Actor.Extensions; +using Dalamud.Game.ClientState.Objects.Types; +using ImGuiNET; +using Lumina.Excel.GeneratedSheets; +using System.Linq; + +namespace Brio.UI.Components.Actor; + +// TODO: This whole thing is pretty ugly and there is a lot of business logic in the UI here. +// Once https://github.com/aers/FFXIVClientStructs/pull/708 is merged and available, I should abstract this better. +public unsafe static class AttachmentControls +{ + private static string _searchTerm = string.Empty; + private static int _selectedEntry = 0; + + public static void Draw(GameObject gameObject) + { + if(gameObject is Character managedChara) + { + DrawCharacter(managedChara); + } + else + { + ImGui.Text("Incompatible actor type."); + } + } + + private static void DrawCharacter(Character character) + { + var chara = character.AsNative(); + if(chara->CompanionObject == null) + { + ImGui.Text("Not compatible with attachments."); + return; + } + + if(chara->Mount.MountObject != null) + { + ImGui.Text("Destroy: "); + + ImGui.SameLine(); + + if(ImGui.Button("Mount###attachment_destroy_mount")) + { + ActorService.Instance.AttachmentInterop.CreateAndSetupMount(&chara->Mount, 0, 0, 0, 0, 0, 0, 0); + } + return; + } + + + if(chara->Ornament.OrnamentObject != null) + { + ImGui.Text("Destroy: "); + + ImGui.SameLine(); + + if(ImGui.Button("Ornament###attachment_destroy_ornament")) + { + ActorService.Instance.AttachmentInterop.SetupOrnament(&chara->Ornament, 0, 0); + } + return; + } + + if(chara->Companion.CompanionObject != null) + { + ImGui.Text("Destroy: "); + + ImGui.SameLine(); + + if(ImGui.Button("Minion###attachment_destroy_companion")) + { + ActorService.Instance.AttachmentInterop.SetupCompanion(&chara->Companion, 0, 0); + } + return; + } + + ImGui.Text("Create: "); + + ImGui.SameLine(); + + if(ImGui.Button("Ornament###attachment_create_ornament")) + { + ImGui.OpenPopup("###global_ornament_selector"); + } + + ImGui.SameLine(); + + if(ImGui.Button("Mount###attachment_create_mount")) + { + ImGui.OpenPopup("###global_mount_selector"); + } + + ImGui.SameLine(); + + if(ImGui.Button("Minion###attachment_create_minion")) + { + ImGui.OpenPopup("###global_minion_selector"); + } + + if(ImGui.BeginPopup("###global_ornament_selector")) + { + ImGui.InputText("###global_ornament_search", ref _searchTerm, 64); + + if(ImGui.BeginListBox("###global_ornament_listbox")) + { + var ornamentSheet = Dalamud.DataManager.Excel.GetSheet(); + if(ornamentSheet != null) + { + var list = ornamentSheet.Where(i => !string.IsNullOrEmpty(i.Singular.RawString)).Where((i) => i.Singular.RawString.Contains(_searchTerm, System.StringComparison.CurrentCultureIgnoreCase)).ToList(); + foreach(var ornament in list) + { + if(ImGui.Selectable($"{ornament.Singular} ({ornament.RowId})###global_ornament_{ornament.RowId}", _selectedEntry == ornament.RowId)) + { + _selectedEntry = (int)ornament.RowId; + } + + if(ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left)) + { + ImGui.CloseCurrentPopup(); + + ActorService.Instance.AttachmentInterop.SetupOrnament(&chara->Ornament, (short)_selectedEntry, 0); + + Dalamud.Framework.RunOnTick(() => + { + chara->Ornament.OrnamentObject->Character.GameObject.EnableDraw(); + }, delayTicks: 5); + } + } + } + ImGui.EndListBox(); + } + + ImGui.EndPopup(); + } + + if(ImGui.BeginPopup("###global_mount_selector")) + { + ImGui.InputText("###global_mount_search", ref _searchTerm, 64); + + if(ImGui.BeginListBox("###global_mount_listbox")) + { + var mountSheet = Dalamud.DataManager.Excel.GetSheet(); + if(mountSheet != null) + { + var list = mountSheet.Where(i => !string.IsNullOrEmpty(i.Singular.RawString)).Where((i) => i.Singular.RawString.Contains(_searchTerm, System.StringComparison.CurrentCultureIgnoreCase)).ToList(); + foreach(var mount in list) + { + if(ImGui.Selectable($"{mount.Singular} ({mount.RowId})###global_mount_{mount.RowId}", _selectedEntry == mount.RowId)) + { + _selectedEntry = (int)mount.RowId; + } + + if(ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left)) + { + ImGui.CloseCurrentPopup(); + + ActorService.Instance.AttachmentInterop.CreateAndSetupMount(&chara->Mount, (short)_selectedEntry, 0, 0, 0, 0, 0, 0); + + Dalamud.Framework.RunOnTick(() => + { + chara->Mount.MountObject->GameObject.EnableDraw(); + }, delayTicks: 5); + } + } + } + ImGui.EndListBox(); + } + + ImGui.EndPopup(); + } + + if(ImGui.BeginPopup("###global_minion_selector")) + { + ImGui.InputText("###global_minion_search", ref _searchTerm, 64); + + if(ImGui.BeginListBox("###global_minion_listbox")) + { + var minionSheet = Dalamud.DataManager.Excel.GetSheet(); + if(minionSheet != null) + { + var list = minionSheet.Where(i => !string.IsNullOrEmpty(i.Singular.RawString)).Where((i) => i.Singular.RawString.Contains(_searchTerm, System.StringComparison.CurrentCultureIgnoreCase)).ToList(); + foreach(var minion in list) + { + if(ImGui.Selectable($"{minion.Singular} ({minion.RowId})###global_minion_{minion.RowId}", _selectedEntry == minion.RowId)) + { + _selectedEntry = (int)minion.RowId; + } + + if(ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left)) + { + ImGui.CloseCurrentPopup(); + + ActorService.Instance.AttachmentInterop.SetupCompanion(&chara->Companion, (short)_selectedEntry, 0); + + Dalamud.Framework.RunOnTick(() => + { + chara->Companion.CompanionObject->Character.GameObject.EnableDraw(); + }, delayTicks: 5); + } + } + } + ImGui.EndListBox(); + } + + ImGui.EndPopup(); + } + } +}