Add keeping track of seen players to player watcher.

This commit is contained in:
Ottermandias 2021-11-16 16:03:49 +01:00
parent 906e057943
commit 743f83d12e
3 changed files with 340 additions and 333 deletions

View file

@ -3,29 +3,28 @@ using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Penumbra.GameData.Structs; 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 interface IPlayerWatcher : IPlayerWatcherBase
{ {
public int Version { get; } public event PlayerChange? PlayerChanged;
public bool Valid { get; } public bool Active { get; }
}
public interface IPlayerWatcher : IPlayerWatcherBase public void Enable();
{ public void Disable();
public event PlayerChange? PlayerChanged; public void SetStatus( bool enabled );
public bool Active { get; }
public void Enable(); public void AddPlayerToWatch( string playerName );
public void Disable(); public void RemovePlayerFromWatch( string playerName );
public void SetStatus( bool enabled ); public CharacterEquipment UpdatePlayerWithoutEvent( Character actor );
public void AddPlayerToWatch( string playerName ); public IEnumerable< (string, (ulong, CharacterEquipment)[]) > WatchedPlayers();
public void RemovePlayerFromWatch( string playerName );
public CharacterEquipment UpdatePlayerWithoutEvent( Character actor );
public IEnumerable< (string, (uint, CharacterEquipment)[]) > WatchedPlayers();
}
} }

View file

@ -9,303 +9,311 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging; using Dalamud.Logging;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
namespace Penumbra.PlayerWatch namespace Penumbra.PlayerWatch;
{
internal readonly struct WatchedPlayer
{
public readonly Dictionary< uint, CharacterEquipment > FoundActors;
public readonly HashSet< PlayerWatcher > RegisteredWatchers;
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); EnablePlayerWatch();
RegisteredWatchers = new HashSet< PlayerWatcher >{ watcher };
} }
} }
internal class PlayerWatchBase : IDisposable internal void UnregisterWatcher( PlayerWatcher watcher )
{ {
public const int GPosePlayerIdx = 201; if( RegisteredWatchers.Remove( watcher ) )
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 )
{ {
_framework = framework; foreach( var (key, value) in Equip.ToArray() )
_clientState = clientState;
_objects = objects;
}
internal void RegisterWatcher( PlayerWatcher watcher )
{
RegisteredWatchers.Add( watcher );
if( watcher.Active )
{ {
EnablePlayerWatch(); if( value.RegisteredWatchers.Remove( watcher ) && value.RegisteredWatchers.Count == 0 )
}
}
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 );
{
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 );
} }
} }
} }
internal void EnablePlayerWatch() CheckActiveStatus();
}
internal void CheckActiveStatus()
{
if( RegisteredWatchers.Any( w => w.Active ) )
{ {
if( !_enabled ) EnablePlayerWatch();
{ }
_enabled = true; else
_framework.Update += OnFrameworkUpdate; {
_clientState.TerritoryChanged += OnTerritoryChange; DisablePlayerWatch();
_clientState.Logout += OnLogout; }
} }
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; Equip.Remove( playerName );
_framework.Update -= OnFrameworkUpdate;
_clientState.TerritoryChanged -= OnTerritoryChange;
_clientState.Logout -= OnLogout;
} }
} }
}
public void Dispose() internal void EnablePlayerWatch()
=> DisablePlayerWatch(); {
if( !_enabled )
private void OnTerritoryChange( object? _1, ushort _2 )
=> Clear();
private void OnLogout( object? _1, object? _2 )
=> Clear();
internal void Clear()
{ {
PluginLog.Debug( "Clearing PlayerWatcher Store." ); _enabled = true;
_cancel = true; _framework.Update += OnFrameworkUpdate;
foreach( var kvp in Equip ) _clientState.TerritoryChanged += OnTerritoryChange;
{ _clientState.Logout += OnLogout;
kvp.Value.FoundActors.Clear(); }
} }
_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 ); watcher.Trigger( player );
foreach( var watcher in watchers.Where( w => w.Active ) ) }
}
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 ) return CharacterFactory.Convert( player );
{
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 ) for( var i = GPosePlayerIdx; i < GPoseTableEnd; ++i )
{ {
if( !_inGPose ) var a = _objects[ i ];
if( a == null )
{ {
return CharacterFactory.Convert( player ); return CharacterFactory.Convert( player );
} }
for( var i = GPosePlayerIdx; i < GPoseTableEnd; ++i ) if( a.Name == player.Name )
{ {
var a = _objects[ i ]; return CharacterFactory.Convert( a );
if( a == null ) }
{ }
return CharacterFactory.Convert( player );
}
if( a.Name == player.Name ) return CharacterFactory.Convert( player )!;
{ }
return CharacterFactory.Convert( a );
} 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 ) return _objects[ _frameTicker ];
{ }
watch = default;
var name = gameObject.Name.ToString();
return name.Length != 0 && Equip.TryGetValue( name, out watch );
}
private static bool InvalidObjectKind( ObjectKind kind ) private void OnFrameworkUpdate( object framework )
{ {
return kind switch var newInGPose = _objects[ GPosePlayerIdx ] != null;
{
ObjectKind.BattleNpc => false,
ObjectKind.EventNpc => false,
ObjectKind.Player => false,
ObjectKind.Retainer => false,
_ => true,
};
}
private GameObject? GetNextObject() if( newInGPose != _inGPose )
{ {
if( _frameTicker == GPosePlayerIdx - 1 ) if( newInGPose )
{ {
_frameTicker = GPoseTableEnd; TriggerGPose();
}
else if( _frameTicker == _objects.Length - 1 )
{
_frameTicker = 0;
} }
else 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; var actor = GetNextObject();
if( actor == null
if( newInGPose != _inGPose ) || InvalidObjectKind( actor.ObjectKind )
|| !TryGetPlayer( actor, out var watch ) )
{ {
if( newInGPose ) continue;
{
TriggerGPose();
}
else
{
Clear();
}
_inGPose = newInGPose;
} }
for( var i = 0; i < ObjectsPerFrame; ++i ) var character = CheckGPoseObject( actor );
if( _cancel )
{ {
var actor = GetNextObject(); _cancel = false;
if( actor == null return;
|| 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.
} }
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.
} }
} }
} }

View file

@ -7,97 +7,97 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Penumbra.GameData.Structs; 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 internal void Trigger( Character actor )
=> _playerWatch != null; => PlayerChanged?.Invoke( actor );
internal PlayerWatcher( Framework framework, ClientState clientState, ObjectTable objects ) public void Dispose()
{
if( _playerWatch == null )
{ {
_playerWatch ??= new PlayerWatchBase( framework, clientState, objects ); return;
_playerWatch.RegisterWatcher( this );
} }
public void Enable() Active = false;
=> SetStatus( true ); PlayerChanged = null;
_playerWatch.UnregisterWatcher( this );
public void Disable() if( _playerWatch.RegisteredWatchers.Count == 0 )
=> SetStatus( false );
public void SetStatus( bool enabled )
{ {
Active = enabled && Valid; _playerWatch.Dispose();
_playerWatch?.CheckActiveStatus(); _playerWatch = null;
}
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() ) );
} }
} }
public static class PlayerWatchFactory private void CheckValidity()
{ {
public static IPlayerWatcher Create( Framework framework, ClientState clientState, ObjectTable objects ) if( !Valid )
=> new PlayerWatcher( framework, clientState, objects ); {
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 );
} }