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 0000000..aaec6f7 Binary files /dev/null and b/icon.png differ 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