diff --git a/Glamourer.GameData/CharacterEquipExtensions.cs b/Glamourer.GameData/CharacterEquipExtensions.cs index 37bd606..efcb54e 100644 --- a/Glamourer.GameData/CharacterEquipExtensions.cs +++ b/Glamourer.GameData/CharacterEquipExtensions.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.PlayerWatch; namespace Glamourer { diff --git a/Glamourer.GameData/Customization/CharaMakeParams.cs b/Glamourer.GameData/Customization/CharaMakeParams.cs index 4ac224f..8e00ed4 100644 --- a/Glamourer.GameData/Customization/CharaMakeParams.cs +++ b/Glamourer.GameData/Customization/CharaMakeParams.cs @@ -48,10 +48,10 @@ public class CharaMakeParams : ExcelRow public sbyte Gender { get; set; } - public Menu[] Menus { get; set; } = new Menu[NumMenus]; - public byte[] Voices { get; set; } = new byte[NumVoices]; - public FacialFeatures[] FacialFeatureByFace { get; set; } = new FacialFeatures[NumFaces]; - public CharaMakeType.UnkData3347Obj[] Equip { get; set; } = new CharaMakeType.UnkData3347Obj[NumEquip]; + public Menu[] Menus { get; set; } = new Menu[NumMenus]; + public byte[] Voices { get; set; } = new byte[NumVoices]; + public FacialFeatures[] FacialFeatureByFace { get; set; } = new FacialFeatures[NumFaces]; + public CharaMakeType.CharaMakeTypeUnkData3347Obj[] Equip { get; set; } = new CharaMakeType.CharaMakeTypeUnkData3347Obj[NumEquip]; public override void PopulateData(RowParser parser, Lumina.GameData gameData, Language language) { @@ -103,7 +103,7 @@ public class CharaMakeParams : ExcelRow for (var i = 0; i < NumEquip; ++i) { - Equip[i] = new CharaMakeType.UnkData3347Obj() + Equip[i] = new CharaMakeType.CharaMakeTypeUnkData3347Obj() { Helmet = parser.ReadColumn( 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 0), diff --git a/Glamourer.GameData/Customization/CharacterCustomization.cs b/Glamourer.GameData/Customization/CharacterCustomization.cs index b518538..79bee13 100644 --- a/Glamourer.GameData/Customization/CharacterCustomization.cs +++ b/Glamourer.GameData/Customization/CharacterCustomization.cs @@ -23,7 +23,7 @@ namespace Glamourer.Customization [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct CharacterCustomization { - public const int CustomizationOffset = 0x830; + public const int CustomizationOffset = 0x840; public const int CustomizationBytes = 26; public static CharacterCustomization Default = new() diff --git a/Glamourer.GameData/GameData.cs b/Glamourer.GameData/GameData.cs index 34f8b45..b2766d5 100644 --- a/Glamourer.GameData/GameData.cs +++ b/Glamourer.GameData/GameData.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Dalamud; using Dalamud.Data; +using Dalamud.Logging; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; @@ -76,7 +77,6 @@ namespace Glamourer var slot = (EquipSlot) item.EquipSlotCategory.Row; if (slot == EquipSlot.Unknown) continue; - slot = slot.ToSlot(); if (!_itemsBySlot.TryGetValue(slot, out var list)) continue; diff --git a/Glamourer.GameData/Glamourer.GameData.csproj b/Glamourer.GameData/Glamourer.GameData.csproj index c2b60ab..af2025c 100644 --- a/Glamourer.GameData/Glamourer.GameData.csproj +++ b/Glamourer.GameData/Glamourer.GameData.csproj @@ -1,63 +1,72 @@ - net5.0-windows + net6.0-windows preview x64 - Glamourer - Glamourer.GameData - 1.0.0.0 + Glamourer + Glamourer.GameData + 1.0.0.0 1.0.0.0 - SoftOtter + SoftOtter Glamourer Copyright © 2020 - true - Library - 4 - true - enable - bin\$(Configuration)\ - $(MSBuildWarningsAsMessages);MSB3277 - + true + Library + 4 + true + enable + bin\$(Configuration)\ + $(MSBuildWarningsAsMessages);MSB3277 + false + false + - - full - DEBUG;TRACE - + + full + DEBUG;TRACE + - - pdbonly - + + pdbonly + - - $(MSBuildWarningsAsMessages);MSB3277 - + + $(MSBuildWarningsAsMessages);MSB3277 + - - - $(DALAMUD_ROOT)\Dalamud.dll - ..\libs\Dalamud.dll - $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll - False - - - $(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll - False - - - $(DALAMUD_ROOT)\Lumina.dll - ..\libs\Lumina.dll - $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll - False - - - $(DALAMUD_ROOT)\Lumina.Excel.dll - ..\libs\Lumina.Excel.dll - $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll - False - - + + $(AppData)\XIVLauncher\addon\Hooks\dev\ + - - - - + + + $(DalamudLibPath)Dalamud.dll + False + + + $(DalamudLibPath)FFXIVClientStructs.dll + False + + + $(DalamudLibPath)Lumina.dll + False + + + $(DalamudLibPath)Lumina.Excel.dll + False + + + $(DalamudLibPath)Newtonsoft.Json.dll + False + + + $(DalamudLibPath)ImGuiScene.dll + False + + + + + + + + \ No newline at end of file diff --git a/Glamourer.sln b/Glamourer.sln index 56bc1ba..5c6e83e 100644 --- a/Glamourer.sln +++ b/Glamourer.sln @@ -15,7 +15,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer.GameData", "Glamo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "..\Penumbra\Penumbra.GameData\Penumbra.GameData.csproj", "{9BEE2336-AA93-4669-8EEA-4756B3B2D024}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.PlayerWatch", "..\Penumbra\Penumbra.PlayerWatch\Penumbra.PlayerWatch.csproj", "{FECEDB39-C103-4333-82A6-A422BDC51EEE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.PlayerWatch", "Penumbra.PlayerWatch\Penumbra.PlayerWatch.csproj", "{FECEDB39-C103-4333-82A6-A422BDC51EEE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Glamourer.zip b/Glamourer.zip index a46b376..4e4e8c4 100644 Binary files a/Glamourer.zip and b/Glamourer.zip differ diff --git a/Glamourer/CharacterSave.cs b/Glamourer/CharacterSave.cs index c9dd7ea..bb45e4e 100644 --- a/Glamourer/CharacterSave.cs +++ b/Glamourer/CharacterSave.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.PlayerWatch; namespace Glamourer; diff --git a/Glamourer/Glamourer.cs b/Glamourer/Glamourer.cs index c20ca21..a39172b 100644 --- a/Glamourer/Glamourer.cs +++ b/Glamourer/Glamourer.cs @@ -3,6 +3,8 @@ using System.Linq; using System.Reflection; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.Command; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Logging; using Dalamud.Plugin; using Glamourer.Api; using Glamourer.Customization; diff --git a/Glamourer/Glamourer.csproj b/Glamourer/Glamourer.csproj index b4a5bec..88cf338 100644 --- a/Glamourer/Glamourer.csproj +++ b/Glamourer/Glamourer.csproj @@ -1,113 +1,124 @@  - net5.0-windows - preview + net6.0-windows + preview x64 - Glamourer - Glamourer - 0.1.0.5 - 0.1.0.5 - SoftOtter + Glamourer + Glamourer + 0.1.1.0 + 0.1.1.0 + SoftOtter Glamourer Copyright © 2020 - true - Library - 4 - true - enable - bin\$(Configuration)\ - $(MSBuildWarningsAsMessages);MSB3277 + true + Library + 4 + true + enable + bin\$(Configuration)\ + $(MSBuildWarningsAsMessages);MSB3277 true - + false + false + - - true - full - false - DEBUG;TRACE - + + true + full + false + DEBUG;TRACE + - - pdbonly - true - TRACE - + + pdbonly + true + TRACE + - - OnOutputUpdated - + + OnOutputUpdated + - - - - - - - - - - - $(appdata)\XIVLauncher\addon\Hooks\dev\Dalamud.dll - False - - - $(appdata)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll - False - - - $(appdata)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll - False - - - $(appdata)\XIVLauncher\addon\Hooks\dev\SDL2-CS.dll - False - - - $(appdata)\XIVLauncher\addon\Hooks\dev\Lumina.dll - False - - - $(appdata)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll - False - + + - - - false - + + + - - + + $(AppData)\XIVLauncher\addon\Hooks\dev\ + - - - - - + + + $(DalamudLibPath)Dalamud.dll + False + + + $(DalamudLibPath)FFXIVClientStructs.dll + False + + + $(DalamudLibPath)ImGui.NET.dll + False + + + $(DalamudLibPath)ImGuiScene.dll + False + + + $(DalamudLibPath)Lumina.dll + False + + + $(DalamudLibPath)Lumina.Excel.dll + False + + + $(DalamudLibPath)Newtonsoft.Json.dll + False + + - - - True - True - Resources.resx - - + + + + - - - ResXFileCodeGenerator - Resources.Designer.cs - - + + + True + True + Resources.resx + + - - - PreserveNewest - - - - - - + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + + + $(GitCommitHash) + + + + + + PreserveNewest + + + + + + \ No newline at end of file diff --git a/Glamourer/Glamourer.json b/Glamourer/Glamourer.json index 0969bb6..574807b 100644 --- a/Glamourer/Glamourer.json +++ b/Glamourer/Glamourer.json @@ -5,10 +5,10 @@ "Description": "Adds functionality to change and store appearance of players, customization and equip. Requires Penumbra to be installed and activated to work. Can also add preview options to the Changed Items tab for Penumbra.", "Tags": [ "Appearance", "Glamour", "Race", "Outfit", "Armor", "Clothes", "Skins", "Customization", "Design", "Character" ], "InternalName": "Glamourer", - "AssemblyVersion": "0.1.0.5", + "AssemblyVersion": "0.1.1.0", "RepoUrl": "https://github.com/Ottermandias/Glamourer", "ApplicableVersion": "any", - "DalamudApiLevel": 6, + "DalamudApiLevel": 7, "ImageUrls": null, "IconUrl": "https://raw.githubusercontent.com/Ottermandias/Glamourer/master/images/icon.png" } \ No newline at end of file diff --git a/Glamourer/Gui/Interface.cs b/Glamourer/Gui/Interface.cs index bc3bf07..b9ac6bb 100644 --- a/Glamourer/Gui/Interface.cs +++ b/Glamourer/Gui/Interface.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Reflection; -using Dalamud.Game.ClientState.Objects.Types; using Glamourer.Designs; using ImGuiNET; using Lumina.Excel.GeneratedSheets; @@ -47,7 +45,7 @@ namespace Glamourer.Gui _stains = GameData.Stains(Dalamud.GameData); _models = GameData.Models(Dalamud.GameData); - _identifier = Penumbra.GameData.GameData.GetIdentifier(Dalamud.GameData, Dalamud.ClientState.ClientLanguage); + _identifier = Penumbra.GameData.GameData.GetIdentifier(Dalamud.GameData); var stainCombo = CreateDefaultStainCombo(_stains.Values.ToArray()); diff --git a/Glamourer/Gui/InterfaceEquipment.cs b/Glamourer/Gui/InterfaceEquipment.cs index 019fc64..22cd084 100644 --- a/Glamourer/Gui/InterfaceEquipment.cs +++ b/Glamourer/Gui/InterfaceEquipment.cs @@ -3,6 +3,7 @@ using ImGuiNET; using Lumina.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.PlayerWatch; namespace Glamourer.Gui { diff --git a/Glamourer/Offsets.cs b/Glamourer/Offsets.cs index 8c08073..e8e2268 100644 --- a/Glamourer/Offsets.cs +++ b/Glamourer/Offsets.cs @@ -4,18 +4,18 @@ public static class Offsets { public static class Character { - public const int Wetness = 0x1ADA; - public const int HatVisible = 0x84E; - public const int VisorToggled = 0x84F; - public const int WeaponHidden1 = 0x84F; - public const int WeaponHidden2 = 0x72C; - public const int Alpha = 0x19E0; + public const int Wetness = 0x1AF3; + public const int HatVisible = 0x85E; + public const int VisorToggled = 0x85F; + public const int WeaponHidden1 = 0x85F; + public const int WeaponHidden2 = 0x73C; + public const int Alpha = 0x19F8; public static class Flags { public const byte IsHatHidden = 0x01; public const byte IsVisorToggled = 0x08; - public const byte IsWet = 0x80; + public const byte IsWet = 0x40; public const byte IsWeaponHidden1 = 0x01; public const byte IsWeaponHidden2 = 0x02; } diff --git a/Penumbra.PlayerWatch/CharacterEquipment.cs b/Penumbra.PlayerWatch/CharacterEquipment.cs new file mode 100644 index 0000000..79c7950 --- /dev/null +++ b/Penumbra.PlayerWatch/CharacterEquipment.cs @@ -0,0 +1,145 @@ +using System; +using System.Runtime.InteropServices; +using Dalamud.Game.ClientState.Objects.Types; +using Penumbra.GameData.Structs; + +// 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.PlayerWatch; + +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public class CharacterEquipment +{ + public const int MainWeaponOffset = 0x6E0; + public const int OffWeaponOffset = 0x748; + public const int EquipmentOffset = 0x818; + public const int EquipmentSlots = 10; + public const int WeaponSlots = 2; + + public CharacterWeapon MainHand; + public CharacterWeapon OffHand; + public CharacterArmor Head; + public CharacterArmor Body; + public CharacterArmor Hands; + public CharacterArmor Legs; + public CharacterArmor Feet; + public CharacterArmor Ears; + public CharacterArmor Neck; + public CharacterArmor Wrists; + public CharacterArmor RFinger; + public CharacterArmor LFinger; + public ushort IsSet; // Also fills struct size to 56, a multiple of 8. + + public CharacterEquipment() + => Clear(); + + public CharacterEquipment(Character actor) + : this(actor.Address) + { } + + public override string ToString() + => IsSet == 0 + ? "(Not Set)" + : $"({MainHand}) | ({OffHand}) | ({Head}) | ({Body}) | ({Hands}) | ({Legs}) | " + + $"({Feet}) | ({Ears}) | ({Neck}) | ({Wrists}) | ({LFinger}) | ({RFinger})"; + + public bool Equal(Character rhs) + => CompareData(new CharacterEquipment(rhs)); + + public bool Equal(CharacterEquipment rhs) + => CompareData(rhs); + + public bool CompareAndUpdate(Character rhs) + => CompareAndOverwrite(new CharacterEquipment(rhs)); + + public bool CompareAndUpdate(CharacterEquipment rhs) + => CompareAndOverwrite(rhs); + + private unsafe CharacterEquipment(IntPtr actorAddress) + { + IsSet = 1; + var actorPtr = (byte*)actorAddress.ToPointer(); + fixed (CharacterWeapon* main = &MainHand, off = &OffHand) + { + Buffer.MemoryCopy(actorPtr + MainWeaponOffset, main, sizeof(CharacterWeapon), sizeof(CharacterWeapon)); + Buffer.MemoryCopy(actorPtr + OffWeaponOffset, off, sizeof(CharacterWeapon), sizeof(CharacterWeapon)); + } + + fixed (CharacterArmor* equipment = &Head) + { + Buffer.MemoryCopy(actorPtr + EquipmentOffset, equipment, EquipmentSlots * sizeof(CharacterArmor), + EquipmentSlots * sizeof(CharacterArmor)); + } + } + + public unsafe void Clear() + { + fixed (CharacterWeapon* main = &MainHand) + { + var structSizeEights = (2 + EquipmentSlots * sizeof(CharacterArmor) + WeaponSlots * sizeof(CharacterWeapon)) / 8; + for (ulong* ptr = (ulong*)main, end = ptr + structSizeEights; ptr != end; ++ptr) + *ptr = 0; + } + } + + private unsafe bool CompareAndOverwrite(CharacterEquipment rhs) + { + var structSizeEights = (2 + EquipmentSlots * sizeof(CharacterArmor) + WeaponSlots * sizeof(CharacterWeapon)) / 8; + var ret = true; + fixed (CharacterWeapon* data1 = &MainHand, data2 = &rhs.MainHand) + { + var ptr1 = (ulong*)data1; + var ptr2 = (ulong*)data2; + for (var end = ptr1 + structSizeEights; ptr1 != end; ++ptr1, ++ptr2) + { + if (*ptr1 != *ptr2) + { + *ptr1 = *ptr2; + ret = false; + } + } + } + + return ret; + } + + private unsafe bool CompareData(CharacterEquipment rhs) + { + var structSizeEights = (2 + EquipmentSlots * sizeof(CharacterArmor) + WeaponSlots * sizeof(CharacterWeapon)) / 8; + fixed (CharacterWeapon* data1 = &MainHand, data2 = &rhs.MainHand) + { + var ptr1 = (ulong*)data1; + var ptr2 = (ulong*)data2; + for (var end = ptr1 + structSizeEights; ptr1 != end; ++ptr1, ++ptr2) + { + if (*ptr1 != *ptr2) + return false; + } + } + + return true; + } + + public unsafe void WriteBytes(byte[] array, int offset = 0) + { + fixed (CharacterWeapon* data = &MainHand) + { + Marshal.Copy(new IntPtr(data), array, offset, 56); + } + } + + public byte[] ToBytes() + { + var ret = new byte[56]; + WriteBytes(ret); + return ret; + } + + public unsafe void FromBytes(byte[] array, int offset = 0) + { + fixed (CharacterWeapon* data = &MainHand) + { + Marshal.Copy(array, offset, new IntPtr(data), 56); + } + } +} diff --git a/Penumbra.PlayerWatch/CharacterFactory.cs b/Penumbra.PlayerWatch/CharacterFactory.cs new file mode 100644 index 0000000..b88cb28 --- /dev/null +++ b/Penumbra.PlayerWatch/CharacterFactory.cs @@ -0,0 +1,63 @@ +using System; +using System.Reflection; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; + +namespace Penumbra.PlayerWatch +{ + public static class CharacterFactory + { + private static ConstructorInfo? _characterConstructor; + + private static void Initialize() + { + _characterConstructor ??= typeof( Character ).GetConstructor( BindingFlags.NonPublic | BindingFlags.Instance, null, new[] + { + typeof( IntPtr ), + }, null )!; + } + + private static Character Character( IntPtr address ) + { + Initialize(); + return ( Character )_characterConstructor?.Invoke( new object[] + { + address, + } )!; + } + + public static Character? Convert( GameObject? actor ) + { + if( actor == null ) + { + return null; + } + + return actor switch + { + PlayerCharacter p => p, + BattleChara b => b, + _ => actor.ObjectKind switch + { + ObjectKind.BattleNpc => Character( actor.Address ), + ObjectKind.Companion => Character( actor.Address ), + ObjectKind.Retainer => Character( actor.Address ), + ObjectKind.EventNpc => Character( actor.Address ), + _ => null, + }, + }; + } + } + + public static class GameObjectExtensions + { + private const int ModelTypeOffset = 0x01B4; + + public static unsafe int ModelType( this GameObject actor ) + => *( int* )( actor.Address + ModelTypeOffset ); + + public static unsafe void SetModelType( this GameObject actor, int value ) + => *( int* )( actor.Address + ModelTypeOffset ) = value; + } +} \ No newline at end of file diff --git a/Penumbra.PlayerWatch/IPlayerWatcher.cs b/Penumbra.PlayerWatch/IPlayerWatcher.cs new file mode 100644 index 0000000..bfdff17 --- /dev/null +++ b/Penumbra.PlayerWatch/IPlayerWatcher.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using Dalamud.Game.ClientState.Objects.Types; +using Penumbra.GameData.Structs; + +namespace Penumbra.PlayerWatch; + +public delegate void PlayerChange( Character actor ); + +public interface IPlayerWatcherBase : IDisposable +{ + public int Version { get; } + public bool Valid { get; } +} + +public interface IPlayerWatcher : IPlayerWatcherBase +{ + public event PlayerChange? PlayerChanged; + public bool Active { get; } + + public void Enable(); + public void Disable(); + public void SetStatus( bool enabled ); + + public void AddPlayerToWatch( string playerName ); + public void RemovePlayerFromWatch( string playerName ); + public CharacterEquipment UpdatePlayerWithoutEvent( Character actor ); + + public IEnumerable< (string, (ulong, CharacterEquipment)[]) > WatchedPlayers(); +} \ No newline at end of file diff --git a/Penumbra.PlayerWatch/Penumbra.PlayerWatch.csproj b/Penumbra.PlayerWatch/Penumbra.PlayerWatch.csproj new file mode 100644 index 0000000..1b64b68 --- /dev/null +++ b/Penumbra.PlayerWatch/Penumbra.PlayerWatch.csproj @@ -0,0 +1,46 @@ + + + net6.0-windows + preview + x64 + Penumbra.PlayerWatch + absolute gangstas + Penumbra + Copyright © 2020 + 1.0.0.0 + 1.0.0.0 + bin\$(Configuration)\ + true + enable + false + false + + + + full + DEBUG;TRACE + + + + pdbonly + + + + $(MSBuildWarningsAsMessages);MSB3277 + + + + $(AppData)\XIVLauncher\addon\Hooks\dev\ + + + + + $(DalamudLibPath)Dalamud.dll + False + + + + + + + \ No newline at end of file diff --git a/Penumbra.PlayerWatch/PlayerWatchBase.cs b/Penumbra.PlayerWatch/PlayerWatchBase.cs new file mode 100644 index 0000000..5b67048 --- /dev/null +++ b/Penumbra.PlayerWatch/PlayerWatchBase.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Logging; +using Penumbra.GameData.Structs; + +namespace Penumbra.PlayerWatch; + +internal readonly struct WatchedPlayer +{ + public readonly Dictionary< ulong, CharacterEquipment > FoundActors; + public readonly HashSet< PlayerWatcher > RegisteredWatchers; + + public WatchedPlayer( PlayerWatcher watcher ) + { + FoundActors = new Dictionary< ulong, CharacterEquipment >( 4 ); + RegisteredWatchers = new HashSet< PlayerWatcher > { watcher }; + } +} + +internal class PlayerWatchBase : IDisposable +{ + public const int GPosePlayerIdx = 201; + public const int GPoseTableEnd = GPosePlayerIdx + 40; + private const int ObjectsPerFrame = 32; + + private readonly Framework _framework; + private readonly ClientState _clientState; + private readonly ObjectTable _objects; + internal readonly HashSet< PlayerWatcher > RegisteredWatchers = new(); + internal readonly Dictionary< string, WatchedPlayer > Equip = new(); + internal HashSet< ulong > SeenActors; + private int _frameTicker; + private bool _inGPose; + private bool _enabled; + private bool _cancel; + + internal PlayerWatchBase( Framework framework, ClientState clientState, ObjectTable objects ) + { + _framework = framework; + _clientState = clientState; + _objects = objects; + SeenActors = new HashSet< ulong >( _objects.Length ); + } + + internal void RegisterWatcher( PlayerWatcher watcher ) + { + RegisteredWatchers.Add( watcher ); + if( watcher.Active ) + { + EnablePlayerWatch(); + } + } + + internal void UnregisterWatcher( PlayerWatcher watcher ) + { + if( RegisteredWatchers.Remove( watcher ) ) + { + foreach( var (key, value) in Equip.ToArray() ) + { + if( value.RegisteredWatchers.Remove( watcher ) && value.RegisteredWatchers.Count == 0 ) + { + Equip.Remove( key ); + } + } + } + + CheckActiveStatus(); + } + + internal void CheckActiveStatus() + { + if( RegisteredWatchers.Any( w => w.Active ) ) + { + EnablePlayerWatch(); + } + else + { + DisablePlayerWatch(); + } + } + + private static ulong GetId( GameObject actor ) + => actor.ObjectId | ( ( ulong )actor.OwnerId << 32 ); + + internal CharacterEquipment UpdatePlayerWithoutEvent( Character actor ) + { + var name = actor.Name.ToString(); + var equipment = new CharacterEquipment( actor ); + if( Equip.TryGetValue( name, out var watched ) ) + { + watched.FoundActors[ GetId( actor ) ] = equipment; + } + + return equipment; + } + + internal void AddPlayerToWatch( string playerName, PlayerWatcher watcher ) + { + if( Equip.TryGetValue( playerName, out var items ) ) + { + items.RegisteredWatchers.Add( watcher ); + } + else + { + Equip[ playerName ] = new WatchedPlayer( watcher ); + } + } + + public void RemovePlayerFromWatch( string playerName, PlayerWatcher watcher ) + { + if( Equip.TryGetValue( playerName, out var items ) ) + { + if( items.RegisteredWatchers.Remove( watcher ) && items.RegisteredWatchers.Count == 0 ) + { + Equip.Remove( playerName ); + } + } + } + + internal void EnablePlayerWatch() + { + if( !_enabled ) + { + _enabled = true; + _framework.Update += OnFrameworkUpdate; + _clientState.TerritoryChanged += OnTerritoryChange; + _clientState.Logout += OnLogout; + } + } + + internal void DisablePlayerWatch() + { + if( _enabled ) + { + _enabled = false; + _framework.Update -= OnFrameworkUpdate; + _clientState.TerritoryChanged -= OnTerritoryChange; + _clientState.Logout -= OnLogout; + } + } + + public void Dispose() + => DisablePlayerWatch(); + + private void OnTerritoryChange( object? _1, ushort _2 ) + => Clear(); + + private void OnLogout( object? _1, object? _2 ) + => Clear(); + + internal void Clear() + { + PluginLog.Debug( "Clearing PlayerWatcher Store." ); + _cancel = true; + foreach( var kvp in Equip ) + { + kvp.Value.FoundActors.Clear(); + } + + _frameTicker = 0; + } + + private static void TriggerEvents( IEnumerable< PlayerWatcher > watchers, Character player ) + { + PluginLog.Debug( "Triggering events for {PlayerName} at {Address}.", player.Name, player.Address ); + foreach( var watcher in watchers.Where( w => w.Active ) ) + { + watcher.Trigger( player ); + } + } + + internal void TriggerGPose() + { + for( var i = GPosePlayerIdx; i < GPoseTableEnd; ++i ) + { + var player = _objects[ i ]; + if( player == null ) + { + return; + } + + if( Equip.TryGetValue( player.Name.ToString(), out var watcher ) ) + { + TriggerEvents( watcher.RegisteredWatchers, ( Character )player ); + } + } + } + + private Character? CheckGPoseObject( GameObject player ) + { + if( !_inGPose ) + { + return CharacterFactory.Convert( player ); + } + + for( var i = GPosePlayerIdx; i < GPoseTableEnd; ++i ) + { + var a = _objects[ i ]; + if( a == null ) + { + return CharacterFactory.Convert( player ); + } + + if( a.Name == player.Name ) + { + return CharacterFactory.Convert( a ); + } + } + + return CharacterFactory.Convert( player )!; + } + + private bool TryGetPlayer( GameObject gameObject, out WatchedPlayer watch ) + { + watch = default; + var name = gameObject.Name.ToString(); + return name.Length != 0 && Equip.TryGetValue( name, out watch ); + } + + private static bool InvalidObjectKind( ObjectKind kind ) + { + return kind switch + { + ObjectKind.BattleNpc => false, + ObjectKind.EventNpc => false, + ObjectKind.Player => false, + ObjectKind.Retainer => false, + _ => true, + }; + } + + private GameObject? GetNextObject() + { + if( _frameTicker == GPosePlayerIdx - 1 ) + { + _frameTicker = GPoseTableEnd; + } + else if( _frameTicker == _objects.Length - 1 ) + { + _frameTicker = 0; + foreach( var (_, equip) in Equip.Values.SelectMany( d => d.FoundActors.Where( p => !SeenActors.Contains( p.Key ) ) ) ) + { + equip.Clear(); + } + + SeenActors.Clear(); + } + else + { + ++_frameTicker; + } + + return _objects[ _frameTicker ]; + } + + private void OnFrameworkUpdate( object framework ) + { + var newInGPose = _objects[ GPosePlayerIdx ] != null; + + if( newInGPose != _inGPose ) + { + if( newInGPose ) + { + TriggerGPose(); + } + else + { + Clear(); + } + + _inGPose = newInGPose; + } + + for( var i = 0; i < ObjectsPerFrame; ++i ) + { + var actor = GetNextObject(); + if( actor == null + || InvalidObjectKind( actor.ObjectKind ) + || !TryGetPlayer( actor, out var watch ) ) + { + continue; + } + + var character = CheckGPoseObject( actor ); + if( _cancel ) + { + _cancel = false; + return; + } + + if( character == null || character.ModelType() != 0 ) + { + continue; + } + + var id = GetId( character ); + SeenActors.Add( id ); + +#if DEBUG + PluginLog.Verbose( "Comparing Gear for {PlayerName:l} ({Id}) at 0x{Address:X}...", character.Name, id, character.Address.ToInt64() ); +#endif + + if( !watch.FoundActors.TryGetValue( id, out var equip ) ) + { + equip = new CharacterEquipment( character ); + watch.FoundActors[ id ] = equip; + TriggerEvents( watch.RegisteredWatchers, character ); + } + else if( !equip.CompareAndUpdate( character ) ) + { + TriggerEvents( watch.RegisteredWatchers, character ); + } + + break; // Only one comparison per frame. + } + } +} \ No newline at end of file diff --git a/Penumbra.PlayerWatch/PlayerWatcher.cs b/Penumbra.PlayerWatch/PlayerWatcher.cs new file mode 100644 index 0000000..7ae94f7 --- /dev/null +++ b/Penumbra.PlayerWatch/PlayerWatcher.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Types; +using Penumbra.GameData.Structs; + +namespace Penumbra.PlayerWatch; + +public class PlayerWatcher : IPlayerWatcher +{ + public int Version + => 3; + + private static PlayerWatchBase? _playerWatch; + + public event PlayerChange? PlayerChanged; + + public bool Active { get; set; } = true; + + public bool Valid + => _playerWatch != null; + + internal PlayerWatcher( Framework framework, ClientState clientState, ObjectTable objects ) + { + _playerWatch ??= new PlayerWatchBase( framework, clientState, objects ); + _playerWatch.RegisterWatcher( this ); + } + + public void Enable() + => SetStatus( true ); + + public void Disable() + => SetStatus( false ); + + public void SetStatus( bool enabled ) + { + Active = enabled && Valid; + _playerWatch?.CheckActiveStatus(); + } + + internal void Trigger( Character actor ) + => PlayerChanged?.Invoke( actor ); + + public void Dispose() + { + if( _playerWatch == null ) + { + return; + } + + Active = false; + PlayerChanged = null; + _playerWatch.UnregisterWatcher( this ); + if( _playerWatch.RegisteredWatchers.Count == 0 ) + { + _playerWatch.Dispose(); + _playerWatch = null; + } + } + + private void CheckValidity() + { + if( !Valid ) + { + throw new Exception( $"PlayerWatch was already disposed." ); + } + } + + public void AddPlayerToWatch( string name ) + { + CheckValidity(); + _playerWatch!.AddPlayerToWatch( name, this ); + } + + public void RemovePlayerFromWatch( string playerName ) + { + CheckValidity(); + _playerWatch!.RemovePlayerFromWatch( playerName, this ); + } + + public CharacterEquipment UpdatePlayerWithoutEvent( Character actor ) + { + CheckValidity(); + return _playerWatch!.UpdatePlayerWithoutEvent( actor ); + } + + public IEnumerable< (string, (ulong, CharacterEquipment)[]) > WatchedPlayers() + { + CheckValidity(); + return _playerWatch!.Equip + .Where( kvp => kvp.Value.RegisteredWatchers.Contains( this ) ) + .Select( kvp => ( kvp.Key, kvp.Value.FoundActors.Select( kvp2 => ( kvp2.Key, kvp2.Value ) ).ToArray() ) ); + } +} + +public static class PlayerWatchFactory +{ + public static IPlayerWatcher Create( Framework framework, ClientState clientState, ObjectTable objects ) + => new PlayerWatcher( framework, clientState, objects ); +} \ No newline at end of file diff --git a/repo.json b/repo.json index 31f23ad..076c553 100644 --- a/repo.json +++ b/repo.json @@ -6,11 +6,11 @@ "Description": "Adds functionality to change and store appearance of players, customization and equip. Requires Penumbra to be installed and activated to work. Can also add preview options to the Changed Items tab for Penumbra.", "Tags": [ "Appearance", "Glamour", "Race", "Outfit", "Armor", "Clothes", "Skins", "Customization", "Design", "Character" ], "InternalName": "Glamourer", - "AssemblyVersion": "0.1.0.5", - "TestingAssemblyVersion": "0.1.0.5", + "AssemblyVersion": "0.1.1.0", + "TestingAssemblyVersion": "0.1.1.0", "RepoUrl": "https://github.com/Ottermandias/Glamourer", "ApplicableVersion": "any", - "DalamudApiLevel": 6, + "DalamudApiLevel": 7, "IsHide": "False", "IsTestingExclusive": "false", "DownloadCount": 1,