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 ShowInCutscenes { get; set; } = false;
public int PollFrequency { get; set; } = 100;
public void Initialize(DalamudPluginInterface pluginInterface) {
this.pi = pluginInterface;
}

View File

@ -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;

View File

@ -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<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) {
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<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;
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)

View File

@ -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<PlayerCharacter>();
public IReadOnlyCollection<PlayerCharacter> CurrentTargeters {
private Targeter[] current = Array.Empty<Targeter>();
public IReadOnlyCollection<Targeter> 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();
}
}
}