From 043c1efc1fb44ffd456c9a6f575bd155c9a322e9 Mon Sep 17 00:00:00 2001 From: Anna Date: Sun, 22 Aug 2021 23:33:57 -0400 Subject: [PATCH] refactor: move to net5 --- .gitattributes | 1 + Peeping Tom.Ipc/From/AllTargetersMessage.cs | 13 ++ Peeping Tom.Ipc/From/IFromMessage.cs | 5 + Peeping Tom.Ipc/From/NewTargeterMessage.cs | 12 ++ .../From/StoppedTargetingMessage.cs | 12 ++ Peeping Tom.Ipc/IpcInfo.cs | 18 ++ Peeping Tom.Ipc/Peeping Tom.Ipc.csproj | 21 +++ Peeping Tom.Ipc/Targeter.cs | 26 +++ Peeping Tom.Ipc/To/IToMessage.cs | 5 + Peeping Tom.Ipc/To/RequestTargetersMessage.cs | 7 + Peeping Tom.sln | 6 + Peeping Tom/DalamudPackager.targets | 1 - Peeping Tom/FodyWeavers.xml | 2 - Peeping Tom/IpcManager.cs | 54 ++++++ Peeping Tom/Peeping Tom.csproj | 24 +-- Peeping Tom/PeepingTom.yaml | 1 + Peeping Tom/Plugin.cs | 115 ++++++++---- Peeping Tom/PluginUi.cs | 124 ++++++------- Peeping Tom/TargetWatcher.cs | 164 ++++++------------ Peeping Tom/Targeting.cs | 25 --- Peeping Tom/Util.cs | 21 --- README.md | 2 + icon.png | Bin 0 -> 12361 bytes icon.svg | 69 ++++++++ 24 files changed, 458 insertions(+), 270 deletions(-) create mode 100755 Peeping Tom.Ipc/From/AllTargetersMessage.cs create mode 100755 Peeping Tom.Ipc/From/IFromMessage.cs create mode 100755 Peeping Tom.Ipc/From/NewTargeterMessage.cs create mode 100755 Peeping Tom.Ipc/From/StoppedTargetingMessage.cs create mode 100755 Peeping Tom.Ipc/IpcInfo.cs create mode 100755 Peeping Tom.Ipc/Peeping Tom.Ipc.csproj create mode 100755 Peeping Tom.Ipc/Targeter.cs create mode 100755 Peeping Tom.Ipc/To/IToMessage.cs create mode 100755 Peeping Tom.Ipc/To/RequestTargetersMessage.cs mode change 100644 => 100755 Peeping Tom.sln create mode 100755 Peeping Tom/IpcManager.cs delete mode 100644 Peeping Tom/Targeting.cs delete mode 100644 Peeping Tom/Util.cs create mode 100644 icon.png create mode 100755 icon.svg diff --git a/.gitattributes b/.gitattributes index 99b6890..dcb8c3a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ * text eol=lf *.wav binary +*.png binary diff --git a/Peeping Tom.Ipc/From/AllTargetersMessage.cs b/Peeping Tom.Ipc/From/AllTargetersMessage.cs new file mode 100755 index 0000000..4aa1fe1 --- /dev/null +++ b/Peeping Tom.Ipc/From/AllTargetersMessage.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace PeepingTom.Ipc.From { + [Serializable] + public class AllTargetersMessage : IFromMessage { + public List<(Targeter targeter, bool currentlyTargeting)> Targeters { get; } + + public AllTargetersMessage(List<(Targeter, bool)> targeters) { + this.Targeters = targeters; + } + } +} diff --git a/Peeping Tom.Ipc/From/IFromMessage.cs b/Peeping Tom.Ipc/From/IFromMessage.cs new file mode 100755 index 0000000..095028e --- /dev/null +++ b/Peeping Tom.Ipc/From/IFromMessage.cs @@ -0,0 +1,5 @@ +namespace PeepingTom.Ipc.From { + public interface IFromMessage { + + } +} diff --git a/Peeping Tom.Ipc/From/NewTargeterMessage.cs b/Peeping Tom.Ipc/From/NewTargeterMessage.cs new file mode 100755 index 0000000..a4aa48f --- /dev/null +++ b/Peeping Tom.Ipc/From/NewTargeterMessage.cs @@ -0,0 +1,12 @@ +using System; + +namespace PeepingTom.Ipc.From { + [Serializable] + public class NewTargeterMessage : IFromMessage { + public Targeter Targeter { get; } + + public NewTargeterMessage(Targeter targeter) { + this.Targeter = targeter; + } + } +} diff --git a/Peeping Tom.Ipc/From/StoppedTargetingMessage.cs b/Peeping Tom.Ipc/From/StoppedTargetingMessage.cs new file mode 100755 index 0000000..3303550 --- /dev/null +++ b/Peeping Tom.Ipc/From/StoppedTargetingMessage.cs @@ -0,0 +1,12 @@ +using System; + +namespace PeepingTom.Ipc.From { + [Serializable] + public class StoppedTargetingMessage : IFromMessage { + public Targeter Targeter { get; } + + public StoppedTargetingMessage(Targeter targeter) { + this.Targeter = targeter; + } + } +} diff --git a/Peeping Tom.Ipc/IpcInfo.cs b/Peeping Tom.Ipc/IpcInfo.cs new file mode 100755 index 0000000..9bb2416 --- /dev/null +++ b/Peeping Tom.Ipc/IpcInfo.cs @@ -0,0 +1,18 @@ +using Dalamud.Plugin; +using PeepingTom.Ipc.From; +using PeepingTom.Ipc.To; + +namespace PeepingTom.Ipc { + public static class IpcInfo { + public const string FromRegistrationName = "PeepingTom.From"; + public const string ToRegistrationName = "PeepingTom.To"; + + public static ICallGateProvider GetProvider(DalamudPluginInterface @interface) { + return @interface.GetIpcProvider(ToRegistrationName); + } + + public static ICallGateSubscriber GetSubscriber(DalamudPluginInterface @interface) { + return @interface.GetIpcSubscriber(FromRegistrationName); + } + } +} diff --git a/Peeping Tom.Ipc/Peeping Tom.Ipc.csproj b/Peeping Tom.Ipc/Peeping Tom.Ipc.csproj new file mode 100755 index 0000000..565c6a4 --- /dev/null +++ b/Peeping Tom.Ipc/Peeping Tom.Ipc.csproj @@ -0,0 +1,21 @@ + + + + net5.0-windows + 1.0.0 + enable + PeepingTom.Ipc + + + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll + false + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll + false + + + + diff --git a/Peeping Tom.Ipc/Targeter.cs b/Peeping Tom.Ipc/Targeter.cs new file mode 100755 index 0000000..d34a88f --- /dev/null +++ b/Peeping Tom.Ipc/Targeter.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.Text.SeStringHandling; + +namespace PeepingTom.Ipc { + [Serializable] + public class Targeter { + public SeString Name { get; } + public uint HomeWorldId { get; } + public uint ObjectId { get; } + public DateTime When { get; } + + public Targeter(PlayerCharacter character) { + this.Name = character.Name; + this.HomeWorldId = character.HomeWorld.Id; + this.ObjectId = character.ObjectId; + this.When = DateTime.UtcNow; + } + + public PlayerCharacter? GetPlayerCharacter(ObjectTable objectTable) { + return objectTable.FirstOrDefault(actor => actor.ObjectId == this.ObjectId && actor is PlayerCharacter) as PlayerCharacter; + } + } +} diff --git a/Peeping Tom.Ipc/To/IToMessage.cs b/Peeping Tom.Ipc/To/IToMessage.cs new file mode 100755 index 0000000..f3e9d4c --- /dev/null +++ b/Peeping Tom.Ipc/To/IToMessage.cs @@ -0,0 +1,5 @@ +namespace PeepingTom.Ipc.To { + public interface IToMessage { + + } +} diff --git a/Peeping Tom.Ipc/To/RequestTargetersMessage.cs b/Peeping Tom.Ipc/To/RequestTargetersMessage.cs new file mode 100755 index 0000000..99980c3 --- /dev/null +++ b/Peeping Tom.Ipc/To/RequestTargetersMessage.cs @@ -0,0 +1,7 @@ +using System; + +namespace PeepingTom.Ipc.To { + [Serializable] + public class RequestTargetersMessage : IToMessage { + } +} diff --git a/Peeping Tom.sln b/Peeping Tom.sln old mode 100644 new mode 100755 index b8884df..cd486ab --- a/Peeping Tom.sln +++ b/Peeping Tom.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Peeping Tom", "Peeping Tom\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9B879446-A687-4B9D-8628-807CCB8C51AE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Peeping Tom.Ipc", "Peeping Tom.Ipc\Peeping Tom.Ipc.csproj", "{F454FB15-2C11-44F3-A651-E5F912F0FE11}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -17,6 +19,10 @@ Global {888F98DF-AF1D-4852-8411-11B1FEEFE674}.Debug|Any CPU.Build.0 = Debug|Any CPU {888F98DF-AF1D-4852-8411-11B1FEEFE674}.Release|Any CPU.ActiveCfg = Release|Any CPU {888F98DF-AF1D-4852-8411-11B1FEEFE674}.Release|Any CPU.Build.0 = Release|Any CPU + {F454FB15-2C11-44F3-A651-E5F912F0FE11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F454FB15-2C11-44F3-A651-E5F912F0FE11}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F454FB15-2C11-44F3-A651-E5F912F0FE11}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F454FB15-2C11-44F3-A651-E5F912F0FE11}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Peeping Tom/DalamudPackager.targets b/Peeping Tom/DalamudPackager.targets index d67396e..5c12c42 100644 --- a/Peeping Tom/DalamudPackager.targets +++ b/Peeping Tom/DalamudPackager.targets @@ -4,7 +4,6 @@ OutputPath="$(OutputPath)" AssemblyName="$(AssemblyName)" VersionComponents="3" - Include="PeepingTom.dll;PeepingTom.json;PeepingTom.pdb" MakeZip="true"/> diff --git a/Peeping Tom/FodyWeavers.xml b/Peeping Tom/FodyWeavers.xml index e3168e2..c0818d9 100644 --- a/Peeping Tom/FodyWeavers.xml +++ b/Peeping Tom/FodyWeavers.xml @@ -1,6 +1,4 @@  - - diff --git a/Peeping Tom/IpcManager.cs b/Peeping Tom/IpcManager.cs new file mode 100755 index 0000000..d801ab9 --- /dev/null +++ b/Peeping Tom/IpcManager.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Plugin; +using PeepingTom.Ipc; +using PeepingTom.Ipc.From; +using PeepingTom.Ipc.To; + +namespace PeepingTom { + internal class IpcManager : IDisposable { + private PeepingTomPlugin Plugin { get; } + + private ICallGateProvider Provider { get; } + private ICallGateSubscriber Subscriber { get; } + + internal IpcManager(PeepingTomPlugin plugin) { + this.Plugin = plugin; + + this.Provider = this.Plugin.Interface.GetIpcProvider(IpcInfo.FromRegistrationName); + this.Subscriber = this.Plugin.Interface.GetIpcSubscriber(IpcInfo.ToRegistrationName); + + this.Subscriber.Subscribe(this.ReceiveMessage); + } + + public void Dispose() { + this.Subscriber.Unsubscribe(this.ReceiveMessage); + } + + internal void SendAllTargeters() { + var targeters = new List<(Targeter, bool)>(); + targeters.AddRange(this.Plugin.Watcher.CurrentTargeters.Select(t => (t, true))); + targeters.AddRange(this.Plugin.Watcher.PreviousTargeters.Select(t => (t, false))); + + this.Provider.SendMessage(new AllTargetersMessage(targeters)); + } + + internal void SendNewTargeter(Targeter targeter) { + this.Provider.SendMessage(new NewTargeterMessage(targeter)); + } + + internal void SendStoppedTargeting(Targeter targeter) { + this.Provider.SendMessage(new StoppedTargetingMessage(targeter)); + } + + private void ReceiveMessage(IToMessage message) { + switch (message) { + case RequestTargetersMessage: { + this.SendAllTargeters(); + break; + } + } + } + } +} diff --git a/Peeping Tom/Peeping Tom.csproj b/Peeping Tom/Peeping Tom.csproj index 63896f6..d1f646b 100755 --- a/Peeping Tom/Peeping Tom.csproj +++ b/Peeping Tom/Peeping Tom.csproj @@ -1,13 +1,15 @@  - net48 + net5-windows PeepingTom 1.7.5 latest enable PeepingTom true + true + false @@ -37,19 +39,16 @@ $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll False - - $(AppData)\XIVLauncher\addon\Hooks\dev\SharpDX.Mathematics.dll - False - - - - - + + + - - + + + + @@ -58,4 +57,7 @@ Language.resx + + + diff --git a/Peeping Tom/PeepingTom.yaml b/Peeping Tom/PeepingTom.yaml index 7e39c42..1eec67c 100644 --- a/Peeping Tom/PeepingTom.yaml +++ b/Peeping Tom/PeepingTom.yaml @@ -1,4 +1,5 @@ author: ascclemens name: Peeping Tom +punchline: Shows who is currently or was previously targeting you. description: Shows who is currently or was previously targeting you. repo_url: https://sr.ht/~jkcclemens/PeepingTom diff --git a/Peeping Tom/Plugin.cs b/Peeping Tom/Plugin.cs index c15e774..cc23b08 100644 --- a/Peeping Tom/Plugin.cs +++ b/Peeping Tom/Plugin.cs @@ -3,78 +3,117 @@ using Dalamud.Plugin; using System; using System.Collections.Generic; using System.Globalization; +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.Gui; +using Dalamud.Game.Gui.Toast; +using Dalamud.IoC; +using Dalamud.Logging; using Lumina.Excel.GeneratedSheets; using PeepingTom.Resources; using XivCommon; +using Condition = Dalamud.Game.ClientState.Conditions.Condition; namespace PeepingTom { // ReSharper disable once ClassNeverInstantiated.Global public class PeepingTomPlugin : IDalamudPlugin { public string Name => "Peeping Tom"; - internal DalamudPluginInterface Interface { get; private set; } = null!; - internal Configuration Config { get; private set; } = null!; - internal PluginUi Ui { get; private set; } = null!; - internal TargetWatcher Watcher { get; private set; } = null!; - internal XivCommonBase Common { get; private set; } = null!; + [PluginService] + internal DalamudPluginInterface Interface { get; init; } = null!; + + [PluginService] + internal ChatGui ChatGui { get; init; } = null!; + + [PluginService] + internal ClientState ClientState { get; init; } = null!; + + [PluginService] + private CommandManager CommandManager { get; init; } = null!; + + [PluginService] + internal Condition Condition { get; init; } = null!; + + [PluginService] + internal DataManager DataManager { get; init; } = null!; + + [PluginService] + internal Framework Framework { get; init; } = null!; + + [PluginService] + internal GameGui GameGui { get; init; } = null!; + + [PluginService] + internal ObjectTable ObjectTable { get; init; } = null!; + + [PluginService] + internal TargetManager TargetManager { get; init; } = null!; + + [PluginService] + internal ToastGui ToastGui { get; init; } = null!; + + internal Configuration Config { get; } + internal PluginUi Ui { get; } + internal TargetWatcher Watcher { get; } + internal XivCommonBase Common { get; } + internal IpcManager IpcManager { get; } internal bool InPvp { get; private set; } - public void Initialize(DalamudPluginInterface pluginInterface) { - this.Interface = pluginInterface; - this.Common = new XivCommonBase(this.Interface); + public PeepingTomPlugin() { + this.Common = new XivCommonBase(); this.Config = this.Interface.GetPluginConfig() as Configuration ?? new Configuration(); this.Config.Initialize(this.Interface); this.Watcher = new TargetWatcher(this); this.Ui = new PluginUi(this); + this.IpcManager = new IpcManager(this); OnLanguageChange(this.Interface.UiLanguage); - this.Interface.OnLanguageChanged += OnLanguageChange; + this.Interface.LanguageChanged += OnLanguageChange; - this.Interface.CommandManager.AddHandler("/ppeepingtom", new CommandInfo(this.OnCommand) { + this.CommandManager.AddHandler("/ppeepingtom", new CommandInfo(this.OnCommand) { HelpMessage = "Use with no arguments to show the list. Use with \"c\" or \"config\" to show the config", }); - this.Interface.CommandManager.AddHandler("/ptom", new CommandInfo(this.OnCommand) { + this.CommandManager.AddHandler("/ptom", new CommandInfo(this.OnCommand) { HelpMessage = "Alias for /ppeepingtom", }); - this.Interface.CommandManager.AddHandler("/ppeep", new CommandInfo(this.OnCommand) { + this.CommandManager.AddHandler("/ppeep", new CommandInfo(this.OnCommand) { HelpMessage = "Alias for /ppeepingtom", }); - this.Interface.Framework.OnUpdateEvent += this.Watcher.OnFrameworkUpdate; - this.Interface.ClientState.OnLogin += this.OnLogin; - this.Interface.ClientState.OnLogout += this.OnLogout; - this.Interface.ClientState.TerritoryChanged += this.OnTerritoryChange; - this.Interface.UiBuilder.OnBuildUi += this.DrawUi; - this.Interface.UiBuilder.OnOpenConfigUi += this.ConfigUi; - - this.Watcher.StartThread(); + this.ClientState.Login += this.OnLogin; + this.ClientState.Logout += this.OnLogout; + this.ClientState.TerritoryChanged += this.OnTerritoryChange; + this.Interface.UiBuilder.Draw += this.DrawUi; + this.Interface.UiBuilder.OpenConfigUi += this.ConfigUi; } public void Dispose() { - this.Common.Dispose(); - this.Interface.Framework.OnUpdateEvent -= this.Watcher.OnFrameworkUpdate; - this.Interface.ClientState.OnLogin -= this.OnLogin; - this.Interface.ClientState.OnLogout -= this.OnLogout; - this.Watcher.WaitStopThread(); - this.Watcher.Dispose(); - this.Interface.UiBuilder.OnBuildUi -= this.DrawUi; - this.Interface.UiBuilder.OnOpenConfigUi -= this.ConfigUi; - this.Interface.CommandManager.RemoveHandler("/ppeepingtom"); - this.Interface.CommandManager.RemoveHandler("/ptom"); - this.Interface.CommandManager.RemoveHandler("/ppeep"); + this.Interface.UiBuilder.OpenConfigUi -= this.ConfigUi; + this.Interface.UiBuilder.Draw -= this.DrawUi; + this.ClientState.TerritoryChanged -= this.OnTerritoryChange; + this.ClientState.Logout -= this.OnLogout; + this.ClientState.Login -= this.OnLogin; + this.CommandManager.RemoveHandler("/ppeep"); + this.CommandManager.RemoveHandler("/ptom"); + this.CommandManager.RemoveHandler("/ppeepingtom"); + this.Interface.LanguageChanged -= OnLanguageChange; + this.IpcManager.Dispose(); this.Ui.Dispose(); - this.Interface.OnLanguageChanged -= OnLanguageChange; + this.Watcher.Dispose(); + this.Common.Dispose(); } private static void OnLanguageChange(string langCode) { Language.Culture = new CultureInfo(langCode); } - private void OnTerritoryChange(object sender, ushort e) { + private void OnTerritoryChange(object? sender, ushort e) { try { - var territory = this.Interface.Data.GetExcelSheet().GetRow(e); - this.InPvp = territory.IsPvpZone; + var territory = this.DataManager.GetExcelSheet()!.GetRow(e); + this.InPvp = territory?.IsPvpZone == true; } catch (KeyNotFoundException) { PluginLog.Warning("Could not get territory for current zone"); } @@ -88,7 +127,7 @@ namespace PeepingTom { } } - private void OnLogin(object sender, EventArgs args) { + private void OnLogin(object? sender, EventArgs args) { if (!this.Config.OpenOnLogin) { return; } @@ -96,7 +135,7 @@ namespace PeepingTom { this.Ui.WantsOpen = true; } - private void OnLogout(object sender, EventArgs args) { + private void OnLogout(object? sender, EventArgs args) { this.Ui.WantsOpen = false; this.Watcher.ClearPrevious(); } @@ -105,7 +144,7 @@ namespace PeepingTom { this.Ui.Draw(); } - private void ConfigUi(object sender, EventArgs args) { + private void ConfigUi() { this.Ui.SettingsOpen = true; } } diff --git a/Peeping Tom/PluginUi.cs b/Peeping Tom/PluginUi.cs index f2f6f26..75b2a57 100644 --- a/Peeping Tom/PluginUi.cs +++ b/Peeping Tom/PluginUi.cs @@ -1,22 +1,25 @@ -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Actors.Types; -using ImGuiNET; +using ImGuiNET; using NAudio.Wave; using System; using System.Collections.Generic; using System.Linq; using System.Numerics; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; +using PeepingTom.Ipc; using PeepingTom.Resources; namespace PeepingTom { internal class PluginUi : IDisposable { private PeepingTomPlugin Plugin { get; } - private Optional PreviousFocus { get; set; } = new(); + private uint? PreviousFocus { get; set; } = new(); private bool _wantsOpen; @@ -52,13 +55,13 @@ namespace PeepingTom { this.ShowSettings(); } - var inCombat = this.Plugin.Interface.ClientState.Condition[ConditionFlag.InCombat]; - var inInstance = this.Plugin.Interface.ClientState.Condition[ConditionFlag.BoundByDuty] - || this.Plugin.Interface.ClientState.Condition[ConditionFlag.BoundByDuty56] - || this.Plugin.Interface.ClientState.Condition[ConditionFlag.BoundByDuty95]; - var inCutscene = this.Plugin.Interface.ClientState.Condition[ConditionFlag.WatchingCutscene] - || this.Plugin.Interface.ClientState.Condition[ConditionFlag.WatchingCutscene78] - || this.Plugin.Interface.ClientState.Condition[ConditionFlag.OccupiedInCutSceneEvent]; + var inCombat = this.Plugin.Condition[ConditionFlag.InCombat]; + var inInstance = this.Plugin.Condition[ConditionFlag.BoundByDuty] + || this.Plugin.Condition[ConditionFlag.BoundByDuty56] + || this.Plugin.Condition[ConditionFlag.BoundByDuty95]; + var inCutscene = this.Plugin.Condition[ConditionFlag.WatchingCutscene] + || this.Plugin.Condition[ConditionFlag.WatchingCutscene78] + || this.Plugin.Condition[ConditionFlag.OccupiedInCutSceneEvent]; // FIXME: this could just be a boolean expression var shouldBeShown = this.WantsOpen; @@ -101,13 +104,13 @@ namespace PeepingTom { goto EndDummy; } - var player = this.Plugin.Interface.ClientState.LocalPlayer; + var player = this.Plugin.ClientState.LocalPlayer; if (player == null) { goto EndDummy; } var targeting = this.Plugin.Watcher.CurrentTargeters - .Select(targeter => this.Plugin.Interface.ClientState.Actors.FirstOrDefault(actor => actor.ActorId == targeter.ActorId)) + .Select(targeter => this.Plugin.ObjectTable.FirstOrDefault(obj => obj.ObjectId == targeter.ObjectId)) .Where(targeter => targeter is PlayerCharacter) .Cast() .ToArray(); @@ -377,10 +380,10 @@ namespace PeepingTom { .Where(actor => actor.TargetActorID == player.ActorId && actor is PlayerCharacter) .Cast(); foreach (var actor in actors) { - var payload = new PlayerPayload(this.Plugin.Interface.Data, actor.Name, actor.HomeWorld.Id); + var payload = new PlayerPayload(this.Plugin.Interface.Data, actor.Name.TextValue, actor.HomeWorld.Id); Payload[] payloads = {payload}; this.Plugin.Interface.Framework.Gui.Chat.PrintChat(new XivChatEntry { - MessageBytes = new SeString(payloads).Encode(), + Message = new SeString(payloads), }); } } @@ -390,10 +393,10 @@ namespace PeepingTom { var target = this.GetCurrentTarget(); if (target != null) { - var payload = new PlayerPayload(this.Plugin.Interface.Data, target.Name, target.HomeWorld.Id); + var payload = new PlayerPayload(this.Plugin.Interface.Data, target.Name.TextValue, target.HomeWorld.Id); Payload[] payloads = {payload}; this.Plugin.Interface.Framework.Gui.Chat.PrintChat(new XivChatEntry { - MessageBytes = new SeString(payloads).Encode(), + Message = new SeString(payloads), }); } } @@ -414,18 +417,18 @@ namespace PeepingTom { // 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? actors = null; + Dictionary? objects = null; if (targeting.Count + (previousTargeters?.Count ?? 0) > 1) { - var dict = new Dictionary(); - foreach (var actor in this.Plugin.Interface.ClientState.Actors) { - if (dict.ContainsKey(actor.ActorId) || actor.ObjectKind != Dalamud.Game.ClientState.Actors.ObjectKind.Player) { + var dict = new Dictionary(); + foreach (var obj in this.Plugin.ObjectTable) { + if (dict.ContainsKey(obj.ObjectId) || obj.ObjectKind != ObjectKind.Player) { continue; } - dict.Add(actor.ActorId, actor); + dict.Add(obj.ObjectId, obj); } - actors = dict; + objects = dict; } var flags = ImGuiWindowFlags.None; @@ -465,37 +468,38 @@ namespace PeepingTom { // } foreach (var targeter in targeting) { - Actor? actor = null; - actors?.TryGetValue(targeter.ActorId, out actor); - this.AddEntry(targeter, actor, ref anyHovered); + GameObject? obj = null; + objects?.TryGetValue(targeter.ObjectId, out obj); + this.AddEntry(targeter, obj, ref anyHovered); } if (this.Plugin.Config.KeepHistory) { // get a list of the previous targeters that aren't currently targeting var previous = (previousTargeters ?? new List()) - .Where(old => targeting.All(actor => actor.ActorId != old.ActorId)) + .Where(old => targeting.All(actor => actor.ObjectId != old.ObjectId)) .Take(this.Plugin.Config.NumHistory); // add previous targeters to the list foreach (var oldTargeter in previous) { - Actor? actor = null; - actors?.TryGetValue(oldTargeter.ActorId, out actor); - this.AddEntry(oldTargeter, actor, ref anyHovered, ImGuiSelectableFlags.Disabled); + GameObject? obj = null; + objects?.TryGetValue(oldTargeter.ObjectId, out obj); + this.AddEntry(oldTargeter, obj, ref anyHovered, ImGuiSelectableFlags.Disabled); } } ImGui.EndListBox(); } - if (this.Plugin.Config.FocusTargetOnHover && !anyHovered && this.PreviousFocus.Get(out var previousFocus)) { - if (previousFocus == null) { - this.Plugin.Interface.ClientState.Targets.SetFocusTarget(null); + var previousFocus = this.PreviousFocus; + if (this.Plugin.Config.FocusTargetOnHover && !anyHovered && previousFocus != null) { + if (previousFocus == uint.MaxValue) { + this.Plugin.TargetManager.FocusTarget = null; } else { - var actor = this.Plugin.Interface.ClientState.Actors.FirstOrDefault(a => a.ActorId == previousFocus.ActorId); + var actor = this.Plugin.ObjectTable.FirstOrDefault(a => a.ObjectId == previousFocus); // either target the actor if still present or target nothing - this.Plugin.Interface.ClientState.Targets.SetFocusTarget(actor); + this.Plugin.TargetManager.FocusTarget = actor; } - this.PreviousFocus = new Optional(); + this.PreviousFocus = null; } ImGui.End(); @@ -515,10 +519,10 @@ namespace PeepingTom { ImGui.EndTooltip(); } - private void AddEntry(Targeter targeter, Actor? actor, ref bool anyHovered, ImGuiSelectableFlags flags = ImGuiSelectableFlags.None) { + private void AddEntry(Targeter targeter, GameObject? obj, ref bool anyHovered, ImGuiSelectableFlags flags = ImGuiSelectableFlags.None) { ImGui.BeginGroup(); - ImGui.Selectable(targeter.Name, false, flags); + ImGui.Selectable(targeter.Name.TextValue, false, flags); if (this.Plugin.Config.ShowTimestamps) { var time = DateTime.UtcNow - targeter.When >= TimeSpan.FromDays(1) @@ -543,48 +547,44 @@ namespace PeepingTom { var left = hover && ImGui.IsMouseClicked(ImGuiMouseButton.Left); var right = hover && ImGui.IsMouseClicked(ImGuiMouseButton.Right); - actor ??= this.Plugin.Interface.ClientState.Actors - .FirstOrDefault(a => a.ActorId == targeter.ActorId); + obj ??= this.Plugin.ObjectTable.FirstOrDefault(a => a.ObjectId == targeter.ObjectId); // don't count as hovered if the actor isn't here (clears focus target when hovering missing actors) - if (actor != null) { + if (obj != null) { anyHovered |= hover; } - if (this.Plugin.Config.FocusTargetOnHover && hover && actor != null) { - if (!this.PreviousFocus.Present) { - this.PreviousFocus = new Optional(this.Plugin.Interface.ClientState.Targets.FocusTarget); - } - - this.Plugin.Interface.ClientState.Targets.SetFocusTarget(actor); + if (this.Plugin.Config.FocusTargetOnHover && hover && obj != null) { + this.PreviousFocus ??= this.Plugin.TargetManager.FocusTarget?.ObjectId ?? uint.MaxValue; + this.Plugin.TargetManager.FocusTarget = obj; } if (left) { if (this.Plugin.Config.OpenExamine && ImGui.GetIO().KeyAlt) { - if (actor != null) { - this.Plugin.Common.Functions.Examine.OpenExamineWindow(actor); + if (obj != null) { + this.Plugin.Common.Functions.Examine.OpenExamineWindow(obj); } else { var error = string.Format(Language.ExamineErrorToast, targeter.Name); - this.Plugin.Interface.Framework.Gui.Toast.ShowError(error); + this.Plugin.ToastGui.ShowError(error); } } else { - var payload = new PlayerPayload(this.Plugin.Interface.Data, targeter.Name, targeter.HomeWorld.Id); - Payload[] payloads = {payload}; - this.Plugin.Interface.Framework.Gui.Chat.PrintChat(new XivChatEntry { - MessageBytes = new SeString(payloads).Encode(), + var payload = new PlayerPayload(targeter.Name.TextValue, targeter.HomeWorldId); + Payload[] payloads = { payload }; + this.Plugin.ChatGui.PrintChat(new XivChatEntry { + Message = new SeString(payloads), }); } - } else if (right && actor != null) { - this.Plugin.Interface.ClientState.Targets.SetCurrentTarget(actor); + } else if (right && obj != null) { + this.Plugin.TargetManager.Target = obj; } } - private void MarkPlayer(Actor? player, Vector4 colour, float size) { + private void MarkPlayer(GameObject? player, Vector4 colour, float size) { if (player == null) { return; } - if (!this.Plugin.Interface.Framework.Gui.WorldToScreen(player.Position, out var screenPos)) { + if (!this.Plugin.GameGui.WorldToScreen(player.Position, out var screenPos)) { return; } @@ -601,18 +601,18 @@ namespace PeepingTom { } private PlayerCharacter? GetCurrentTarget() { - var player = this.Plugin.Interface.ClientState.LocalPlayer; + var player = this.Plugin.ClientState.LocalPlayer; if (player == null) { return null; } - var targetId = player.TargetActorID; + var targetId = player.TargetObjectId; if (targetId <= 0) { return null; } - return this.Plugin.Interface.ClientState.Actors - .Where(actor => actor.ActorId == targetId && actor is PlayerCharacter) + return this.Plugin.ObjectTable + .Where(actor => actor.ObjectId == targetId && actor is PlayerCharacter) .Select(actor => actor as PlayerCharacter) .FirstOrDefault(); } diff --git a/Peeping Tom/TargetWatcher.cs b/Peeping Tom/TargetWatcher.cs index a1703a2..516173c 100644 --- a/Peeping Tom/TargetWatcher.cs +++ b/Peeping Tom/TargetWatcher.cs @@ -1,7 +1,4 @@ -using Dalamud.Game.ClientState.Actors.Types; -using Dalamud.Game.Internal; -using Dalamud.Plugin; -using NAudio.Wave; +using NAudio.Wave; using Resourcer; using System; using System.Collections.Generic; @@ -9,110 +6,78 @@ using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; +using Dalamud.Game; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.Text; -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Game.Text.SeStringHandling.Payloads; +using PeepingTom.Ipc; using PeepingTom.Resources; namespace PeepingTom { internal class TargetWatcher : IDisposable { private PeepingTomPlugin Plugin { get; } - private Stopwatch? Watch { get; set; } + private Stopwatch UpdateWatch { get; } = new(); + private Stopwatch? SoundWatch { get; set; } private int LastTargetAmount { get; set; } - private volatile bool _stop; - private volatile bool _needsUpdate = true; - private Thread? Thread { get; set; } - - private readonly object _dataMutex = new(); - private TargetThreadData? Data { get; set; } - - private readonly Mutex _currentMutex = new(); private Targeter[] Current { get; set; } = Array.Empty(); - public IReadOnlyCollection CurrentTargeters { - get { - this._currentMutex.WaitOne(); - var current = this.Current.ToArray(); - this._currentMutex.ReleaseMutex(); - return current; - } - } + public IReadOnlyCollection CurrentTargeters => this.Current; - private readonly Mutex _previousMutex = new(); private List Previous { get; } = new(); - public IReadOnlyCollection PreviousTargeters { - get { - this._previousMutex.WaitOne(); - var previous = this.Previous.ToArray(); - this._previousMutex.ReleaseMutex(); - return previous; - } - } + public IReadOnlyCollection PreviousTargeters => this.Previous; public TargetWatcher(PeepingTomPlugin plugin) { this.Plugin = plugin; + this.UpdateWatch.Start(); + + this.Plugin.Framework.Update += this.OnFrameworkUpdate; + } + + public void Dispose() { + this.Plugin.Framework.Update -= this.OnFrameworkUpdate; } public void ClearPrevious() { - this._previousMutex.WaitOne(); this.Previous.Clear(); - this._previousMutex.ReleaseMutex(); } - public void StartThread() { - this.Thread = new Thread(() => { - 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(); - } - - public void OnFrameworkUpdate(Framework framework) { - if (!this._needsUpdate || this.Plugin.InPvp) { + private void OnFrameworkUpdate(Framework framework) { + if (this.Plugin.InPvp) { return; } - lock (this._dataMutex) { - this.Data = new TargetThreadData(this.Plugin.Interface); + if (this.UpdateWatch.Elapsed > TimeSpan.FromMilliseconds(this.Plugin.Config.PollFrequency)) { + this.Update(); } - - this._needsUpdate = false; } private void Update() { - lock (this._dataMutex) { - var 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 - var current = this.GetTargeting(this.Data!.Actors, player); - this.Current = (Targeter[]) current.Clone(); - - // release - this._currentMutex.ReleaseMutex(); + var player = this.Plugin.ClientState.LocalPlayer; + if (player == null) { + return; } + // get targeters and set a copy so we can release the mutex faster + var newCurrent = this.GetTargeting(this.Plugin.ObjectTable, player); + + foreach (var newTargeter in newCurrent.Where(t => this.Current.All(c => c.ObjectId != t.ObjectId))) { + this.Plugin.IpcManager.SendNewTargeter(newTargeter); + } + + foreach (var stopped in this.Current.Where(t => newCurrent.All(c => c.ObjectId != t.ObjectId))) { + this.Plugin.IpcManager.SendStoppedTargeting(stopped); + } + + this.Current = newCurrent; + this.HandleHistory(this.Current); // play sound if necessary if (this.CanPlaySound()) { - this.Watch?.Restart(); + this.SoundWatch?.Restart(); this.PlaySound(); } @@ -124,47 +89,44 @@ namespace PeepingTom { return; } - this._previousMutex.WaitOne(); - foreach (var targeter in targeting) { // add the targeter to the previous list - if (this.Previous.Any(old => old.ActorId == targeter.ActorId)) { - this.Previous.RemoveAll(old => old.ActorId == targeter.ActorId); + if (this.Previous.Any(old => old.ObjectId == targeter.ObjectId)) { + this.Previous.RemoveAll(old => old.ObjectId == targeter.ObjectId); } this.Previous.Insert(0, targeter); } // only keep the configured number of previous targeters (ignoring ones that are currently targeting) - while (this.Previous.Count(old => targeting.All(actor => actor.ActorId != old.ActorId)) > this.Plugin.Config.NumHistory) { + while (this.Previous.Count(old => targeting.All(actor => actor.ObjectId != old.ObjectId)) > this.Plugin.Config.NumHistory) { this.Previous.RemoveAt(this.Previous.Count - 1); } - - this._previousMutex.ReleaseMutex(); } - private Targeter[] GetTargeting(IEnumerable actors, Actor player) { - return actors - .Where(actor => actor.TargetActorID == player.ActorId && actor is PlayerCharacter) + private Targeter[] GetTargeting(IEnumerable objects, GameObject player) { + return objects + .Where(obj => obj.TargetObjectId == player.ObjectId && obj is PlayerCharacter) + // .Where(obj => Marshal.ReadByte(obj.Address + ActorOffsets.PlayerCharacterTargetActorId + 4) == 0) .Cast() .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) + .Where(actor => this.Plugin.Config.LogSelf || actor.ObjectId != player.ObjectId) .Select(actor => new Targeter(actor)) .ToArray(); } - private static byte GetStatus(Actor actor) { + private static byte GetStatus(GameObject actor) { var statusPtr = actor.Address + 0x1980; // updated 5.4 return Marshal.ReadByte(statusPtr); } - private static bool InCombat(Actor actor) => (GetStatus(actor) & 2) > 0; + private static bool InCombat(GameObject actor) => (GetStatus(actor) & 2) > 0; - private static bool InParty(Actor actor) => (GetStatus(actor) & 16) > 0; + private static bool InParty(GameObject actor) => (GetStatus(actor) & 16) > 0; - private static bool InAlliance(Actor actor) => (GetStatus(actor) & 32) > 0; + private static bool InAlliance(GameObject actor) => (GetStatus(actor) & 32) > 0; private bool CanPlaySound() { if (!this.Plugin.Config.PlaySoundOnTarget) { @@ -179,12 +141,12 @@ namespace PeepingTom { return false; } - if (this.Watch == null) { - this.Watch = new Stopwatch(); + if (this.SoundWatch == null) { + this.SoundWatch = new Stopwatch(); return true; } - var secs = this.Watch.Elapsed.TotalSeconds; + var secs = this.SoundWatch.Elapsed.TotalSeconds; return secs >= this.Plugin.Config.SoundCooldown; } @@ -228,28 +190,10 @@ namespace PeepingTom { } private void SendError(string message) { - var payloads = new Payload[] { - new TextPayload($"[{this.Plugin.Name}] {message}"), - }; - this.Plugin.Interface.Framework.Gui.Chat.PrintChat(new XivChatEntry { - MessageBytes = new SeString(payloads).Encode(), + this.Plugin.ChatGui.PrintChat(new XivChatEntry { + Message = $"[{this.Plugin.Name}] {message}", Type = XivChatType.ErrorMessage, }); } - - public void Dispose() { - this._currentMutex.Dispose(); - this._previousMutex.Dispose(); - } - } - - internal class TargetThreadData { - public PlayerCharacter LocalPlayer { get; } - public Actor[] Actors { get; } - - public TargetThreadData(DalamudPluginInterface pi) { - this.LocalPlayer = pi.ClientState.LocalPlayer; - this.Actors = pi.ClientState.Actors.ToArray(); - } } } diff --git a/Peeping Tom/Targeting.cs b/Peeping Tom/Targeting.cs deleted file mode 100644 index 3a9cb39..0000000 --- a/Peeping Tom/Targeting.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Dalamud.Game.ClientState.Actors.Resolvers; -using Dalamud.Game.ClientState.Actors.Types; -using Dalamud.Plugin; -using System; -using System.Linq; - -namespace PeepingTom { - public class Targeter { - public string Name { get; } - public World HomeWorld { get; } - public int ActorId { get; } - public DateTime When { get; } - - public Targeter(PlayerCharacter character) { - this.Name = character.Name; - this.HomeWorld = character.HomeWorld; - this.ActorId = character.ActorId; - this.When = DateTime.UtcNow; - } - - public PlayerCharacter? GetPlayerCharacter(DalamudPluginInterface pi) { - return pi.ClientState.Actors.FirstOrDefault(actor => actor.ActorId == this.ActorId && actor is PlayerCharacter) as PlayerCharacter; - } - } -} diff --git a/Peeping Tom/Util.cs b/Peeping Tom/Util.cs deleted file mode 100644 index b605366..0000000 --- a/Peeping Tom/Util.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace PeepingTom { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords")] - public class Optional where T : class { - public bool Present { get; } - private readonly T? _value; - - public Optional(T? value) { - this._value = value; - this.Present = true; - } - - public Optional() { - this.Present = false; - } - - public bool Get(out T? value) { - value = this._value; - return this.Present; - } - } -} diff --git a/README.md b/README.md index 448cfdd..bfde9a3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # Peeping Tom This plugin for FFXIVLauncher shows who was or currently is targeting you. + +Icon: Eyes emoji from Twemoji 2.4 diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..aaec6f7ac4fc0ea4ea7e82a84763cea58721f8ec GIT binary patch literal 12361 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 literal 0 HcmV?d00001 diff --git a/icon.svg b/icon.svg new file mode 100755 index 0000000..cb722e8 --- /dev/null +++ b/icon.svg @@ -0,0 +1,69 @@ + +image/svg+xml