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(); + } + } }