diff --git a/Pal.Client/Hooks.cs b/Pal.Client/Hooks.cs new file mode 100644 index 0000000..dca3228 --- /dev/null +++ b/Pal.Client/Hooks.cs @@ -0,0 +1,89 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Memory; +using Dalamud.Utility.Signatures; +using System; +using System.Text; + +namespace Pal.Client +{ + internal unsafe class Hooks + { +#pragma warning disable CS0649 + private delegate nint ActorVfxCreateDelegate(char* a1, nint a2, nint a3, float a4, char a5, ushort a6, char a7); + + [Signature("40 53 55 56 57 48 81 EC ?? ?? ?? ?? 0F 29 B4 24 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 0F B6 AC 24 ?? ?? ?? ?? 0F 28 F3 49 8B F8", DetourName = nameof(ActorVfxCreate))] + private Hook ActorVfxCreateHook { get; init; } = null!; +#pragma warning restore CS0649 + + public Hooks() + { + SignatureHelper.Initialise(this); + ActorVfxCreateHook.Enable(); + } + + /// + /// Even with a pomander of sight, the BattleChara's position for the trap remains at {0, 0, 0} until it is activated. + /// Upon exploding, the trap's position is moved to the exact location that the pomander of sight would have revealed. + /// + /// That exact position appears to be used for VFX playing when you walk into it - even if you barely walk into the + /// outer ring of an otter/luring/impeding/landmine trap, the VFX plays at the exact center and not at your character's + /// location. + /// + /// Especially at higher floors, you're more likely to walk into an undiscovered trap compared to e.g. 51-60, + /// and you probably don't want to/can't use sight on every floor - yet the trap location is still useful information. + /// + /// Some (but not all) chests also count as BattleChara named 'Trap', however the effect upon opening isn't played via + /// ActorVfxCreate even if they explode (but probably as a Vfx with static location, doesn't matter for here). + /// + /// Landmines and luring traps also don't play a VFX attached to their BattleChara. + /// + /// otter: vfx/common/eff/dk05th_stdn0t.avfx
+ /// toading: vfx/common/eff/dk05th_stdn0t.avfx
+ /// enfeebling: vfx/common/eff/dk05th_stdn0t.avfx
+ /// landmine: none
+ /// luring: none
+ /// impeding: vfx/common/eff/dk05ht_ipws0t.avfx (one of silence/pacification)
+ /// impeding: vfx/common/eff/dk05ht_slet0t.avfx (the other of silence/pacification)
+ /// + /// It is of course annoying that, when testing, almost all traps are landmines. + /// There's also vfx/common/eff/dk01gd_inv0h.avfx for e.g. impeding when you're invulnerable, but not sure if that + /// has other trigger conditions. + ///
+ public nint ActorVfxCreate(char* a1, nint a2, nint a3, float a4, char a5, ushort a6, char a7) + { + try + { + if (Service.Plugin.IsInDeepDungeon()) + { + var vfxPath = MemoryHelper.ReadString(new nint(a1), Encoding.ASCII, 256); + var obj = Service.ObjectTable.CreateObjectReference(a2); + + /* + if (Service.Configuration.BetaKey == "VFX") + Service.Chat.Print($"{vfxPath} on {obj}"); + */ + + if (obj is BattleChara bc && (bc.NameId == /* potd */ 5042 || bc.NameId == /* hoh */ 7395)) + { + if (vfxPath == "vfx/common/eff/dk05th_stdn0t.avfx" || vfxPath == "vfx/common/eff/dk05ht_ipws0t.avfx") + { + Service.Plugin.NextUpdateObjects.Enqueue(obj.Address); + } + } + } + } + catch (Exception e) + { + PluginLog.Error(e, "VFX Create Hook failed"); + } + return ActorVfxCreateHook.Original(a1, a2, a3, a4, a5, a6, a7); + } + + public void Dispose() + { + ActorVfxCreateHook?.Dispose(); + } + } +} diff --git a/Pal.Client/LocalState.cs b/Pal.Client/LocalState.cs index d2bff21..9ac55b8 100644 --- a/Pal.Client/LocalState.cs +++ b/Pal.Client/LocalState.cs @@ -62,7 +62,7 @@ namespace Pal.Client localState = new LocalState(territoryType) { - Markers = new ConcurrentBag(save.Markers), + Markers = new ConcurrentBag(save.Markers.Where(o => o.Type != Marker.EType.Debug)), }; version = save.Version; } diff --git a/Pal.Client/Marker.cs b/Pal.Client/Marker.cs index 41f5b8f..9748ab3 100644 --- a/Pal.Client/Marker.cs +++ b/Pal.Client/Marker.cs @@ -53,7 +53,7 @@ namespace Pal.Client /// /// To make rollbacks of local data easier, keep track of the version which was used to write the marker initially. /// - public string SinceVersion { get; set; } + public string? SinceVersion { get; set; } [JsonIgnore] public Element? SplatoonElement { get; set; } @@ -86,7 +86,7 @@ namespace Pal.Client } - public bool IsPermanent() => Type == EType.Trap || Type == EType.Hoard; + public bool IsPermanent() => Type == EType.Trap || Type == EType.Hoard || Type == EType.Debug; public enum EType { @@ -95,6 +95,7 @@ namespace Pal.Client #region Permanent Markers Trap = ObjectType.Trap, Hoard = ObjectType.Hoard, + Debug = ObjectType.Debug, #endregion # region Markers that only show up if they're currently visible diff --git a/Pal.Client/Pal.Client.csproj b/Pal.Client/Pal.Client.csproj index d8e8918..42d4d29 100644 --- a/Pal.Client/Pal.Client.csproj +++ b/Pal.Client/Pal.Client.csproj @@ -3,7 +3,7 @@ net7.0-windows 11.0 - 2.3 + 2.4 enable diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index a7ec563..466a8c1 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -41,6 +41,7 @@ namespace Pal.Client { Marker.EType.Trap, new MarkerConfig { Radius = 1.7f } }, { Marker.EType.Hoard, new MarkerConfig { Radius = 1.7f, OffsetY = -0.03f } }, { Marker.EType.SilverCoffer, new MarkerConfig { Radius = 1f } }, + { Marker.EType.Debug, new MarkerConfig { Radius = 1.5f } }, }; private LocalizedChatMessages _localizedChatMessages = new(); @@ -53,6 +54,7 @@ namespace Pal.Client public string? DebugMessage { get; set; } internal Queue EarlyEventQueue { get; } = new(); internal Queue LateEventQueue { get; } = new(); + internal ConcurrentQueue NextUpdateObjects { get; } = new(); public string Name => "Palace Pal"; @@ -79,6 +81,7 @@ namespace Pal.Client Service.Plugin = this; Service.Configuration = (Configuration?)pluginInterface.GetPluginConfig() ?? pluginInterface.Create()!; Service.Configuration.Migrate(); + Service.Hooks = new Hooks(); var agreementWindow = pluginInterface.Create(); if (agreementWindow is not null) @@ -174,6 +177,10 @@ namespace Pal.Client DebugNearest(m => m.Type == Marker.EType.Hoard); break; + case "dnear": + DebugNearest(m => m.Type == Marker.EType.Debug); + break; + default: Service.Chat.PrintError($"[Palace Pal] Unknown sub-command '{arguments}' for '{command}'."); break; @@ -199,6 +206,7 @@ namespace Pal.Client Service.WindowSystem.RemoveAllWindows(); Service.RemoteApi.Dispose(); + Service.Hooks.Dispose(); try { @@ -274,6 +282,7 @@ namespace Pal.Client { LastTerritory = Service.ClientState.TerritoryType; TerritorySyncState = SyncState.NotAttempted; + NextUpdateObjects.Clear(); if (IsInDeepDungeon()) GetFloorMarkers(LastTerritory); @@ -414,6 +423,12 @@ namespace Pal.Client marker.SplatoonElement = element; elements.Add(element); } + else if (marker.Type == Marker.EType.Debug && Service.Configuration.BetaKey == "VFX") + { + var element = CreateSplatoonElement(marker.Type, marker.Position, DetermineColor(marker, visibleMarkers)); + marker.SplatoonElement = element; + elements.Add(element); + } } } @@ -444,13 +459,15 @@ namespace Pal.Client else return COLOR_INVISIBLE; } - else + else if (marker.Type == Marker.EType.Hoard) { if (PomanderOfIntuition == PomanderState.Inactive || !Service.Configuration.OnlyVisibleHoardAfterPomander || visibleMarkers.Any(x => x == marker)) return ImGui.ColorConvertFloat4ToU32(Service.Configuration.HoardColor); else return COLOR_INVISIBLE; } + else + return ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0.5f, 1, 0.4f)); } private void HandleEphemeralMarkers(IList visibleMarkers, bool recreateLayout) @@ -596,7 +613,7 @@ namespace Pal.Client var playerPosition = Service.ClientState.LocalPlayer?.Position; if (playerPosition == null) return; - Service.Chat.Print($"[Pal] {playerPosition}"); + Service.Chat.Print($"[Palace Pal] {playerPosition}"); var nearbyMarkers = state.Markers .Where(m => predicate(m)) @@ -641,6 +658,13 @@ namespace Pal.Client } } + while (NextUpdateObjects.TryDequeue(out nint address)) + { + var obj = Service.ObjectTable.FirstOrDefault(x => x.Address == address); + if (obj != null && obj.Position.Length() > 0.1) + result.Add(new Marker(Marker.EType.Debug, obj.Position) { Seen = true }); + } + return result; } diff --git a/Pal.Client/Service.cs b/Pal.Client/Service.cs index b2a88e7..5db8122 100644 --- a/Pal.Client/Service.cs +++ b/Pal.Client/Service.cs @@ -27,5 +27,6 @@ namespace Pal.Client public static WindowSystem WindowSystem { get; set; } = new(typeof(Service).AssemblyQualifiedName); internal static RemoteApi RemoteApi { get; set; } = new RemoteApi(); public static Configuration Configuration { get; set; } = null!; + internal static Hooks Hooks { get; set; } = null!; } } diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index 8196cb1..8456dcd 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -306,6 +306,11 @@ namespace Pal.Client.Windows int silverCoffers = plugin.EphemeralMarkers.Count(x => x.Type == Marker.EType.SilverCoffer); ImGui.Text($"{silverCoffers} silver coffer{(silverCoffers == 1 ? "" : "s")} visible on current floor"); } + if (Service.Configuration.BetaKey == "VFX") + { + int debugMarkers = currentFloor.Markers.Count(x => x.Type == Marker.EType.Debug); + ImGui.Text($"{debugMarkers} debug marker{(debugMarkers == 1 ? "" : "s")}"); + } ImGui.Text($"Pomander of Sight: {plugin.PomanderOfSight}"); ImGui.Text($"Pomander of Intuition: {plugin.PomanderOfIntuition}");