2020-08-08 09:20:48 +00:00
|
|
|
|
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;
|
2020-08-24 17:13:42 +00:00
|
|
|
|
using NAudio.Wave;
|
2020-12-29 16:08:21 +00:00
|
|
|
|
using Resourcer;
|
2020-08-08 09:20:48 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Runtime.InteropServices;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
|
|
|
|
|
namespace PeepingTom {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
internal class TargetWatcher : IDisposable {
|
|
|
|
|
private PeepingTomPlugin Plugin { get; }
|
2020-08-08 09:20:48 +00:00
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
private Stopwatch? Watch { get; set; }
|
|
|
|
|
private int LastTargetAmount { get; set; }
|
2020-08-08 09:20:48 +00:00
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
private volatile bool _stop;
|
|
|
|
|
private volatile bool _needsUpdate = true;
|
|
|
|
|
private Thread? Thread { get; set; }
|
2020-08-08 20:23:51 +00:00
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
private readonly object _dataMutex = new object();
|
|
|
|
|
private TargetThreadData? Data { get; set; }
|
|
|
|
|
|
|
|
|
|
private readonly Mutex _currentMutex = new Mutex();
|
|
|
|
|
private Targeter[] Current { get; set; } = Array.Empty<Targeter>();
|
2020-08-08 20:23:51 +00:00
|
|
|
|
|
|
|
|
|
public IReadOnlyCollection<Targeter> CurrentTargeters {
|
2020-08-08 09:20:48 +00:00
|
|
|
|
get {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this._currentMutex.WaitOne();
|
|
|
|
|
var current = this.Current.ToArray();
|
|
|
|
|
this._currentMutex.ReleaseMutex();
|
2020-08-08 09:20:48 +00:00
|
|
|
|
return current;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
private readonly Mutex _previousMutex = new Mutex();
|
|
|
|
|
private List<Targeter> Previous { get; set; } = new List<Targeter>();
|
|
|
|
|
|
2020-08-08 09:20:48 +00:00
|
|
|
|
public IReadOnlyCollection<Targeter> PreviousTargeters {
|
|
|
|
|
get {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this._previousMutex.WaitOne();
|
|
|
|
|
var previous = this.Previous.ToArray();
|
|
|
|
|
this._previousMutex.ReleaseMutex();
|
2020-08-08 09:20:48 +00:00
|
|
|
|
return previous;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-08 10:05:23 +00:00
|
|
|
|
public TargetWatcher(PeepingTomPlugin plugin) {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this.Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin), "PeepingTomPlugin cannot be null");
|
2020-08-08 09:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-08 10:21:57 +00:00
|
|
|
|
public void ClearPrevious() {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this._previousMutex.WaitOne();
|
|
|
|
|
this.Previous.Clear();
|
|
|
|
|
this._previousMutex.ReleaseMutex();
|
2020-08-08 09:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-08 20:23:51 +00:00
|
|
|
|
public void StartThread() {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this.Thread = new Thread(() => {
|
|
|
|
|
while (!this._stop) {
|
2020-08-08 20:23:51 +00:00
|
|
|
|
this.Update();
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this._needsUpdate = true;
|
|
|
|
|
Thread.Sleep(this.Plugin.Config.PollFrequency);
|
2020-08-08 20:23:51 +00:00
|
|
|
|
}
|
2020-12-29 16:08:21 +00:00
|
|
|
|
});
|
|
|
|
|
this.Thread.Start();
|
2020-08-08 20:23:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void WaitStopThread() {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this._stop = true;
|
|
|
|
|
this.Thread?.Join();
|
2020-08-08 20:23:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-08 09:20:48 +00:00
|
|
|
|
public void OnFrameworkUpdate(Framework framework) {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
if (!this._needsUpdate) {
|
2020-08-08 09:20:48 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
lock (this._dataMutex) {
|
|
|
|
|
this.Data = new TargetThreadData(this.Plugin.Interface);
|
2020-08-08 20:23:51 +00:00
|
|
|
|
}
|
2020-12-29 16:08:21 +00:00
|
|
|
|
|
|
|
|
|
this._needsUpdate = false;
|
2020-08-08 20:23:51 +00:00
|
|
|
|
}
|
2020-08-08 09:20:48 +00:00
|
|
|
|
|
2020-08-08 20:23:51 +00:00
|
|
|
|
private void Update() {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
lock (this._dataMutex) {
|
|
|
|
|
var player = this.Data?.LocalPlayer;
|
2020-08-08 20:23:51 +00:00
|
|
|
|
if (player == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// block until lease
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this._currentMutex.WaitOne();
|
2020-08-08 20:23:51 +00:00
|
|
|
|
|
|
|
|
|
// get targeters and set a copy so we can release the mutex faster
|
2020-12-29 16:08:21 +00:00
|
|
|
|
var current = this.GetTargeting(this.Data!.Actors, player);
|
|
|
|
|
this.Current = (Targeter[]) current.Clone();
|
2020-08-08 20:23:51 +00:00
|
|
|
|
|
|
|
|
|
// release
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this._currentMutex.ReleaseMutex();
|
2020-08-08 20:23:51 +00:00
|
|
|
|
}
|
2020-08-08 09:20:48 +00:00
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this.HandleHistory(this.Current);
|
2020-08-08 09:20:48 +00:00
|
|
|
|
|
|
|
|
|
// play sound if necessary
|
2020-08-08 10:05:23 +00:00
|
|
|
|
if (this.CanPlaySound()) {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this.Watch?.Restart();
|
2020-08-08 09:20:48 +00:00
|
|
|
|
this.PlaySound();
|
|
|
|
|
}
|
2020-12-29 16:08:21 +00:00
|
|
|
|
|
|
|
|
|
this.LastTargetAmount = this.Current.Length;
|
2020-08-08 09:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-08 20:23:51 +00:00
|
|
|
|
private void HandleHistory(Targeter[] targeting) {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
if (!this.Plugin.Config.KeepHistory || (!this.Plugin.Config.HistoryWhenClosed && !this.Plugin.Ui.Visible)) {
|
2020-08-08 09:20:48 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this._previousMutex.WaitOne();
|
2020-08-08 10:21:44 +00:00
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
foreach (var targeter in targeting) {
|
2020-08-08 09:20:48 +00:00
|
|
|
|
// add the targeter to the previous list
|
2020-12-29 16:08:21 +00:00
|
|
|
|
if (this.Previous.Any(old => old.ActorId == targeter.ActorId)) {
|
|
|
|
|
this.Previous.RemoveAll(old => old.ActorId == targeter.ActorId);
|
2020-08-08 09:20:48 +00:00
|
|
|
|
}
|
2020-12-29 16:08:21 +00:00
|
|
|
|
|
|
|
|
|
this.Previous.Insert(0, targeter);
|
2020-08-08 09:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// only keep the configured number of previous targeters (ignoring ones that are currently targeting)
|
2020-12-29 16:08:21 +00:00
|
|
|
|
while (this.Previous.Count(old => targeting.All(actor => actor.ActorId != old.ActorId)) > this.Plugin.Config.NumHistory) {
|
|
|
|
|
this.Previous.RemoveAt(this.Previous.Count - 1);
|
2020-08-08 09:20:48 +00:00
|
|
|
|
}
|
2020-08-08 10:21:44 +00:00
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this._previousMutex.ReleaseMutex();
|
2020-08-08 09:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
private Targeter[] GetTargeting(IEnumerable<Actor> actors, Actor player) {
|
2020-08-08 20:23:51 +00:00
|
|
|
|
return actors
|
2020-08-08 09:20:48 +00:00
|
|
|
|
.Where(actor => actor.TargetActorID == player.ActorId && actor is PlayerCharacter)
|
2020-12-29 16:08:21 +00:00
|
|
|
|
.Cast<PlayerCharacter>()
|
|
|
|
|
.Where(actor => this.Plugin.Config.LogParty || !InParty(actor))
|
|
|
|
|
.Where(actor => this.Plugin.Config.LogAlliance || !InAlliance(actor))
|
|
|
|
|
.Where(actor => this.Plugin.Config.LogInCombat || !InCombat(actor))
|
|
|
|
|
.Where(actor => this.Plugin.Config.LogSelf || actor.ActorId != player.ActorId)
|
2020-08-08 20:23:51 +00:00
|
|
|
|
.Select(actor => new Targeter(actor))
|
2020-08-08 09:20:48 +00:00
|
|
|
|
.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-11 12:13:19 +00:00
|
|
|
|
private static byte GetStatus(Actor actor) {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
var statusPtr = actor.Address + 0x1906; // updated 5.3
|
2020-08-08 09:20:48 +00:00
|
|
|
|
return Marshal.ReadByte(statusPtr);
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-11 12:13:19 +00:00
|
|
|
|
private static bool InCombat(Actor actor) => (GetStatus(actor) & 2) > 0;
|
2020-08-08 09:20:48 +00:00
|
|
|
|
|
2020-08-11 12:13:19 +00:00
|
|
|
|
private static bool InParty(Actor actor) => (GetStatus(actor) & 16) > 0;
|
2020-08-09 02:15:04 +00:00
|
|
|
|
|
2020-08-11 12:13:19 +00:00
|
|
|
|
private static bool InAlliance(Actor actor) => (GetStatus(actor) & 32) > 0;
|
2020-08-08 09:20:48 +00:00
|
|
|
|
|
|
|
|
|
private bool CanPlaySound() {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
if (!this.Plugin.Config.PlaySoundOnTarget) {
|
2020-08-08 10:05:23 +00:00
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
if (this.Current.Length <= this.LastTargetAmount) {
|
2020-08-08 10:05:23 +00:00
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
if (!this.Plugin.Config.PlaySoundWhenClosed && !this.Plugin.Ui.Visible) {
|
2020-08-08 10:05:23 +00:00
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
if (this.Watch == null) {
|
|
|
|
|
this.Watch = new Stopwatch();
|
2020-08-08 09:20:48 +00:00
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
var secs = this.Watch.Elapsed.TotalSeconds;
|
|
|
|
|
return secs >= this.Plugin.Config.SoundCooldown;
|
2020-08-08 09:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void PlaySound() {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
var soundDevice = this.Plugin.Config.SoundDevice;
|
2020-08-24 17:13:42 +00:00
|
|
|
|
if (soundDevice < -1 || soundDevice > WaveOut.DeviceCount) {
|
|
|
|
|
soundDevice = -1;
|
2020-08-08 09:20:48 +00:00
|
|
|
|
}
|
2020-08-24 17:13:42 +00:00
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
new Thread(() => {
|
2020-08-24 17:13:42 +00:00
|
|
|
|
WaveStream reader;
|
2020-08-08 09:20:48 +00:00
|
|
|
|
try {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
if (this.Plugin.Config.SoundPath == null) {
|
|
|
|
|
reader = new WaveFileReader(Resource.AsStream("Resources/target.wav"));
|
2020-08-24 17:13:42 +00:00
|
|
|
|
} else {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
reader = new AudioFileReader(this.Plugin.Config.SoundPath);
|
2020-08-24 17:13:42 +00:00
|
|
|
|
}
|
2020-12-29 16:08:21 +00:00
|
|
|
|
#pragma warning disable CA1031 // Do not catch general exception types
|
2020-08-24 17:13:42 +00:00
|
|
|
|
} catch (Exception e) {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
#pragma warning restore CA1031 // Do not catch general exception types
|
2020-08-24 17:13:42 +00:00
|
|
|
|
this.SendError($"Could not play sound file: {e.Message}");
|
|
|
|
|
return;
|
2020-08-08 09:20:48 +00:00
|
|
|
|
}
|
2020-08-24 17:13:42 +00:00
|
|
|
|
|
2020-12-29 16:11:32 +00:00
|
|
|
|
using WaveChannel32 channel = new WaveChannel32(reader) {
|
|
|
|
|
Volume = this.Plugin.Config.SoundVolume,
|
|
|
|
|
};
|
2020-08-25 18:06:18 +00:00
|
|
|
|
|
2020-08-24 17:13:42 +00:00
|
|
|
|
using (reader) {
|
2020-12-29 16:11:32 +00:00
|
|
|
|
using var output = new WaveOutEvent {DeviceNumber = soundDevice};
|
|
|
|
|
output.Init(channel);
|
|
|
|
|
output.Play();
|
2020-08-24 17:13:42 +00:00
|
|
|
|
|
2020-12-29 16:11:32 +00:00
|
|
|
|
while (output.PlaybackState == PlaybackState.Playing) {
|
|
|
|
|
Thread.Sleep(500);
|
2020-08-24 17:13:42 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-29 16:08:21 +00:00
|
|
|
|
}).Start();
|
2020-08-08 09:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SendError(string message) {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
Payload[] payloads = {
|
|
|
|
|
new TextPayload($"[{this.Plugin.Name}] {message}"),
|
|
|
|
|
};
|
|
|
|
|
this.Plugin.Interface.Framework.Gui.Chat.PrintChat(new XivChatEntry {MessageBytes = new SeString(payloads).Encode(), Type = XivChatType.ErrorMessage,});
|
2020-08-08 09:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose() {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this._currentMutex.Dispose();
|
|
|
|
|
this._previousMutex.Dispose();
|
2020-08-08 09:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-08-08 20:23:51 +00:00
|
|
|
|
|
2020-12-29 16:08:21 +00:00
|
|
|
|
internal class TargetThreadData {
|
|
|
|
|
public PlayerCharacter LocalPlayer { get; }
|
|
|
|
|
public Actor[] Actors { get; }
|
2020-08-08 20:23:51 +00:00
|
|
|
|
|
|
|
|
|
public TargetThreadData(DalamudPluginInterface pi) {
|
2020-12-29 16:08:21 +00:00
|
|
|
|
this.LocalPlayer = pi.ClientState.LocalPlayer;
|
|
|
|
|
this.Actors = pi.ClientState.Actors.ToArray();
|
2020-08-08 20:23:51 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-08-08 09:20:48 +00:00
|
|
|
|
}
|