refactor: move to net5

This commit is contained in:
Anna 2021-08-22 23:33:57 -04:00
parent b451db08f6
commit 15a1674ed2
Signed by: anna
GPG Key ID: 0B391D8F06FCD9E0
24 changed files with 458 additions and 270 deletions

1
.gitattributes vendored
View File

@ -1,2 +1,3 @@
* text eol=lf
*.wav binary
*.png binary

View File

@ -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;
}
}
}

View File

@ -0,0 +1,5 @@
namespace PeepingTom.Ipc.From {
public interface IFromMessage {
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

18
Peeping Tom.Ipc/IpcInfo.cs Executable file
View File

@ -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<IToMessage, object> GetProvider(DalamudPluginInterface @interface) {
return @interface.GetIpcProvider<IToMessage, object>(ToRegistrationName);
}
public static ICallGateSubscriber<IFromMessage, object> GetSubscriber(DalamudPluginInterface @interface) {
return @interface.GetIpcSubscriber<IFromMessage, object>(FromRegistrationName);
}
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0-windows</TargetFramework>
<Version>1.0.0</Version>
<Nullable>enable</Nullable>
<RootNamespace>PeepingTom.Ipc</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>

26
Peeping Tom.Ipc/Targeter.cs Executable file
View File

@ -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;
}
}
}

View File

@ -0,0 +1,5 @@
namespace PeepingTom.Ipc.To {
public interface IToMessage {
}
}

View File

@ -0,0 +1,7 @@
using System;
namespace PeepingTom.Ipc.To {
[Serializable]
public class RequestTargetersMessage : IToMessage {
}
}

6
Peeping Tom.sln Normal file → Executable file
View File

@ -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

View File

@ -4,7 +4,6 @@
OutputPath="$(OutputPath)"
AssemblyName="$(AssemblyName)"
VersionComponents="3"
Include="PeepingTom.dll;PeepingTom.json;PeepingTom.pdb"
MakeZip="true"/>
</Target>
</Project>

View File

@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Resourcer/>
<ResourcesMerge/>
<ILMerge ExcludeResources="^NAudio\.WinForms\..+"/>
</Weavers>

54
Peeping Tom/IpcManager.cs Executable file
View File

@ -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<IFromMessage, object> Provider { get; }
private ICallGateSubscriber<IToMessage, object> Subscriber { get; }
internal IpcManager(PeepingTomPlugin plugin) {
this.Plugin = plugin;
this.Provider = this.Plugin.Interface.GetIpcProvider<IFromMessage, object>(IpcInfo.FromRegistrationName);
this.Subscriber = this.Plugin.Interface.GetIpcSubscriber<IToMessage, object>(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;
}
}
}
}
}

View File

@ -1,13 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<TargetFramework>net5-windows</TargetFramework>
<RootNamespace>PeepingTom</RootNamespace>
<Version>1.7.5</Version>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<AssemblyName>PeepingTom</AssemblyName>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\target.wav"/>
@ -37,19 +39,16 @@
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="SharpDX.Mathematics">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\SharpDX.Mathematics.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="1.2.1"/>
<PackageReference Include="Fody" Version="6.5.1" PrivateAssets="all"/>
<PackageReference Include="ILMerge.Fody" Version="1.16.0" PrivateAssets="all"/>
<PackageReference Include="NAudio" Version="2.0.0"/>
<PackageReference Include="DalamudPackager" Version="2.1.2"/>
<PackageReference Include="Fody" Version="6.5.2" PrivateAssets="all"/>
<PackageReference Include="NAudio" Version="2.0.1"/>
<PackageReference Include="Resourcer.Fody" Version="1.8.0" PrivateAssets="all"/>
<PackageReference Include="ResourcesMerge.Fody" Version="1.0.1" PrivateAssets="all"/>
<PackageReference Include="XivCommon" Version="1.5.0"/>
<PackageReference Include="XivCommon" Version="3.0.1"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Peeping Tom.Ipc\Peeping Tom.Ipc.csproj"/>
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\Language.Designer.cs">
@ -58,4 +57,7 @@
<DependentUpon>Language.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Content Include="..\icon.png" Link="images/icon.png" CopyToOutputDirectory="PreserveNewest" Visible="false"/>
</ItemGroup>
</Project>

View File

@ -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

View File

@ -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<TerritoryType>().GetRow(e);
this.InPvp = territory.IsPvpZone;
var territory = this.DataManager.GetExcelSheet<TerritoryType>()!.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;
}
}

View File

@ -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<Actor> 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<PlayerCharacter>()
.ToArray();
@ -377,10 +380,10 @@ namespace PeepingTom {
.Where(actor => actor.TargetActorID == player.ActorId && actor is PlayerCharacter)
.Cast<PlayerCharacter>();
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<int, Actor>? actors = null;
Dictionary<uint, GameObject>? objects = null;
if (targeting.Count + (previousTargeters?.Count ?? 0) > 1) {
var dict = new Dictionary<int, Actor>();
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<uint, GameObject>();
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<Targeter>())
.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<Actor>();
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<Actor>(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();
}

View File

@ -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<Targeter>();
public IReadOnlyCollection<Targeter> CurrentTargeters {
get {
this._currentMutex.WaitOne();
var current = this.Current.ToArray();
this._currentMutex.ReleaseMutex();
return current;
}
}
public IReadOnlyCollection<Targeter> CurrentTargeters => this.Current;
private readonly Mutex _previousMutex = new();
private List<Targeter> Previous { get; } = new();
public IReadOnlyCollection<Targeter> PreviousTargeters {
get {
this._previousMutex.WaitOne();
var previous = this.Previous.ToArray();
this._previousMutex.ReleaseMutex();
return previous;
}
}
public IReadOnlyCollection<Targeter> 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<Actor> actors, Actor player) {
return actors
.Where(actor => actor.TargetActorID == player.ActorId && actor is PlayerCharacter)
private Targeter[] GetTargeting(IEnumerable<GameObject> 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<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)
.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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -1,21 +0,0 @@
namespace PeepingTom {
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords")]
public class Optional<T> 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;
}
}
}

View File

@ -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

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

69
icon.svg Executable file
View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 47.5 47.5"
style="enable-background:new 0 0 47.5 47.5;"
xml:space="preserve"
version="1.1"
id="svg2"
sodipodi:docname="icon.svg"
inkscape:export-filename="D:\code\PeepingTom\icon.png"
inkscape:export-xdpi="1023.69"
inkscape:export-ydpi="1023.69"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
id="namedview18"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
showgrid="false"
inkscape:zoom="17.515789"
inkscape:cx="23.721454"
inkscape:cy="23.778546"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1592"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" /><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs6"><clipPath
id="clipPath16"
clipPathUnits="userSpaceOnUse"><path
id="path18"
d="M 0,38 38,38 38,0 0,0 0,38 Z" /></clipPath></defs><g
transform="matrix(1.25,0,0,-1.25,0,47.5)"
id="g10"><g
id="g12"><g
clip-path="url(#clipPath16)"
id="g14"><g
transform="translate(18,19)"
id="g20"><path
id="path22"
style="fill:#e1e8ed;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 0,0 c 0,-5.522 -3.806,-10 -8.5,-10 -4.694,0 -8.5,4.478 -8.5,10 0,5.523 3.806,10 8.5,10 C -3.806,10 0,5.523 0,0" /></g><g
transform="translate(37,19)"
id="g24"><path
id="path26"
style="fill:#e1e8ed;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 0,0 c 0,-5.522 -3.806,-10 -8.5,-10 -4.694,0 -8.5,4.478 -8.5,10 0,5.523 3.806,10 8.5,10 C -3.806,10 0,5.523 0,0" /></g><g
transform="translate(9,15)"
id="g28"><path
id="path30"
style="fill:#292f33;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 0,0 c 0,-1.657 -1.343,-3 -3,-3 -1.657,0 -3,1.343 -3,3 0,1.657 1.343,3 3,3 1.657,0 3,-1.343 3,-3" /></g><g
transform="translate(28,15)"
id="g32"><path
id="path34"
style="fill:#292f33;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 0,0 c 0,-1.657 -1.344,-3 -3,-3 -1.656,0 -3,1.343 -3,3 0,1.657 1.344,3 3,3 1.656,0 3,-1.343 3,-3" /></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB