diff --git a/Penumbra.PlayerWatch/IPlayerWatcher.cs b/Penumbra.PlayerWatch/IPlayerWatcher.cs index 8a884bc1..bfdff17e 100644 --- a/Penumbra.PlayerWatch/IPlayerWatcher.cs +++ b/Penumbra.PlayerWatch/IPlayerWatcher.cs @@ -3,29 +3,28 @@ using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Structs; -namespace Penumbra.PlayerWatch +namespace Penumbra.PlayerWatch; + +public delegate void PlayerChange( Character actor ); + +public interface IPlayerWatcherBase : IDisposable { - public delegate void PlayerChange( Character actor ); + public int Version { get; } + public bool Valid { get; } +} - 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 interface IPlayerWatcher : IPlayerWatcherBase - { - public event PlayerChange? PlayerChanged; - public bool Active { get; } + public void Enable(); + public void Disable(); + public void SetStatus( bool enabled ); - 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 void AddPlayerToWatch( string playerName ); - public void RemovePlayerFromWatch( string playerName ); - public CharacterEquipment UpdatePlayerWithoutEvent( Character actor ); - - public IEnumerable< (string, (uint, CharacterEquipment)[]) > WatchedPlayers(); - } + public IEnumerable< (string, (ulong, CharacterEquipment)[]) > WatchedPlayers(); } \ No newline at end of file diff --git a/Penumbra.PlayerWatch/PlayerWatchBase.cs b/Penumbra.PlayerWatch/PlayerWatchBase.cs index 3045af88..41cb9fdb 100644 --- a/Penumbra.PlayerWatch/PlayerWatchBase.cs +++ b/Penumbra.PlayerWatch/PlayerWatchBase.cs @@ -9,303 +9,311 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Penumbra.GameData.Structs; -namespace Penumbra.PlayerWatch -{ - internal readonly struct WatchedPlayer - { - public readonly Dictionary< uint, CharacterEquipment > FoundActors; - public readonly HashSet< PlayerWatcher > RegisteredWatchers; +namespace Penumbra.PlayerWatch; - public WatchedPlayer( PlayerWatcher watcher ) +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 + 48; + 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 ) { - FoundActors = new Dictionary< uint, CharacterEquipment >(4); - RegisteredWatchers = new HashSet< PlayerWatcher >{ watcher }; + EnablePlayerWatch(); } } - internal class PlayerWatchBase : IDisposable + internal void UnregisterWatcher( PlayerWatcher watcher ) { - public const int GPosePlayerIdx = 201; - public const int GPoseTableEnd = GPosePlayerIdx + 48; - 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(); - private int _frameTicker; - private bool _inGPose; - private bool _enabled; - private bool _cancel; - - internal PlayerWatchBase( Framework framework, ClientState clientState, ObjectTable objects ) + if( RegisteredWatchers.Remove( watcher ) ) { - _framework = framework; - _clientState = clientState; - _objects = objects; - } - - internal void RegisterWatcher( PlayerWatcher watcher ) - { - RegisteredWatchers.Add( watcher ); - if( watcher.Active ) + foreach( var (key, value) in Equip.ToArray() ) { - 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 ) { - 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 uint GetId( GameObject actor ) - => actor.ObjectId ^ actor.OwnerId; - - 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 ); + Equip.Remove( key ); } } } - internal void EnablePlayerWatch() + CheckActiveStatus(); + } + + internal void CheckActiveStatus() + { + if( RegisteredWatchers.Any( w => w.Active ) ) { - if( !_enabled ) - { - _enabled = true; - _framework.Update += OnFrameworkUpdate; - _clientState.TerritoryChanged += OnTerritoryChange; - _clientState.Logout += OnLogout; - } + 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; } - internal void DisablePlayerWatch() + return equipment; + } + + internal void AddPlayerToWatch( string playerName, PlayerWatcher watcher ) + { + if( Equip.TryGetValue( playerName, out var items ) ) { - if( _enabled ) + 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 ) { - _enabled = false; - _framework.Update -= OnFrameworkUpdate; - _clientState.TerritoryChanged -= OnTerritoryChange; - _clientState.Logout -= OnLogout; + Equip.Remove( playerName ); } } + } - public void Dispose() - => DisablePlayerWatch(); - - private void OnTerritoryChange( object? _1, ushort _2 ) - => Clear(); - - private void OnLogout( object? _1, object? _2 ) - => Clear(); - - internal void Clear() + internal void EnablePlayerWatch() + { + if( !_enabled ) { - PluginLog.Debug( "Clearing PlayerWatcher Store." ); - _cancel = true; - foreach( var kvp in Equip ) - { - kvp.Value.FoundActors.Clear(); - } + _enabled = true; + _framework.Update += OnFrameworkUpdate; + _clientState.TerritoryChanged += OnTerritoryChange; + _clientState.Logout += OnLogout; + } + } - _frameTicker = 0; + 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(); } - private static void TriggerEvents( IEnumerable< PlayerWatcher > watchers, Character player ) + _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 ) ) { - 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 ) { - watcher.Trigger( player ); + return; + } + + if( Equip.TryGetValue( player.Name.ToString(), out var watcher ) ) + { + TriggerEvents( watcher.RegisteredWatchers, ( Character )player ); } } + } - internal void TriggerGPose() + private Character? CheckGPoseObject( GameObject player ) + { + if( !_inGPose ) { - 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 ); - } - } + return CharacterFactory.Convert( player ); } - private Character? CheckGPoseObject( GameObject player ) + for( var i = GPosePlayerIdx; i < GPoseTableEnd; ++i ) { - if( !_inGPose ) + var a = _objects[ i ]; + if( a == null ) { return CharacterFactory.Convert( player ); } - for( var i = GPosePlayerIdx; i < GPoseTableEnd; ++i ) + if( a.Name == player.Name ) { - var a = _objects[ i ]; - if( a == null ) - { - return CharacterFactory.Convert( player ); - } + return CharacterFactory.Convert( a ); + } + } - 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(); } - return CharacterFactory.Convert( player )!; + SeenActors.Clear(); + } + else + { + ++_frameTicker; } - private bool TryGetPlayer( GameObject gameObject, out WatchedPlayer watch ) - { - watch = default; - var name = gameObject.Name.ToString(); - return name.Length != 0 && Equip.TryGetValue( name, out watch ); - } + return _objects[ _frameTicker ]; + } - private static bool InvalidObjectKind( ObjectKind kind ) - { - return kind switch - { - ObjectKind.BattleNpc => false, - ObjectKind.EventNpc => false, - ObjectKind.Player => false, - ObjectKind.Retainer => false, - _ => true, - }; - } + private void OnFrameworkUpdate( object framework ) + { + var newInGPose = _objects[ GPosePlayerIdx ] != null; - private GameObject? GetNextObject() + if( newInGPose != _inGPose ) { - if( _frameTicker == GPosePlayerIdx - 1 ) + if( newInGPose ) { - _frameTicker = GPoseTableEnd; - } - else if( _frameTicker == _objects.Length - 1 ) - { - _frameTicker = 0; + TriggerGPose(); } else { - ++_frameTicker; + Clear(); } - return _objects[ _frameTicker ]; + _inGPose = newInGPose; } - private void OnFrameworkUpdate( object framework ) + for( var i = 0; i < ObjectsPerFrame; ++i ) { - var newInGPose = _objects[ GPosePlayerIdx ] != null; - - if( newInGPose != _inGPose ) + var actor = GetNextObject(); + if( actor == null + || InvalidObjectKind( actor.ObjectKind ) + || !TryGetPlayer( actor, out var watch ) ) { - if( newInGPose ) - { - TriggerGPose(); - } - else - { - Clear(); - } - - _inGPose = newInGPose; + continue; } - for( var i = 0; i < ObjectsPerFrame; ++i ) + var character = CheckGPoseObject( actor ); + if( _cancel ) { - 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 ); - PluginLog.Verbose( "Comparing Gear for {PlayerName} ({Id}) at {Address}...", character.Name, id, character.Address); - 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. + _cancel = false; + return; } + + if( character == null || character.ModelType() != 0 ) + { + continue; + } + + var id = GetId( character ); + SeenActors.Add( id ); + PluginLog.Verbose( "Comparing Gear for {PlayerName} ({Id}) at {Address}...", character.Name, id, character.Address ); + 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 index 9eab7134..7ae94f79 100644 --- a/Penumbra.PlayerWatch/PlayerWatcher.cs +++ b/Penumbra.PlayerWatch/PlayerWatcher.cs @@ -7,97 +7,97 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Structs; -namespace Penumbra.PlayerWatch +namespace Penumbra.PlayerWatch; + +public class PlayerWatcher : IPlayerWatcher { - 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 ) { - public int Version { get; } = 2; + _playerWatch ??= new PlayerWatchBase( framework, clientState, objects ); + _playerWatch.RegisterWatcher( this ); + } - private static PlayerWatchBase? _playerWatch; + public void Enable() + => SetStatus( true ); - public event PlayerChange? PlayerChanged; + public void Disable() + => SetStatus( false ); - public bool Active { get; set; } = true; + public void SetStatus( bool enabled ) + { + Active = enabled && Valid; + _playerWatch?.CheckActiveStatus(); + } - public bool Valid - => _playerWatch != null; + internal void Trigger( Character actor ) + => PlayerChanged?.Invoke( actor ); - internal PlayerWatcher( Framework framework, ClientState clientState, ObjectTable objects ) + public void Dispose() + { + if( _playerWatch == null ) { - _playerWatch ??= new PlayerWatchBase( framework, clientState, objects ); - _playerWatch.RegisterWatcher( this ); + return; } - public void Enable() - => SetStatus( true ); - - public void Disable() - => SetStatus( false ); - - public void SetStatus( bool enabled ) + Active = false; + PlayerChanged = null; + _playerWatch.UnregisterWatcher( this ); + if( _playerWatch.RegisteredWatchers.Count == 0 ) { - 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, (uint, 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() ) ); + _playerWatch.Dispose(); + _playerWatch = null; } } - public static class PlayerWatchFactory + private void CheckValidity() { - public static IPlayerWatcher Create( Framework framework, ClientState clientState, ObjectTable objects ) - => new PlayerWatcher( framework, clientState, objects ); + 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