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.
This commit is contained in:
Anna 2020-08-08 16:23:51 -04:00
parent f5378f5c9d
commit e9a2fb11a8
4 changed files with 112 additions and 22 deletions

View File

@ -45,6 +45,8 @@ namespace PeepingTom {
public bool ShowInInstance { get; set; } = false; public bool ShowInInstance { get; set; } = false;
public bool ShowInCutscenes { get; set; } = false; public bool ShowInCutscenes { get; set; } = false;
public int PollFrequency { get; set; } = 100;
public void Initialize(DalamudPluginInterface pluginInterface) { public void Initialize(DalamudPluginInterface pluginInterface) {
this.pi = pluginInterface; this.pi = pluginInterface;
} }

View File

@ -33,6 +33,8 @@ namespace PeepingTom {
this.Interface.Framework.OnUpdateEvent += this.Watcher.OnFrameworkUpdate; this.Interface.Framework.OnUpdateEvent += this.Watcher.OnFrameworkUpdate;
this.Interface.UiBuilder.OnBuildUi += this.DrawUI; this.Interface.UiBuilder.OnBuildUi += this.DrawUI;
this.Interface.UiBuilder.OnOpenConfigUi += this.ConfigUI; this.Interface.UiBuilder.OnOpenConfigUi += this.ConfigUI;
this.Watcher.StartThread();
} }
private void OnCommand(string command, string args) { private void OnCommand(string command, string args) {
@ -46,6 +48,7 @@ namespace PeepingTom {
protected virtual void Dispose(bool includeManaged) { protected virtual void Dispose(bool includeManaged) {
this.hookManager.Dispose(); this.hookManager.Dispose();
this.Interface.Framework.OnUpdateEvent -= this.Watcher.OnFrameworkUpdate; this.Interface.Framework.OnUpdateEvent -= this.Watcher.OnFrameworkUpdate;
this.Watcher.WaitStopThread();
this.Watcher.Dispose(); this.Watcher.Dispose();
this.Interface.UiBuilder.OnBuildUi -= DrawUI; this.Interface.UiBuilder.OnBuildUi -= DrawUI;
this.Interface.UiBuilder.OnOpenConfigUi -= ConfigUI; this.Interface.UiBuilder.OnOpenConfigUi -= ConfigUI;

View File

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace PeepingTom { namespace PeepingTom {
@ -72,7 +73,11 @@ namespace PeepingTom {
if (this.plugin.Config.MarkTargeting) { if (this.plugin.Config.MarkTargeting) {
PlayerCharacter player = this.plugin.Interface.ClientState.LocalPlayer; PlayerCharacter player = this.plugin.Interface.ClientState.LocalPlayer;
if (player != null) { if (player != null) {
IReadOnlyCollection<PlayerCharacter> 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) { foreach (PlayerCharacter targeter in targeting) {
MarkPlayer(targeter, this.plugin.Config.TargetingColour, this.plugin.Config.TargetingSize); MarkPlayer(targeter, this.plugin.Config.TargetingColour, this.plugin.Config.TargetingSize);
} }
@ -254,6 +259,16 @@ namespace PeepingTom {
ImGui.EndTabItem(); 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")) { if (ImGui.BeginTabItem("Debug")) {
bool debugMarkers = this.plugin.Config.DebugMarkers; bool debugMarkers = this.plugin.Config.DebugMarkers;
if (ImGui.Checkbox("Debug markers", ref debugMarkers)) { if (ImGui.Checkbox("Debug markers", ref debugMarkers)) {
@ -310,7 +325,20 @@ namespace PeepingTom {
} }
private void ShowMainWindow() { private void ShowMainWindow() {
IReadOnlyCollection<PlayerCharacter> targeting = this.plugin.Watcher.CurrentTargeters; IReadOnlyCollection<Targeter> 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<int, Actor> 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; ImGuiWindowFlags flags = ImGuiWindowFlags.AlwaysAutoResize;
if (!this.plugin.Config.AllowMovement) { if (!this.plugin.Config.AllowMovement) {
@ -328,8 +356,10 @@ namespace PeepingTom {
// .Take(2)) { // .Take(2)) {
// this.AddEntry(new Targeter(p), p, ref anyHovered); // this.AddEntry(new Targeter(p), p, ref anyHovered);
//} //}
foreach (PlayerCharacter targeter in targeting) { foreach (Targeter targeter in targeting) {
this.AddEntry(new Targeter(targeter), targeter, ref anyHovered); Actor actor = null;
actors?.TryGetValue(targeter.ActorId, out actor);
this.AddEntry(targeter, actor, ref anyHovered);
} }
if (this.plugin.Config.KeepHistory) { if (this.plugin.Config.KeepHistory) {
// get a list of the previous targeters that aren't currently targeting // get a list of the previous targeters that aren't currently targeting
@ -339,7 +369,9 @@ namespace PeepingTom {
.ToArray(); .ToArray();
// add previous targeters to the list // add previous targeters to the list
foreach (Targeter oldTargeter in previous) { 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(); ImGui.ListBoxFooter();
@ -364,6 +396,7 @@ namespace PeepingTom {
bool hover = ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled); bool hover = ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled);
bool left = hover && ImGui.IsMouseClicked(0); bool left = hover && ImGui.IsMouseClicked(0);
bool right = hover && ImGui.IsMouseClicked(1); bool right = hover && ImGui.IsMouseClicked(1);
if (actor == null) { if (actor == null) {
actor = this.plugin.Interface.ClientState.Actors actor = this.plugin.Interface.ClientState.Actors
.Where(a => a.ActorId == targeter.ActorId) .Where(a => a.ActorId == targeter.ActorId)

View File

@ -20,12 +20,19 @@ namespace PeepingTom {
private long soundLastPlayed = 0; private long soundLastPlayed = 0;
private int lastTargetAmount = 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 readonly Mutex currentMutex = new Mutex();
private PlayerCharacter[] current = Array.Empty<PlayerCharacter>(); private Targeter[] current = Array.Empty<Targeter>();
public IReadOnlyCollection<PlayerCharacter> CurrentTargeters { public IReadOnlyCollection<Targeter> CurrentTargeters {
get { get {
this.currentMutex.WaitOne(); this.currentMutex.WaitOne();
PlayerCharacter[] current = (PlayerCharacter[])this.current.Clone(); Targeter[] current = this.current.ToArray();
this.currentMutex.ReleaseMutex(); this.currentMutex.ReleaseMutex();
return current; return current;
} }
@ -52,21 +59,55 @@ namespace PeepingTom {
this.previousMutex.ReleaseMutex(); 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) { public void OnFrameworkUpdate(Framework framework) {
PlayerCharacter player = this.plugin.Interface.ClientState.LocalPlayer; if (!this.needsUpdate) {
if (player == null) {
return; return;
} }
// block until lease lock (this.dataMutex) {
this.currentMutex.WaitOne(); this.data = new TargetThreadData(this.plugin.Interface);
}
this.needsUpdate = false;
}
// get targeters and set a copy so we can release the mutex faster private void Update() {
PlayerCharacter[] current = this.GetTargeting(player); lock (this.dataMutex) {
this.current = (PlayerCharacter[])current.Clone(); if (this.data == null) {
return;
}
// release PlayerCharacter player = this.data.localPlayer;
this.currentMutex.ReleaseMutex(); 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); this.HandleHistory(current);
@ -78,19 +119,19 @@ namespace PeepingTom {
this.lastTargetAmount = this.current.Length; 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)) { if (!this.plugin.Config.KeepHistory || (!this.plugin.Config.HistoryWhenClosed && !this.plugin.Ui.Visible)) {
return; return;
} }
this.previousMutex.WaitOne(); this.previousMutex.WaitOne();
foreach (PlayerCharacter targeter in targeting) { foreach (Targeter targeter in targeting) {
// add the targeter to the previous list // add the targeter to the previous list
if (this.previousTargeters.Any(old => old.ActorId == targeter.ActorId)) { if (this.previousTargeters.Any(old => old.ActorId == targeter.ActorId)) {
this.previousTargeters.RemoveAll(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) // only keep the configured number of previous targeters (ignoring ones that are currently targeting)
@ -101,14 +142,15 @@ namespace PeepingTom {
this.previousMutex.ReleaseMutex(); this.previousMutex.ReleaseMutex();
} }
private PlayerCharacter[] GetTargeting(Actor player) { private Targeter[] GetTargeting(Actor[] actors, Actor player) {
return this.plugin.Interface.ClientState.Actors return actors
.Where(actor => actor.TargetActorID == player.ActorId && actor is PlayerCharacter) .Where(actor => actor.TargetActorID == player.ActorId && actor is PlayerCharacter)
.Select(actor => actor as 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.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.LogAlliance || !this.InAlliance(actor))
.Where(actor => this.plugin.Config.LogInCombat || !this.InCombat(actor)) .Where(actor => this.plugin.Config.LogInCombat || !this.InCombat(actor))
.Where(actor => this.plugin.Config.LogSelf || actor.ActorId != player.ActorId) .Where(actor => this.plugin.Config.LogSelf || actor.ActorId != player.ActorId)
.Select(actor => new Targeter(actor))
.ToArray(); .ToArray();
} }
@ -180,4 +222,14 @@ namespace PeepingTom {
this.previousMutex.Dispose(); 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();
}
}
} }