mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Some early glamourer changes.
This commit is contained in:
parent
c2bc8252f1
commit
9dee0862cc
17 changed files with 192 additions and 840 deletions
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Penumbra.GameData.Enums;
|
||||
|
||||
|
|
@ -51,6 +52,22 @@ public static class EquipSlotExtensions
|
|||
_ => EquipSlot.Unknown,
|
||||
};
|
||||
|
||||
public static uint ToIndex( this EquipSlot slot )
|
||||
=> slot switch
|
||||
{
|
||||
EquipSlot.Head => 0,
|
||||
EquipSlot.Body => 1,
|
||||
EquipSlot.Hands => 2,
|
||||
EquipSlot.Legs => 3,
|
||||
EquipSlot.Feet => 4,
|
||||
EquipSlot.Ears => 5,
|
||||
EquipSlot.Neck => 6,
|
||||
EquipSlot.Wrists => 7,
|
||||
EquipSlot.RFinger => 8,
|
||||
EquipSlot.LFinger => 9,
|
||||
_ => uint.MaxValue,
|
||||
};
|
||||
|
||||
public static string ToSuffix( this EquipSlot value )
|
||||
{
|
||||
return value switch
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Penumbra.GameData.Structs;
|
||||
|
||||
[StructLayout( LayoutKind.Explicit, Pack = 1 )]
|
||||
public readonly struct CharacterArmor
|
||||
public readonly struct CharacterArmor : IEquatable< CharacterArmor >
|
||||
{
|
||||
[FieldOffset( 0 )]
|
||||
public readonly uint Value;
|
||||
|
||||
[FieldOffset( 0 )]
|
||||
public readonly SetId Set;
|
||||
|
||||
|
|
@ -14,9 +18,31 @@ public readonly struct CharacterArmor
|
|||
[FieldOffset( 3 )]
|
||||
public readonly StainId Stain;
|
||||
|
||||
[FieldOffset( 0 )]
|
||||
public readonly uint Value;
|
||||
public CharacterArmor( SetId set, byte variant, StainId stain )
|
||||
{
|
||||
Value = 0;
|
||||
Set = set;
|
||||
Variant = variant;
|
||||
Stain = stain;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> $"{Set},{Variant},{Stain}";
|
||||
|
||||
public static readonly CharacterArmor Empty;
|
||||
|
||||
public bool Equals( CharacterArmor other )
|
||||
=> Value == other.Value;
|
||||
|
||||
public override bool Equals( object? obj )
|
||||
=> obj is CharacterArmor other && Equals( other );
|
||||
|
||||
public override int GetHashCode()
|
||||
=> ( int )Value;
|
||||
|
||||
public static bool operator ==( CharacterArmor left, CharacterArmor right )
|
||||
=> left.Value == right.Value;
|
||||
|
||||
public static bool operator !=( CharacterArmor left, CharacterArmor right )
|
||||
=> left.Value != right.Value;
|
||||
}
|
||||
|
|
@ -1,16 +1,51 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Penumbra.GameData.Structs
|
||||
namespace Penumbra.GameData.Structs;
|
||||
|
||||
[StructLayout( LayoutKind.Explicit, Pack = 1, Size = 7 )]
|
||||
public readonly struct CharacterWeapon : IEquatable< CharacterWeapon >
|
||||
{
|
||||
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
|
||||
public readonly struct CharacterWeapon
|
||||
{
|
||||
[FieldOffset( 0 )]
|
||||
public readonly SetId Set;
|
||||
|
||||
[FieldOffset( 2 )]
|
||||
public readonly WeaponType Type;
|
||||
|
||||
[FieldOffset( 4 )]
|
||||
public readonly ushort Variant;
|
||||
|
||||
[FieldOffset( 6 )]
|
||||
public readonly StainId Stain;
|
||||
|
||||
public ulong Value
|
||||
=> ( ulong )Set | ( ( ulong )Type << 16 ) | ( ( ulong )Variant << 32 ) | ( ( ulong )Stain << 48 );
|
||||
|
||||
public override string ToString()
|
||||
=> $"{Set},{Type},{Variant},{Stain}";
|
||||
|
||||
public CharacterWeapon( SetId set, WeaponType type, ushort variant, StainId stain )
|
||||
{
|
||||
Set = set;
|
||||
Type = type;
|
||||
Variant = variant;
|
||||
Stain = stain;
|
||||
}
|
||||
|
||||
public static readonly CharacterWeapon Empty = new(0, 0, 0, 0);
|
||||
|
||||
public bool Equals( CharacterWeapon other )
|
||||
=> Value == other.Value;
|
||||
|
||||
public override bool Equals( object? obj )
|
||||
=> obj is CharacterWeapon other && Equals( other );
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Value.GetHashCode();
|
||||
|
||||
public static bool operator ==( CharacterWeapon left, CharacterWeapon right )
|
||||
=> left.Value == right.Value;
|
||||
|
||||
public static bool operator !=( CharacterWeapon left, CharacterWeapon right )
|
||||
=> left.Value != right.Value;
|
||||
}
|
||||
|
|
@ -4,11 +4,11 @@ using System.IO;
|
|||
using System.Runtime.InteropServices;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Penumbra.GameData.Structs
|
||||
namespace Penumbra.GameData.Structs;
|
||||
|
||||
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
|
||||
public readonly struct RspEntry
|
||||
{
|
||||
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
|
||||
public readonly struct RspEntry
|
||||
{
|
||||
public const int ByteSize = ( int )RspAttribute.NumAttributes * 4;
|
||||
|
||||
private readonly float[] Attributes;
|
||||
|
|
@ -24,8 +24,8 @@ namespace Penumbra.GameData.Structs
|
|||
}
|
||||
|
||||
Attributes = new float[( int )RspAttribute.NumAttributes];
|
||||
using MemoryStream s = new( bytes ) { Position = offset };
|
||||
using BinaryReader br = new( s );
|
||||
using MemoryStream s = new(bytes) { Position = offset };
|
||||
using BinaryReader br = new(s);
|
||||
for( var i = 0; i < ( int )RspAttribute.NumAttributes; ++i )
|
||||
{
|
||||
Attributes[ i ] = br.ReadSingle();
|
||||
|
|
@ -54,5 +54,4 @@ namespace Penumbra.GameData.Structs
|
|||
|
||||
return s.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
using System;
|
||||
|
||||
namespace Penumbra.GameData.Structs
|
||||
namespace Penumbra.GameData.Structs;
|
||||
|
||||
public readonly struct SetId : IComparable< SetId >
|
||||
{
|
||||
public readonly struct SetId : IComparable< SetId >
|
||||
{
|
||||
public readonly ushort Value;
|
||||
|
||||
public SetId( ushort value )
|
||||
=> Value = value;
|
||||
|
||||
public static implicit operator SetId( ushort id )
|
||||
=> new( id );
|
||||
=> new(id);
|
||||
|
||||
public static explicit operator ushort( SetId id )
|
||||
=> id.Value;
|
||||
|
|
@ -20,5 +20,4 @@ namespace Penumbra.GameData.Structs
|
|||
|
||||
public int CompareTo( SetId other )
|
||||
=> Value.CompareTo( other.Value );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
using System;
|
||||
|
||||
namespace Penumbra.GameData.Structs
|
||||
namespace Penumbra.GameData.Structs;
|
||||
|
||||
public readonly struct StainId : IEquatable< StainId >
|
||||
{
|
||||
public readonly struct StainId : IEquatable< StainId >
|
||||
{
|
||||
public readonly byte Value;
|
||||
|
||||
public StainId( byte value )
|
||||
=> Value = value;
|
||||
|
||||
public static implicit operator StainId( byte id )
|
||||
=> new( id );
|
||||
=> new(id);
|
||||
|
||||
public static explicit operator byte( StainId id )
|
||||
=> id.Value;
|
||||
|
|
@ -26,5 +26,4 @@ namespace Penumbra.GameData.Structs
|
|||
|
||||
public override int GetHashCode()
|
||||
=> Value.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
using System;
|
||||
|
||||
namespace Penumbra.GameData.Structs
|
||||
namespace Penumbra.GameData.Structs;
|
||||
|
||||
public readonly struct WeaponType : IEquatable< WeaponType >
|
||||
{
|
||||
public readonly struct WeaponType : IEquatable< WeaponType >
|
||||
{
|
||||
public readonly ushort Value;
|
||||
|
||||
public WeaponType( ushort value )
|
||||
=> Value = value;
|
||||
|
||||
public static implicit operator WeaponType( ushort id )
|
||||
=> new( id );
|
||||
=> new(id);
|
||||
|
||||
public static explicit operator ushort( WeaponType id )
|
||||
=> id.Value;
|
||||
|
|
@ -26,5 +26,4 @@ namespace Penumbra.GameData.Structs
|
|||
|
||||
public override int GetHashCode()
|
||||
=> Value.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
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();
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<AssemblyTitle>Penumbra.PlayerWatch</AssemblyTitle>
|
||||
<Company>absolute gangstas</Company>
|
||||
<Product>Penumbra</Product>
|
||||
<Copyright>Copyright © 2020</Copyright>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugType>full</DebugType>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Dalamud">
|
||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
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 );
|
||||
}
|
||||
|
|
@ -12,8 +12,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{EE551E87-FDB3-4612-B500-DC870C07C605}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.PlayerWatch", "Penumbra.PlayerWatch\Penumbra.PlayerWatch.csproj", "{01685BD8-8847-4B49-BF90-1683B4C76B0E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "OtterGui\OtterGui.csproj", "{87750518-1A20-40B4-9FC1-22F906EFB290}"
|
||||
EndProject
|
||||
Global
|
||||
|
|
@ -30,10 +28,6 @@ Global
|
|||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{01685BD8-8847-4B49-BF90-1683B4C76B0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{01685BD8-8847-4B49-BF90-1683B4C76B0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{01685BD8-8847-4B49-BF90-1683B4C76B0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{01685BD8-8847-4B49-BF90-1683B4C76B0E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ using EmbedIO.WebApi;
|
|||
using ImGuiNET;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.Api;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Interop;
|
||||
|
|
@ -75,7 +76,7 @@ public class Penumbra : IDalamudPlugin
|
|||
|
||||
Framework = new FrameworkManager();
|
||||
CharacterUtility = new CharacterUtility();
|
||||
Backup.CreateBackup( PenumbraBackupFiles() );
|
||||
Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() );
|
||||
Config = Configuration.Load();
|
||||
|
||||
TempMods = new TempModManager();
|
||||
|
|
|
|||
|
|
@ -15,16 +15,6 @@
|
|||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugType>full</DebugType>
|
||||
<DefineConstants>DEBUG;TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<DefineConstants>$(DefineConstants)TRACE;USE_EQP;USE_EQDP;USE_GMP;USE_EST;USE_CMP;USE_IMC</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
|
||||
</PropertyGroup>
|
||||
|
|
@ -72,7 +62,6 @@
|
|||
<ItemGroup>
|
||||
<ProjectReference Include="..\OtterGui\OtterGui.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.PlayerWatch\Penumbra.PlayerWatch.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,147 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
|
||||
namespace Penumbra.Util;
|
||||
|
||||
public static class Backup
|
||||
{
|
||||
public const int MaxNumBackups = 10;
|
||||
|
||||
// Create a backup named by ISO 8601 of the current time.
|
||||
// If the newest previously existing backup equals the current state of files,
|
||||
// do not create a new backup.
|
||||
// If the maximum number of backups is exceeded afterwards, delete the oldest backup.
|
||||
public static void CreateBackup( IReadOnlyCollection< FileInfo > files )
|
||||
{
|
||||
try
|
||||
{
|
||||
var configDirectory = Dalamud.PluginInterface.ConfigDirectory.Parent!.FullName;
|
||||
var directory = CreateBackupDirectory();
|
||||
var (newestFile, oldestFile, numFiles) = CheckExistingBackups( directory );
|
||||
var newBackupName = Path.Combine( directory.FullName, $"{DateTime.Now:yyyyMMddHHmmss}.zip" );
|
||||
if( newestFile == null || CheckNewestBackup( newestFile, configDirectory, files.Count ) )
|
||||
{
|
||||
CreateBackup( files, newBackupName, configDirectory );
|
||||
if( numFiles > MaxNumBackups )
|
||||
{
|
||||
oldestFile!.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not create backups:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Obtain the backup directory. Create it if it does not exist.
|
||||
private static DirectoryInfo CreateBackupDirectory()
|
||||
{
|
||||
var path = Path.Combine( Dalamud.PluginInterface.ConfigDirectory.Parent!.Parent!.FullName, "backups",
|
||||
Dalamud.PluginInterface.ConfigDirectory.Name );
|
||||
var dir = new DirectoryInfo( path );
|
||||
if( !dir.Exists )
|
||||
{
|
||||
dir = Directory.CreateDirectory( dir.FullName );
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
// Check the already existing backups.
|
||||
// Only keep MaxNumBackups at once, and delete the oldest if the number would be exceeded.
|
||||
// Return the newest backup.
|
||||
private static (FileInfo? Newest, FileInfo? Oldest, int Count) CheckExistingBackups( DirectoryInfo backupDirectory )
|
||||
{
|
||||
var count = 0;
|
||||
FileInfo? newest = null;
|
||||
FileInfo? oldest = null;
|
||||
|
||||
foreach( var file in backupDirectory.EnumerateFiles( "*.zip" ) )
|
||||
{
|
||||
++count;
|
||||
var time = file.CreationTimeUtc;
|
||||
if( ( oldest?.CreationTimeUtc ?? DateTime.MaxValue ) > time )
|
||||
{
|
||||
oldest = file;
|
||||
}
|
||||
|
||||
if( ( newest?.CreationTimeUtc ?? DateTime.MinValue ) < time )
|
||||
{
|
||||
newest = file;
|
||||
}
|
||||
}
|
||||
|
||||
return ( newest, oldest, count );
|
||||
}
|
||||
|
||||
// Compare the newest backup against the currently existing files.
|
||||
// If there are any differences, return false, and if they are completely identical, return true.
|
||||
private static bool CheckNewestBackup( FileInfo newestFile, string configDirectory, int fileCount )
|
||||
{
|
||||
using var oldFileStream = File.Open( newestFile.FullName, FileMode.Open );
|
||||
using var oldZip = new ZipArchive( oldFileStream, ZipArchiveMode.Read );
|
||||
// Number of stored files is different.
|
||||
if( fileCount != oldZip.Entries.Count )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Since number of files is identical,
|
||||
// the backups are identical if every file in the old backup
|
||||
// still exists and is identical.
|
||||
foreach( var entry in oldZip.Entries )
|
||||
{
|
||||
var file = Path.Combine( configDirectory, entry.FullName );
|
||||
if( !File.Exists( file ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
using var currentData = File.OpenRead( file );
|
||||
using var oldData = entry.Open();
|
||||
|
||||
if( !Equals( currentData, oldData ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the actual backup, storing all the files relative to the given configDirectory in the zip.
|
||||
private static void CreateBackup( IEnumerable< FileInfo > files, string fileName, string configDirectory )
|
||||
{
|
||||
using var fileStream = File.Open( fileName, FileMode.Create );
|
||||
using var zip = new ZipArchive( fileStream, ZipArchiveMode.Create );
|
||||
foreach( var file in files.Where( f => File.Exists( f.FullName ) ) )
|
||||
{
|
||||
zip.CreateEntryFromFile( file.FullName, Path.GetRelativePath( configDirectory, file.FullName ), CompressionLevel.Optimal );
|
||||
}
|
||||
}
|
||||
|
||||
// Compare two streams per byte and return if they are equal.
|
||||
private static bool Equals( Stream lhs, Stream rhs )
|
||||
{
|
||||
while( true )
|
||||
{
|
||||
var current = lhs.ReadByte();
|
||||
var old = rhs.ReadByte();
|
||||
if( current != old )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( current == -1 )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue