From e9a2fb11a82a1c1f908e8e2ceb09f700a3814b5c Mon Sep 17 00:00:00 2001 From: Anna Date: Sat, 8 Aug 2020 16:23:51 -0400 Subject: [PATCH] feat: use a thread to scan for targeters Also make the poll interval configurable. For the case of one targeter, the UI scans through the ActorTable to find the relevant Actor, achieving either a partial or full scan. In the case of more than one targeter, the UI constructs a Dictionary mapping ActorId to Actor, then indexes that dictionary for each targeter. This achieves one full scan, which may or may not be more efficient than multiple partial scans, depending on where the actors are located in the table. The UI must do this because current targeters are no longer guaranteed to be spawned anymore, especially with a high polling frequency, and the UI needs accurate information about the targeters' address in memory. --- Peeping Tom/Configuration.cs | 2 + Peeping Tom/Plugin.cs | 3 ++ Peeping Tom/PluginUI.cs | 43 +++++++++++++++--- Peeping Tom/TargetWatcher.cs | 86 +++++++++++++++++++++++++++++------- 4 files changed, 112 insertions(+), 22 deletions(-) diff --git a/Peeping Tom/Configuration.cs b/Peeping Tom/Configuration.cs index 8878be0..8c01622 100644 --- a/Peeping Tom/Configuration.cs +++ b/Peeping Tom/Configuration.cs @@ -45,6 +45,8 @@ namespace PeepingTom { public bool ShowInInstance { get; set; } = false; public bool ShowInCutscenes { get; set; } = false; + public int PollFrequency { get; set; } = 100; + public void Initialize(DalamudPluginInterface pluginInterface) { this.pi = pluginInterface; } diff --git a/Peeping Tom/Plugin.cs b/Peeping Tom/Plugin.cs index 3321525..6f90acc 100644 --- a/Peeping Tom/Plugin.cs +++ b/Peeping Tom/Plugin.cs @@ -33,6 +33,8 @@ namespace PeepingTom { this.Interface.Framework.OnUpdateEvent += this.Watcher.OnFrameworkUpdate; this.Interface.UiBuilder.OnBuildUi += this.DrawUI; this.Interface.UiBuilder.OnOpenConfigUi += this.ConfigUI; + + this.Watcher.StartThread(); } private void OnCommand(string command, string args) { @@ -46,6 +48,7 @@ namespace PeepingTom { protected virtual void Dispose(bool includeManaged) { this.hookManager.Dispose(); this.Interface.Framework.OnUpdateEvent -= this.Watcher.OnFrameworkUpdate; + this.Watcher.WaitStopThread(); this.Watcher.Dispose(); this.Interface.UiBuilder.OnBuildUi -= DrawUI; this.Interface.UiBuilder.OnOpenConfigUi -= ConfigUI; diff --git a/Peeping Tom/PluginUI.cs b/Peeping Tom/PluginUI.cs index 83925ed..05bf364 100644 --- a/Peeping Tom/PluginUI.cs +++ b/Peeping Tom/PluginUI.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Reflection; using System.Runtime.InteropServices; namespace PeepingTom { @@ -72,7 +73,11 @@ namespace PeepingTom { if (this.plugin.Config.MarkTargeting) { PlayerCharacter player = this.plugin.Interface.ClientState.LocalPlayer; if (player != null) { - IReadOnlyCollection targeting = this.plugin.Watcher.CurrentTargeters; + PlayerCharacter[] targeting = this.plugin.Watcher.CurrentTargeters + .Select(targeter => this.plugin.Interface.ClientState.Actors.FirstOrDefault(actor => actor.ActorId == targeter.ActorId)) + .Where(targeter => targeter != null) + .Select(targeter => targeter as PlayerCharacter) + .ToArray(); foreach (PlayerCharacter targeter in targeting) { MarkPlayer(targeter, this.plugin.Config.TargetingColour, this.plugin.Config.TargetingSize); } @@ -254,6 +259,16 @@ namespace PeepingTom { ImGui.EndTabItem(); } + if (ImGui.BeginTabItem("Advanced")) { + int pollFrequency = this.plugin.Config.PollFrequency; + if (ImGui.DragInt("Poll frequency in milliseconds", ref pollFrequency, .1f, 1, 1600)) { + this.plugin.Config.PollFrequency = pollFrequency; + this.plugin.Config.Save(); + } + + ImGui.EndTabItem(); + } + if (ImGui.BeginTabItem("Debug")) { bool debugMarkers = this.plugin.Config.DebugMarkers; if (ImGui.Checkbox("Debug markers", ref debugMarkers)) { @@ -310,7 +325,20 @@ namespace PeepingTom { } private void ShowMainWindow() { - IReadOnlyCollection targeting = this.plugin.Watcher.CurrentTargeters; + IReadOnlyCollection targeting = this.plugin.Watcher.CurrentTargeters; + + // to prevent looping over a subset of the actors repeatedly when multiple people are targeting, + // create a dictionary for O(1) lookups by actor id + Dictionary actors = null; + if (targeting.Count > 1) { + FieldInfo field = typeof(Actor).GetField("actorStruct", BindingFlags.Instance | BindingFlags.NonPublic); + actors = this.plugin.Interface.ClientState.Actors + // only take into account players + .Where(actor => actor.ObjectKind == Dalamud.Game.ClientState.Actors.ObjectKind.Player) + // filter out minions + .Where(actor => ((Dalamud.Game.ClientState.Structs.Actor)field.GetValue(actor)).PlayerTargetStatus == 2) + .ToDictionary(actor => actor.ActorId); + } ImGuiWindowFlags flags = ImGuiWindowFlags.AlwaysAutoResize; if (!this.plugin.Config.AllowMovement) { @@ -328,8 +356,10 @@ namespace PeepingTom { // .Take(2)) { // this.AddEntry(new Targeter(p), p, ref anyHovered); //} - foreach (PlayerCharacter targeter in targeting) { - this.AddEntry(new Targeter(targeter), targeter, ref anyHovered); + foreach (Targeter targeter in targeting) { + Actor actor = null; + actors?.TryGetValue(targeter.ActorId, out actor); + this.AddEntry(targeter, actor, ref anyHovered); } if (this.plugin.Config.KeepHistory) { // get a list of the previous targeters that aren't currently targeting @@ -339,7 +369,9 @@ namespace PeepingTom { .ToArray(); // add previous targeters to the list foreach (Targeter oldTargeter in previous) { - this.AddEntry(oldTargeter, null, ref anyHovered, ImGuiSelectableFlags.Disabled); + Actor actor = null; + actors?.TryGetValue(oldTargeter.ActorId, out actor); + this.AddEntry(oldTargeter, actor, ref anyHovered, ImGuiSelectableFlags.Disabled); } } ImGui.ListBoxFooter(); @@ -364,6 +396,7 @@ namespace PeepingTom { bool hover = ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled); bool left = hover && ImGui.IsMouseClicked(0); bool right = hover && ImGui.IsMouseClicked(1); + if (actor == null) { actor = this.plugin.Interface.ClientState.Actors .Where(a => a.ActorId == targeter.ActorId) diff --git a/Peeping Tom/TargetWatcher.cs b/Peeping Tom/TargetWatcher.cs index d18117e..d7d8276 100644 --- a/Peeping Tom/TargetWatcher.cs +++ b/Peeping Tom/TargetWatcher.cs @@ -20,12 +20,19 @@ namespace PeepingTom { private long soundLastPlayed = 0; private int lastTargetAmount = 0; + private volatile bool stop = false; + private volatile bool needsUpdate = true; + private Thread thread; + + private readonly object dataMutex = new object(); + private TargetThreadData data; + private readonly Mutex currentMutex = new Mutex(); - private PlayerCharacter[] current = Array.Empty(); - public IReadOnlyCollection CurrentTargeters { + private Targeter[] current = Array.Empty(); + public IReadOnlyCollection CurrentTargeters { get { this.currentMutex.WaitOne(); - PlayerCharacter[] current = (PlayerCharacter[])this.current.Clone(); + Targeter[] current = this.current.ToArray(); this.currentMutex.ReleaseMutex(); return current; } @@ -52,21 +59,55 @@ namespace PeepingTom { this.previousMutex.ReleaseMutex(); } + public void StartThread() { + this.thread = new Thread(new ThreadStart(() => { + while (!this.stop) { + this.Update(); + this.needsUpdate = true; + Thread.Sleep(this.plugin.Config.PollFrequency); + } + })); + this.thread.Start(); + } + + public void WaitStopThread() { + this.stop = true; + this.thread?.Join(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "delegate")] public void OnFrameworkUpdate(Framework framework) { - PlayerCharacter player = this.plugin.Interface.ClientState.LocalPlayer; - if (player == null) { + if (!this.needsUpdate) { return; } - // block until lease - this.currentMutex.WaitOne(); + lock (this.dataMutex) { + this.data = new TargetThreadData(this.plugin.Interface); + } + this.needsUpdate = false; + } - // get targeters and set a copy so we can release the mutex faster - PlayerCharacter[] current = this.GetTargeting(player); - this.current = (PlayerCharacter[])current.Clone(); + private void Update() { + lock (this.dataMutex) { + if (this.data == null) { + return; + } - // release - this.currentMutex.ReleaseMutex(); + PlayerCharacter player = this.data.localPlayer; + if (player == null) { + return; + } + + // block until lease + this.currentMutex.WaitOne(); + + // get targeters and set a copy so we can release the mutex faster + Targeter[] current = this.GetTargeting(this.data.actors, player); + this.current = (Targeter[])current.Clone(); + + // release + this.currentMutex.ReleaseMutex(); + } this.HandleHistory(current); @@ -78,19 +119,19 @@ namespace PeepingTom { this.lastTargetAmount = this.current.Length; } - private void HandleHistory(PlayerCharacter[] targeting) { + private void HandleHistory(Targeter[] targeting) { if (!this.plugin.Config.KeepHistory || (!this.plugin.Config.HistoryWhenClosed && !this.plugin.Ui.Visible)) { return; } this.previousMutex.WaitOne(); - foreach (PlayerCharacter targeter in targeting) { + foreach (Targeter targeter in targeting) { // add the targeter to the previous list if (this.previousTargeters.Any(old => old.ActorId == targeter.ActorId)) { this.previousTargeters.RemoveAll(old => old.ActorId == targeter.ActorId); } - this.previousTargeters.Insert(0, new Targeter(targeter)); + this.previousTargeters.Insert(0, targeter); } // only keep the configured number of previous targeters (ignoring ones that are currently targeting) @@ -101,14 +142,15 @@ namespace PeepingTom { this.previousMutex.ReleaseMutex(); } - private PlayerCharacter[] GetTargeting(Actor player) { - return this.plugin.Interface.ClientState.Actors + private Targeter[] GetTargeting(Actor[] actors, Actor player) { + return actors .Where(actor => actor.TargetActorID == player.ActorId && actor is PlayerCharacter) .Select(actor => actor as PlayerCharacter) .Where(actor => this.plugin.Config.LogParty || this.plugin.Interface.ClientState.PartyList.All(member => member.Actor?.ActorId != actor.ActorId)) .Where(actor => this.plugin.Config.LogAlliance || !this.InAlliance(actor)) .Where(actor => this.plugin.Config.LogInCombat || !this.InCombat(actor)) .Where(actor => this.plugin.Config.LogSelf || actor.ActorId != player.ActorId) + .Select(actor => new Targeter(actor)) .ToArray(); } @@ -180,4 +222,14 @@ namespace PeepingTom { this.previousMutex.Dispose(); } } + + class TargetThreadData { + public PlayerCharacter localPlayer; + public Actor[] actors; + + public TargetThreadData(DalamudPluginInterface pi) { + this.localPlayer = pi.ClientState.LocalPlayer; + this.actors = pi.ClientState.Actors.ToArray(); + } + } }