diff --git a/Peeping Tom/Peeping Tom.csproj b/Peeping Tom/Peeping Tom.csproj
index 34e890b..08c28dd 100644
--- a/Peeping Tom/Peeping Tom.csproj
+++ b/Peeping Tom/Peeping Tom.csproj
@@ -72,6 +72,7 @@
True
Resources.resx
+
diff --git a/Peeping Tom/Plugin.cs b/Peeping Tom/Plugin.cs
index e670ebf..abad079 100644
--- a/Peeping Tom/Plugin.cs
+++ b/Peeping Tom/Plugin.cs
@@ -10,12 +10,14 @@ namespace PeepingTom {
private Configuration config;
private PluginUI ui;
private HookManager hookManager;
+ private TargetWatcher watcher;
public void Initialize(DalamudPluginInterface pluginInterface) {
this.pi = pluginInterface ?? throw new ArgumentNullException(nameof(pluginInterface), "DalamudPluginInterface argument was null");
this.config = this.pi.GetPluginConfig() as Configuration ?? new Configuration();
this.config.Initialize(this.pi);
- this.ui = new PluginUI(this, this.config, this.pi);
+ this.watcher = new TargetWatcher(this.pi, this.config);
+ this.ui = new PluginUI(this, this.config, this.pi, this.watcher);
this.hookManager = new HookManager(this.pi, this.ui, this.config);
this.pi.CommandManager.AddHandler("/ppeepingtom", new CommandInfo(this.OnCommand) {
@@ -28,6 +30,7 @@ namespace PeepingTom {
HelpMessage = "Alias for /ppeepingtom"
});
+ this.pi.Framework.OnUpdateEvent += this.watcher.OnFrameworkUpdate;
this.pi.UiBuilder.OnBuildUi += this.DrawUI;
this.pi.UiBuilder.OnOpenConfigUi += this.ConfigUI;
}
@@ -42,6 +45,8 @@ namespace PeepingTom {
protected virtual void Dispose(bool includeManaged) {
this.hookManager.Dispose();
+ this.pi.Framework.OnUpdateEvent -= this.watcher.OnFrameworkUpdate;
+ this.watcher.Dispose();
this.pi.UiBuilder.OnBuildUi -= DrawUI;
this.pi.UiBuilder.OnOpenConfigUi -= ConfigUI;
this.pi.CommandManager.RemoveHandler("/ppeepingtom");
diff --git a/Peeping Tom/PluginUI.cs b/Peeping Tom/PluginUI.cs
index bf21a6c..248a717 100644
--- a/Peeping Tom/PluginUI.cs
+++ b/Peeping Tom/PluginUI.cs
@@ -19,11 +19,9 @@ namespace PeepingTom {
private readonly PeepingTomPlugin plugin;
private readonly Configuration config;
private readonly DalamudPluginInterface pi;
+ private readonly TargetWatcher watcher;
- private readonly List previousTargeters = new List();
private Optional previousFocus = new Optional();
- private long soundLastPlayed = 0;
- private int lastTargetAmount = 0;
private bool visible = false;
public bool Visible {
@@ -37,16 +35,16 @@ namespace PeepingTom {
set { this.settingsVisible = value; }
}
- public PluginUI(PeepingTomPlugin plugin, Configuration config, DalamudPluginInterface pluginInterface) {
+ public PluginUI(PeepingTomPlugin plugin, Configuration config, DalamudPluginInterface pluginInterface, TargetWatcher watcher) {
this.plugin = plugin;
this.config = config;
this.pi = pluginInterface;
+ this.watcher = watcher;
}
public void Dispose() {
this.Visible = false;
this.SettingsVisible = false;
- this.previousTargeters.Clear();
}
public void Draw() {
@@ -83,7 +81,7 @@ namespace PeepingTom {
if (this.config.MarkTargeting) {
PlayerCharacter player = this.pi.ClientState.LocalPlayer;
if (player != null) {
- PlayerCharacter[] targeting = this.GetTargeting(player);
+ IReadOnlyCollection targeting = this.watcher.CurrentTargeters;
foreach (PlayerCharacter targeter in targeting) {
MarkPlayer(targeter, this.config.TargetingColour, this.config.TargetingSize);
}
@@ -309,17 +307,8 @@ namespace PeepingTom {
}
private void ShowMainWindow() {
- PlayerCharacter player = this.pi.ClientState.LocalPlayer;
- if (player == null) {
- return;
- }
-
- PlayerCharacter[] targeting = this.GetTargeting(player);
- if (this.config.PlaySoundOnTarget && targeting.Length > this.lastTargetAmount && this.CanPlaySound()) {
- this.soundLastPlayed = Stopwatch.GetTimestamp();
- this.PlaySound();
- }
- this.lastTargetAmount = targeting.Length;
+ IReadOnlyCollection targeting = this.watcher.CurrentTargeters;
+
ImGuiWindowFlags flags = ImGuiWindowFlags.AlwaysAutoResize;
if (!this.config.AllowMovement) {
flags |= ImGuiWindowFlags.NoMove;
@@ -327,7 +316,7 @@ namespace PeepingTom {
if (ImGui.Begin(this.plugin.Name, ref this.visible, flags)) {
ImGui.Text("Targeting you");
bool anyHovered = false;
- if (ImGui.ListBoxHeader("##targeting", targeting.Length, 5)) {
+ if (ImGui.ListBoxHeader("##targeting", targeting.Count, 5)) {
// add the two first players for testing
//foreach (PlayerCharacter p in this.pi.ClientState.Actors
// .Where(actor => actor is PlayerCharacter)
@@ -337,22 +326,11 @@ namespace PeepingTom {
// this.AddEntry(new Targeter(p), p, ref anyHovered);
//}
foreach (PlayerCharacter targeter in targeting) {
- if (this.config.KeepHistory) {
- // 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.AddEntry(new Targeter(targeter), targeter, ref anyHovered);
}
if (this.config.KeepHistory) {
- // only keep the configured number of previous targeters (ignoring ones that are currently targeting)
- while (this.previousTargeters.Where(old => targeting.All(actor => actor.ActorId != old.ActorId)).Count() > this.config.NumHistory) {
- this.previousTargeters.RemoveAt(this.previousTargeters.Count - 1);
- }
// get a list of the previous targeters that aren't currently targeting
- Targeter[] previous = this.previousTargeters
+ Targeter[] previous = this.watcher.PreviousTargeters
.Where(old => targeting.All(actor => actor.ActorId != old.ActorId))
.Take(this.config.NumHistory)
.ToArray();
@@ -458,67 +436,5 @@ namespace PeepingTom {
.Select(actor => actor as PlayerCharacter)
.FirstOrDefault();
}
-
- private bool CanPlaySound() {
- if (this.soundLastPlayed == 0) {
- return true;
- }
-
- long current = Stopwatch.GetTimestamp();
- long diff = current - this.soundLastPlayed;
- // only play every 10 seconds?
- float secs = (float)diff / Stopwatch.Frequency;
- return secs >= this.config.SoundCooldown;
- }
-
- private void PlaySound() {
- SoundPlayer player;
- if (this.config.SoundPath == null) {
- player = new SoundPlayer(Properties.Resources.Target);
- } else {
- player = new SoundPlayer(this.config.SoundPath);
- }
- using (player) {
- try {
- player.Play();
- } catch (FileNotFoundException e) {
- this.SendError($"Could not play sound: {e.Message}");
- } catch (InvalidOperationException e) {
- this.SendError($"Could not play sound: {e.Message}");
- }
- }
- }
-
- private void SendError(string message) {
- Payload[] payloads = { new TextPayload($"[Who's Looking] {message}") };
- this.pi.Framework.Gui.Chat.PrintChat(new XivChatEntry {
- MessageBytes = new SeString(payloads).Encode(),
- Type = XivChatType.ErrorMessage
- });
- }
-
- private byte GetStatus(Actor actor) {
- IntPtr statusPtr = this.pi.TargetModuleScanner.ResolveRelativeAddress(actor.Address, 0x1901);
- return Marshal.ReadByte(statusPtr);
- }
-
- private bool InCombat(Actor actor) {
- return (GetStatus(actor) & 2) > 0;
- }
-
- private bool InAlliance(Actor actor) {
- return (GetStatus(actor) & 32) > 0;
- }
-
- private PlayerCharacter[] GetTargeting(Actor player) {
- return this.pi.ClientState.Actors
- .Where(actor => actor.TargetActorID == player.ActorId && actor is PlayerCharacter)
- .Select(actor => actor as PlayerCharacter)
- .Where(actor => this.config.LogParty || this.pi.ClientState.PartyList.All(member => member.Actor?.ActorId != actor.ActorId))
- .Where(actor => this.config.LogAlliance || !this.InAlliance(actor))
- .Where(actor => this.config.LogInCombat || !this.InCombat(actor))
- .Where(actor => this.config.LogSelf || actor.ActorId != player.ActorId)
- .ToArray();
- }
}
}
diff --git a/Peeping Tom/TargetWatcher.cs b/Peeping Tom/TargetWatcher.cs
new file mode 100644
index 0000000..4397802
--- /dev/null
+++ b/Peeping Tom/TargetWatcher.cs
@@ -0,0 +1,172 @@
+using Dalamud.Game.Chat;
+using Dalamud.Game.Chat.SeStringHandling;
+using Dalamud.Game.Chat.SeStringHandling.Payloads;
+using Dalamud.Game.ClientState.Actors.Types;
+using Dalamud.Game.Internal;
+using Dalamud.Plugin;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Media;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace PeepingTom {
+ class TargetWatcher : IDisposable {
+ private readonly DalamudPluginInterface pi;
+ private readonly Configuration config;
+
+ private long soundLastPlayed = 0;
+ private int lastTargetAmount = 0;
+
+ public readonly Mutex currentMutex = new Mutex();
+ private PlayerCharacter[] current = Array.Empty();
+ public IReadOnlyCollection CurrentTargeters {
+ get {
+ this.currentMutex.WaitOne();
+ PlayerCharacter[] current = (PlayerCharacter[])this.current.Clone();
+ this.currentMutex.ReleaseMutex();
+ return current;
+ }
+ }
+
+ public readonly Mutex previousMutex = new Mutex();
+ private readonly List previousTargeters = new List();
+ public IReadOnlyCollection PreviousTargeters {
+ get {
+ this.previousMutex.WaitOne();
+ Targeter[] previous = this.previousTargeters.ToArray();
+ this.previousMutex.ReleaseMutex();
+ return previous;
+ }
+ }
+
+ public TargetWatcher(DalamudPluginInterface pi, Configuration config) {
+ this.pi = pi ?? throw new ArgumentNullException(nameof(pi), "DalamudPluginInterface cannot be null");
+ this.config = config ?? throw new ArgumentNullException(nameof(config), "Configuration cannot be null");
+ }
+
+ public Out WithCurrent(Func, Out> func) {
+ this.currentMutex.WaitOne();
+ Out output = func(this.current);
+ this.currentMutex.ReleaseMutex();
+ return output;
+ }
+
+ public void OnFrameworkUpdate(Framework framework) {
+ PlayerCharacter player = this.pi.ClientState.LocalPlayer;
+ if (player == null) {
+ return;
+ }
+
+ // block until lease
+ this.currentMutex.WaitOne();
+
+ // get targeters and set a copy so we can release the mutex faster
+ PlayerCharacter[] current = this.GetTargeting(player);
+ this.current = (PlayerCharacter[])current.Clone();
+
+ // release
+ this.currentMutex.ReleaseMutex();
+
+ this.HandleHistory(current);
+
+ // play sound if necessary
+ if (this.config.PlaySoundOnTarget && this.current.Length > this.lastTargetAmount && this.CanPlaySound()) {
+ this.soundLastPlayed = Stopwatch.GetTimestamp();
+ this.PlaySound();
+ }
+ this.lastTargetAmount = this.current.Length;
+ }
+
+ private void HandleHistory(PlayerCharacter[] targeting) {
+ if (!this.config.KeepHistory) {
+ return;
+ }
+
+ foreach (PlayerCharacter 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));
+ }
+
+ // only keep the configured number of previous targeters (ignoring ones that are currently targeting)
+ while (this.previousTargeters.Where(old => targeting.All(actor => actor.ActorId != old.ActorId)).Count() > this.config.NumHistory) {
+ this.previousTargeters.RemoveAt(this.previousTargeters.Count - 1);
+ }
+ }
+
+ private PlayerCharacter[] GetTargeting(Actor player) {
+ return this.pi.ClientState.Actors
+ .Where(actor => actor.TargetActorID == player.ActorId && actor is PlayerCharacter)
+ .Select(actor => actor as PlayerCharacter)
+ .Where(actor => this.config.LogParty || this.pi.ClientState.PartyList.All(member => member.Actor?.ActorId != actor.ActorId))
+ .Where(actor => this.config.LogAlliance || !this.InAlliance(actor))
+ .Where(actor => this.config.LogInCombat || !this.InCombat(actor))
+ .Where(actor => this.config.LogSelf || actor.ActorId != player.ActorId)
+ .ToArray();
+ }
+
+ private byte GetStatus(Actor actor) {
+ IntPtr statusPtr = this.pi.TargetModuleScanner.ResolveRelativeAddress(actor.Address, 0x1901);
+ return Marshal.ReadByte(statusPtr);
+ }
+
+ private bool InCombat(Actor actor) {
+ return (GetStatus(actor) & 2) > 0;
+ }
+
+ private bool InAlliance(Actor actor) {
+ return (GetStatus(actor) & 32) > 0;
+ }
+
+ private bool CanPlaySound() {
+ if (this.soundLastPlayed == 0) {
+ return true;
+ }
+
+ long current = Stopwatch.GetTimestamp();
+ long diff = current - this.soundLastPlayed;
+ // only play every 10 seconds?
+ float secs = (float)diff / Stopwatch.Frequency;
+ return secs >= this.config.SoundCooldown;
+ }
+
+ private void PlaySound() {
+ SoundPlayer player;
+ if (this.config.SoundPath == null) {
+ player = new SoundPlayer(Properties.Resources.Target);
+ } else {
+ player = new SoundPlayer(this.config.SoundPath);
+ }
+ using (player) {
+ try {
+ player.Play();
+ } catch (FileNotFoundException e) {
+ this.SendError($"Could not play sound: {e.Message}");
+ } catch (InvalidOperationException e) {
+ this.SendError($"Could not play sound: {e.Message}");
+ }
+ }
+ }
+
+ private void SendError(string message) {
+ Payload[] payloads = { new TextPayload($"[Who's Looking] {message}") };
+ this.pi.Framework.Gui.Chat.PrintChat(new XivChatEntry {
+ MessageBytes = new SeString(payloads).Encode(),
+ Type = XivChatType.ErrorMessage
+ });
+ }
+
+ public void Dispose() {
+ this.currentMutex.Dispose();
+ this.previousMutex.Dispose();
+ }
+ }
+}