From b0d14751cdfea10464214f611e1bfb268bf52b87 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 25 Jun 2021 16:43:41 +0200 Subject: [PATCH] Moved Hooks to Interop, added PlayerWatcher that keeps tabs on specific actors and their equip state by name and triggers an event when equip changes. --- Penumbra/Configuration.cs | 1 + Penumbra/Game/CharEquipment.cs | 148 ++++++++++++++++++ Penumbra/{Game => Interop}/ActorRefresher.cs | 2 +- .../GameResourceManagement.cs | 2 +- Penumbra/{Hooks => Interop}/MusicManager.cs | 2 +- Penumbra/Interop/PlayerWatcher.cs | 102 ++++++++++++ Penumbra/{Hooks => Interop}/ResourceLoader.cs | 2 +- Penumbra/Meta/MetaManager.cs | 2 +- Penumbra/Plugin.cs | 10 +- Penumbra/UI/MenuTabs/TabCollections.cs | 2 +- Penumbra/UI/MenuTabs/TabSettings.cs | 15 +- 11 files changed, 280 insertions(+), 8 deletions(-) create mode 100644 Penumbra/Game/CharEquipment.cs rename Penumbra/{Game => Interop}/ActorRefresher.cs (99%) rename Penumbra/{Hooks => Interop}/GameResourceManagement.cs (99%) rename Penumbra/{Hooks => Interop}/MusicManager.cs (98%) create mode 100644 Penumbra/Interop/PlayerWatcher.cs rename Penumbra/{Hooks => Interop}/ResourceLoader.cs (99%) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 4760f258..0ce12f15 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -19,6 +19,7 @@ namespace Penumbra public bool DisableFileSystemNotifications { get; set; } public bool EnableHttpApi { get; set; } + public bool EnableActorWatch { get; set; } = false; public string ModDirectory { get; set; } = @"D:/ffxiv/fs_mods/"; diff --git a/Penumbra/Game/CharEquipment.cs b/Penumbra/Game/CharEquipment.cs new file mode 100644 index 00000000..2b1e3ede --- /dev/null +++ b/Penumbra/Game/CharEquipment.cs @@ -0,0 +1,148 @@ +using System; +using System.Runtime.InteropServices; +using Dalamud.Game.ClientState.Actors.Types; + +// Read the customization data regarding weapons and displayable equipment from an actor struct. +// Stores the data in a 56 bytes, i.e. 7 longs for easier comparison. +namespace Penumbra.Game +{ + [StructLayout( LayoutKind.Sequential, Pack = 1 )] + public class CharEquipment + { + [StructLayout( LayoutKind.Sequential, Pack = 1 )] + private readonly struct Weapon + { + public readonly ushort _1; + public readonly ushort _2; + public readonly ushort _3; + public readonly byte _4; + + public override string ToString() + => $"{_1},{_2},{_3},{_4}"; + } + + [StructLayout( LayoutKind.Sequential, Pack = 1 )] + private readonly struct Equip + { + public readonly ushort _1; + public readonly byte _2; + public readonly byte _3; + + public override string ToString() + => $"{_1},{_2},{_3}"; + } + + private const int MainWeaponOffset = 0x0F08; + private const int OffWeaponOffset = 0x0F70; + private const int EquipmentOffset = 0x1040; + private const int EquipmentSlots = 10; + private const int WeaponSlots = 2; + + private readonly ushort IsSet; // Also fills struct size to 56, a multiple of 8. + private readonly Weapon Mainhand; + private readonly Weapon Offhand; + private readonly Equip Head; + private readonly Equip Body; + private readonly Equip Hands; + private readonly Equip Legs; + private readonly Equip Feet; + private readonly Equip Ear; + private readonly Equip Neck; + private readonly Equip Wrist; + private readonly Equip LFinger; + private readonly Equip RFinger; + + public CharEquipment() + => Clear(); + + public CharEquipment( Actor actor ) + : this( actor.Address ) + { } + + public override string ToString() + => IsSet == 0 + ? "(Not Set)" + : $"({Mainhand}) | ({Offhand}) | ({Head}) | ({Body}) | ({Hands}) | ({Legs}) | " + + $"({Feet}) | ({Ear}) | ({Neck}) | ({Wrist}) | ({LFinger}) | ({RFinger})"; + + public bool Equal( Actor rhs ) + => CompareData( new CharEquipment( rhs ) ); + + public bool Equal( CharEquipment rhs ) + => CompareData( rhs ); + + public bool CompareAndUpdate( Actor rhs ) + => CompareAndOverwrite( new CharEquipment( rhs ) ); + + public bool CompareAndUpdate( CharEquipment rhs ) + => CompareAndOverwrite( rhs ); + + private unsafe CharEquipment( IntPtr actorAddress ) + { + IsSet = 1; + var actorPtr = ( byte* )actorAddress.ToPointer(); + fixed( Weapon* main = &Mainhand, off = &Offhand ) + { + Buffer.MemoryCopy( actorPtr + MainWeaponOffset, main, sizeof( Weapon ), sizeof( Weapon ) ); + Buffer.MemoryCopy( actorPtr + OffWeaponOffset, off, sizeof( Weapon ), sizeof( Weapon ) ); + } + + fixed( Equip* equipment = &Head ) + { + Buffer.MemoryCopy( actorPtr + EquipmentOffset, equipment, EquipmentSlots * sizeof( Equip ), EquipmentSlots * sizeof( Equip ) ); + } + } + + public unsafe void Clear() + { + fixed( Weapon* main = &Mainhand ) + { + var structSizeEights = ( EquipmentSlots * sizeof( Equip ) + WeaponSlots * sizeof( Weapon ) ) / 8; + for( ulong* ptr = ( ulong* )main, end = ptr + structSizeEights; ptr != end; ++ptr ) + { + *ptr = 0; + } + } + } + + private unsafe bool CompareAndOverwrite( CharEquipment rhs ) + { + var structSizeHalf = ( EquipmentSlots * sizeof( Equip ) + WeaponSlots * sizeof( Weapon ) ) / 8; + var ret = true; + fixed( Weapon* data1 = &Mainhand, data2 = &rhs.Mainhand ) + { + var ptr1 = ( ulong* )data1; + var ptr2 = ( ulong* )data2; + for( var end = ptr1 + structSizeHalf; ptr1 != end; ++ptr1, ++ptr2 ) + { + if( *ptr1 != *ptr2 ) + { + *ptr1 = *ptr2; + ret = false; + } + } + } + + return ret; + } + + private unsafe bool CompareData( CharEquipment rhs ) + { + var structSizeHalf = ( EquipmentSlots * sizeof( Equip ) + WeaponSlots * sizeof( Weapon ) ) / 8; + fixed( Weapon* data1 = &Mainhand, data2 = &rhs.Mainhand ) + { + var ptr1 = ( ulong* )data1; + var ptr2 = ( ulong* )data2; + for( var end = ptr1 + structSizeHalf; ptr1 != end; ++ptr1, ++ptr2 ) + { + if( *ptr1 != *ptr2 ) + { + return false; + } + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/Penumbra/Game/ActorRefresher.cs b/Penumbra/Interop/ActorRefresher.cs similarity index 99% rename from Penumbra/Game/ActorRefresher.cs rename to Penumbra/Interop/ActorRefresher.cs index 03049f79..1c5172c8 100644 --- a/Penumbra/Game/ActorRefresher.cs +++ b/Penumbra/Interop/ActorRefresher.cs @@ -7,7 +7,7 @@ using Dalamud.Game.ClientState.Actors.Types; using Dalamud.Plugin; using Penumbra.Mods; -namespace Penumbra.Game +namespace Penumbra.Interop { public enum Redraw { diff --git a/Penumbra/Hooks/GameResourceManagement.cs b/Penumbra/Interop/GameResourceManagement.cs similarity index 99% rename from Penumbra/Hooks/GameResourceManagement.cs rename to Penumbra/Interop/GameResourceManagement.cs index 0d5db16d..a0f00ecc 100644 --- a/Penumbra/Hooks/GameResourceManagement.cs +++ b/Penumbra/Interop/GameResourceManagement.cs @@ -4,7 +4,7 @@ using Dalamud.Plugin; using Penumbra.Structs; using Reloaded.Hooks.Definitions.X64; -namespace Penumbra.Hooks +namespace Penumbra.Interop { public class GameResourceManagement { diff --git a/Penumbra/Hooks/MusicManager.cs b/Penumbra/Interop/MusicManager.cs similarity index 98% rename from Penumbra/Hooks/MusicManager.cs rename to Penumbra/Interop/MusicManager.cs index 9778d27e..987c7f25 100644 --- a/Penumbra/Hooks/MusicManager.cs +++ b/Penumbra/Interop/MusicManager.cs @@ -1,7 +1,7 @@ using System; using Dalamud.Plugin; -namespace Penumbra.Hooks +namespace Penumbra.Interop { // Use this to disable streaming of specific soundfiles, // which will allow replacement of .scd files. diff --git a/Penumbra/Interop/PlayerWatcher.cs b/Penumbra/Interop/PlayerWatcher.cs new file mode 100644 index 00000000..b049a0df --- /dev/null +++ b/Penumbra/Interop/PlayerWatcher.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using Dalamud.Game.ClientState.Actors; +using Dalamud.Game.ClientState.Actors.Types; +using Dalamud.Plugin; +using Penumbra.Game; + +namespace Penumbra.Interop +{ + public class PlayerWatcher : IDisposable + { + private const int ActorsPerFrame = 4; + + private readonly DalamudPluginInterface _pi; + private readonly Dictionary< string, CharEquipment > _equip = new(); + private int _frameTicker; + + public PlayerWatcher( DalamudPluginInterface pi ) + => _pi = pi; + + public delegate void ActorChange( Actor which ); + public event ActorChange? ActorChanged; + + public void AddPlayerToWatch( string playerName ) + { + if( !_equip.ContainsKey( playerName ) ) + { + _equip[ playerName ] = new CharEquipment(); + } + } + + public void SetActorWatch( bool on ) + { + if( on ) + { + EnableActorWatch(); + } + else + { + DisableActorWatch(); + } + } + + public void EnableActorWatch() + { + _pi.Framework.OnUpdateEvent += OnFrameworkUpdate; + _pi.ClientState.TerritoryChanged += OnTerritoryChange; + _pi.ClientState.OnLogout += OnLogout; + } + + public void DisableActorWatch() + { + _pi.Framework.OnUpdateEvent -= OnFrameworkUpdate; + _pi.ClientState.TerritoryChanged -= OnTerritoryChange; + _pi.ClientState.OnLogout -= OnLogout; + } + + public void Dispose() + => DisableActorWatch(); + + public void OnTerritoryChange( object _1, ushort _2 ) + => Clear(); + + public void OnLogout( object _1, object _2 ) + => Clear(); + + public void Clear() + { + foreach( var kvp in _equip ) + { + kvp.Value.Clear(); + } + + _frameTicker = 0; + } + + public void OnFrameworkUpdate( object framework ) + { + var actors = _pi.ClientState.Actors; + for( var i = 0; i < ActorsPerFrame; ++i ) + { + _frameTicker = _frameTicker < actors.Length - 2 + ? _frameTicker + 2 + : 0; + + var actor = actors[ _frameTicker ]; + if( actor == null + || actor.ObjectKind != ObjectKind.Player + || actor.Name == null + || actor.Name.Length == 0 ) + { + continue; + } + + if( _equip.TryGetValue( actor.Name, out var equip ) && !equip.CompareAndUpdate( actor ) ) + { + ActorChanged?.Invoke( actor ); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Hooks/ResourceLoader.cs b/Penumbra/Interop/ResourceLoader.cs similarity index 99% rename from Penumbra/Hooks/ResourceLoader.cs rename to Penumbra/Interop/ResourceLoader.cs index 199038be..04d2fb2b 100644 --- a/Penumbra/Hooks/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoader.cs @@ -12,7 +12,7 @@ using Reloaded.Hooks.Definitions; using Reloaded.Hooks.Definitions.X64; using FileMode = Penumbra.Structs.FileMode; -namespace Penumbra.Hooks +namespace Penumbra.Interop { public class ResourceLoader : IDisposable { diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs index 3fc80de3..9551d8f4 100644 --- a/Penumbra/Meta/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -4,7 +4,7 @@ using System.IO; using System.Linq; using Dalamud.Plugin; using Lumina.Data.Files; -using Penumbra.Hooks; +using Penumbra.Interop; using Penumbra.Meta.Files; using Penumbra.Util; diff --git a/Penumbra/Plugin.cs b/Penumbra/Plugin.cs index 7205ff89..bf0d6236 100644 --- a/Penumbra/Plugin.cs +++ b/Penumbra/Plugin.cs @@ -6,7 +6,7 @@ using EmbedIO; using EmbedIO.WebApi; using Penumbra.API; using Penumbra.Game; -using Penumbra.Hooks; +using Penumbra.Interop; using Penumbra.Meta.Files; using Penumbra.Mods; using Penumbra.UI; @@ -33,6 +33,7 @@ namespace Penumbra public SettingsInterface SettingsInterface { get; set; } = null!; public MusicManager SoundShit { get; set; } = null!; public ActorRefresher ActorRefresher { get; set; } = null!; + public PlayerWatcher PlayerWatcher { get; set; } = null!; private WebServer? _webServer; @@ -53,6 +54,7 @@ namespace Penumbra modManager.DiscoverMods(); ActorRefresher = new ActorRefresher( PluginInterface, modManager ); + PlayerWatcher = new PlayerWatcher( PluginInterface ); ResourceLoader = new ResourceLoader( this ); @@ -74,6 +76,11 @@ namespace Penumbra { CreateWebServer(); } + + if( Configuration.EnableActorWatch ) + { + PlayerWatcher.EnableActorWatch(); + } } public void CreateWebServer() @@ -103,6 +110,7 @@ namespace Penumbra public void Dispose() { ActorRefresher.Dispose(); + PlayerWatcher.Dispose(); PluginInterface.UiBuilder.OnBuildUi -= SettingsInterface.Draw; PluginInterface.CommandManager.RemoveHandler( CommandName ); diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index 7a124ddc..4f1ce388 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Numerics; using Dalamud.Plugin; using ImGuiNET; -using Penumbra.Hooks; +using Penumbra.Interop; using Penumbra.Mod; using Penumbra.Mods; using Penumbra.Util; diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index df29fde4..755ef0a6 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Text.RegularExpressions; using Dalamud.Plugin; using ImGuiNET; -using Penumbra.Hooks; +using Penumbra.Interop; using Penumbra.Util; namespace Penumbra.UI @@ -17,6 +17,7 @@ namespace Penumbra.UI private const string LabelRediscoverButton = "Rediscover Mods"; private const string LabelOpenFolder = "Open Mods Folder"; private const string LabelEnabled = "Enable Mods"; + private const string LabelEnabledPlayerWatch = "Enable automatic Character Redraws"; private const string LabelShowAdvanced = "Show Advanced Settings"; private const string LabelLogLoadedFiles = "Log all loaded files"; private const string LabelDisableNotifications = "Disable filesystem change notifications"; @@ -131,6 +132,17 @@ namespace Penumbra.UI } } + private void DrawEnabledPlayerWatcher() + { + var enabled = _config.EnableActorWatch; + if( ImGui.Checkbox( LabelEnabledPlayerWatch, ref enabled ) ) + { + _config.EnableActorWatch = enabled; + _configChanged = true; + _base._plugin.PlayerWatcher.SetActorWatch( enabled ); + } + } + private static void DrawReloadResourceButton() { if( ImGui.Button( LabelReloadResource ) ) @@ -163,6 +175,7 @@ namespace Penumbra.UI Custom.ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); DrawEnabledBox(); + DrawEnabledPlayerWatcher(); Custom.ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); DrawShowAdvancedBox();