refactor: pull targeting code out of ui

This has some consequences. First, LocalPlayer is no longer necessary
for rendering the UI, meaning the UI stays open even during loading
screens and other situations where there is no player.

Secondly, the sound effect will play even when the window is closed.

Thirdly, the history will update even when the window is closed.

The last two points will become config options in future.

I would also like to make TargetWatcher run in a task and run perhaps
every 100ms instead of every framework update.
This commit is contained in:
Anna 2020-08-08 05:20:48 -04:00
parent 056f6f7505
commit 1c31f19b89
4 changed files with 187 additions and 93 deletions

View File

@ -72,6 +72,7 @@
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="TargetWatcher.cs" />
<Compile Include="Util.cs" />
</ItemGroup>
<ItemGroup>

View File

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

View File

@ -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<Targeter> previousTargeters = new List<Targeter>();
private Optional<Actor> previousFocus = new Optional<Actor>();
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<PlayerCharacter> 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<PlayerCharacter> 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();
}
}
}

View File

@ -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<PlayerCharacter>();
public IReadOnlyCollection<PlayerCharacter> 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<Targeter> previousTargeters = new List<Targeter>();
public IReadOnlyCollection<Targeter> 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<Out>(Func<IReadOnlyCollection<PlayerCharacter>, 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();
}
}
}