2025-08-22 02:19:48 +01:00
using Dalamud.Plugin ;
using MareSynchronos.MareConfiguration.Models ;
using MareSynchronos.PlayerData.Handlers ;
using MareSynchronos.Services ;
using MareSynchronos.Services.Mediator ;
using Microsoft.Extensions.Logging ;
using Penumbra.Api.Enums ;
using Penumbra.Api.Helpers ;
using Penumbra.Api.IpcSubscribers ;
using System.Collections.Concurrent ;
namespace MareSynchronos.Interop.Ipc ;
public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase , IIpcCaller
{
private readonly DalamudUtilService _dalamudUtil ;
private readonly MareMediator _mareMediator ;
private readonly RedrawManager _redrawManager ;
private bool _shownPenumbraUnavailable = false ;
private string? _penumbraModDirectory ;
public string? ModDirectory
{
get = > _penumbraModDirectory ;
private set
{
if ( ! string . Equals ( _penumbraModDirectory , value , StringComparison . Ordinal ) )
{
_penumbraModDirectory = value ;
_mareMediator . Publish ( new PenumbraDirectoryChangedMessage ( _penumbraModDirectory ) ) ;
}
}
}
private readonly ConcurrentDictionary < IntPtr , bool > _penumbraRedrawRequests = new ( ) ;
private readonly EventSubscriber _penumbraDispose ;
private readonly EventSubscriber < nint , string , string > _penumbraGameObjectResourcePathResolved ;
private readonly EventSubscriber _penumbraInit ;
private readonly EventSubscriber < ModSettingChange , Guid , string , bool > _penumbraModSettingChanged ;
private readonly EventSubscriber < nint , int > _penumbraObjectIsRedrawn ;
private readonly AddTemporaryMod _penumbraAddTemporaryMod ;
private readonly AssignTemporaryCollection _penumbraAssignTemporaryCollection ;
private readonly ConvertTextureFile _penumbraConvertTextureFile ;
private readonly CreateTemporaryCollection _penumbraCreateNamedTemporaryCollection ;
private readonly GetEnabledState _penumbraEnabled ;
private readonly GetPlayerMetaManipulations _penumbraGetMetaManipulations ;
private readonly RedrawObject _penumbraRedraw ;
private readonly DeleteTemporaryCollection _penumbraRemoveTemporaryCollection ;
private readonly RemoveTemporaryMod _penumbraRemoveTemporaryMod ;
private readonly GetModDirectory _penumbraResolveModDir ;
private readonly ResolvePlayerPathsAsync _penumbraResolvePaths ;
private readonly GetGameObjectResourcePaths _penumbraResourcePaths ;
private bool _pluginLoaded ;
private Version _pluginVersion ;
public IpcCallerPenumbra ( ILogger < IpcCallerPenumbra > logger , IDalamudPluginInterface pi , DalamudUtilService dalamudUtil ,
MareMediator mareMediator , RedrawManager redrawManager ) : base ( logger , mareMediator )
{
_dalamudUtil = dalamudUtil ;
_mareMediator = mareMediator ;
_redrawManager = redrawManager ;
_penumbraInit = Initialized . Subscriber ( pi , PenumbraInit ) ;
_penumbraDispose = Disposed . Subscriber ( pi , PenumbraDispose ) ;
_penumbraResolveModDir = new GetModDirectory ( pi ) ;
_penumbraRedraw = new RedrawObject ( pi ) ;
_penumbraObjectIsRedrawn = GameObjectRedrawn . Subscriber ( pi , RedrawEvent ) ;
_penumbraGetMetaManipulations = new GetPlayerMetaManipulations ( pi ) ;
_penumbraRemoveTemporaryMod = new RemoveTemporaryMod ( pi ) ;
_penumbraAddTemporaryMod = new AddTemporaryMod ( pi ) ;
_penumbraCreateNamedTemporaryCollection = new CreateTemporaryCollection ( pi ) ;
_penumbraRemoveTemporaryCollection = new DeleteTemporaryCollection ( pi ) ;
_penumbraAssignTemporaryCollection = new AssignTemporaryCollection ( pi ) ;
_penumbraResolvePaths = new ResolvePlayerPathsAsync ( pi ) ;
_penumbraEnabled = new GetEnabledState ( pi ) ;
_penumbraModSettingChanged = ModSettingChanged . Subscriber ( pi , ( change , arg1 , arg , b ) = >
{
if ( change = = ModSettingChange . EnableState )
_mareMediator . Publish ( new PenumbraModSettingChangedMessage ( ) ) ;
} ) ;
_penumbraConvertTextureFile = new ConvertTextureFile ( pi ) ;
_penumbraResourcePaths = new GetGameObjectResourcePaths ( pi ) ;
_penumbraGameObjectResourcePathResolved = GameObjectResourcePathResolved . Subscriber ( pi , ResourceLoaded ) ;
var plugin = PluginWatcherService . GetInitialPluginState ( pi , "Penumbra" ) ;
_pluginLoaded = plugin ? . IsLoaded ? ? false ;
_pluginVersion = plugin ? . Version ? ? new ( 0 , 0 , 0 , 0 ) ;
Mediator . SubscribeKeyed < PluginChangeMessage > ( this , "Penumbra" , ( msg ) = >
{
_pluginLoaded = msg . IsLoaded ;
_pluginVersion = msg . Version ;
CheckAPI ( ) ;
} ) ;
CheckAPI ( ) ;
CheckModDirectory ( ) ;
Mediator . Subscribe < PenumbraRedrawCharacterMessage > ( this , ( msg ) = >
{
_penumbraRedraw . Invoke ( msg . Character . ObjectIndex , RedrawType . AfterGPose ) ;
} ) ;
Mediator . Subscribe < DalamudLoginMessage > ( this , ( msg ) = > _shownPenumbraUnavailable = false ) ;
}
public bool APIAvailable { get ; private set ; } = false ;
public void CheckAPI ( )
{
bool penumbraAvailable = false ;
try
{
2025-08-29 20:47:15 +01:00
penumbraAvailable = _pluginLoaded & & _pluginVersion > = new Version ( 1 , 5 , 1 , 0 ) ;
2025-08-22 02:19:48 +01:00
try
{
penumbraAvailable & = _penumbraEnabled . Invoke ( ) ;
}
catch
{
penumbraAvailable = false ;
}
_shownPenumbraUnavailable = _shownPenumbraUnavailable & & ! penumbraAvailable ;
APIAvailable = penumbraAvailable ;
}
catch
{
APIAvailable = penumbraAvailable ;
}
finally
{
if ( ! penumbraAvailable & & ! _shownPenumbraUnavailable )
{
_shownPenumbraUnavailable = true ;
_mareMediator . Publish ( new NotificationMessage ( "Penumbra inactive" ,
2025-08-22 21:26:51 +01:00
"Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Snowcloak. If you just updated Penumbra, ignore this message." ,
2025-08-22 02:19:48 +01:00
NotificationType . Error ) ) ;
}
}
}
public void CheckModDirectory ( )
{
if ( ! APIAvailable )
{
ModDirectory = string . Empty ;
}
else
{
ModDirectory = _penumbraResolveModDir ! . Invoke ( ) . ToLowerInvariant ( ) ;
}
}
protected override void Dispose ( bool disposing )
{
base . Dispose ( disposing ) ;
_redrawManager . Cancel ( ) ;
_penumbraModSettingChanged . Dispose ( ) ;
_penumbraGameObjectResourcePathResolved . Dispose ( ) ;
_penumbraDispose . Dispose ( ) ;
_penumbraInit . Dispose ( ) ;
_penumbraObjectIsRedrawn . Dispose ( ) ;
}
public async Task AssignTemporaryCollectionAsync ( ILogger logger , Guid collName , int idx )
{
if ( ! APIAvailable ) return ;
await _dalamudUtil . RunOnFrameworkThread ( ( ) = >
{
var retAssign = _penumbraAssignTemporaryCollection . Invoke ( collName , idx , forceAssignment : true ) ;
logger . LogTrace ( "Assigning Temp Collection {collName} to index {idx}, Success: {ret}" , collName , idx , retAssign ) ;
return collName ;
} ) . ConfigureAwait ( false ) ;
}
public async Task ConvertTextureFiles ( ILogger logger , Dictionary < string , string [ ] > textures , IProgress < ( string , int ) > progress , CancellationToken token )
{
if ( ! APIAvailable ) return ;
_mareMediator . Publish ( new HaltScanMessage ( nameof ( ConvertTextureFiles ) ) ) ;
int currentTexture = 0 ;
foreach ( var texture in textures )
{
if ( token . IsCancellationRequested ) break ;
progress . Report ( ( texture . Key , + + currentTexture ) ) ;
logger . LogInformation ( "Converting Texture {path} to {type}" , texture . Key , TextureType . Bc7Tex ) ;
var convertTask = _penumbraConvertTextureFile . Invoke ( texture . Key , texture . Key , TextureType . Bc7Tex , mipMaps : true ) ;
await convertTask . ConfigureAwait ( false ) ;
if ( convertTask . IsCompletedSuccessfully & & texture . Value . Any ( ) )
{
foreach ( var duplicatedTexture in texture . Value )
{
logger . LogInformation ( "Migrating duplicate {dup}" , duplicatedTexture ) ;
try
{
File . Copy ( texture . Key , duplicatedTexture , overwrite : true ) ;
}
catch ( Exception ex )
{
logger . LogError ( ex , "Failed to copy duplicate {dup}" , duplicatedTexture ) ;
}
}
}
}
_mareMediator . Publish ( new ResumeScanMessage ( nameof ( ConvertTextureFiles ) ) ) ;
await _dalamudUtil . RunOnFrameworkThread ( async ( ) = >
{
var gameObject = await _dalamudUtil . CreateGameObjectAsync ( await _dalamudUtil . GetPlayerPointerAsync ( ) . ConfigureAwait ( false ) ) . ConfigureAwait ( false ) ;
_penumbraRedraw . Invoke ( gameObject ! . ObjectIndex , setting : RedrawType . Redraw ) ;
} ) . ConfigureAwait ( false ) ;
}
public async Task < Guid > CreateTemporaryCollectionAsync ( ILogger logger , string uid )
{
if ( ! APIAvailable ) return Guid . Empty ;
return await _dalamudUtil . RunOnFrameworkThread ( ( ) = >
{
2025-08-25 18:21:14 +01:00
Guid collId ;
2025-08-22 02:19:48 +01:00
var collName = "ElfSync_" + uid ;
2025-08-25 18:21:14 +01:00
PenumbraApiEc penEC = _penumbraCreateNamedTemporaryCollection . Invoke ( uid , collName , out collId ) ;
2025-08-22 02:19:48 +01:00
logger . LogTrace ( "Creating Temp Collection {collName}, GUID: {collId}" , collName , collId ) ;
2025-08-25 18:21:14 +01:00
if ( penEC ! = PenumbraApiEc . Success )
{
logger . LogError ( "Failed to create temporary collection for {collName} with error code {penEC}. Please include this line in any error reports" , collName , penEC ) ;
return Guid . Empty ;
}
2025-08-22 02:19:48 +01:00
return collId ;
} ) . ConfigureAwait ( false ) ;
}
public async Task < Dictionary < string , HashSet < string > > ? > GetCharacterData ( ILogger logger , GameObjectHandler handler )
{
if ( ! APIAvailable ) return null ;
return await _dalamudUtil . RunOnFrameworkThread ( ( ) = >
{
logger . LogTrace ( "Calling On IPC: Penumbra.GetGameObjectResourcePaths" ) ;
var idx = handler . GetGameObject ( ) ? . ObjectIndex ;
if ( idx = = null ) return null ;
return _penumbraResourcePaths . Invoke ( idx . Value ) [ 0 ] ;
} ) . ConfigureAwait ( false ) ;
}
public string GetMetaManipulations ( )
{
if ( ! APIAvailable ) return string . Empty ;
return _penumbraGetMetaManipulations . Invoke ( ) ;
}
public async Task RedrawAsync ( ILogger logger , GameObjectHandler handler , Guid applicationId , CancellationToken token )
{
if ( ! APIAvailable | | _dalamudUtil . IsZoning ) return ;
try
{
await _redrawManager . RedrawSemaphore . WaitAsync ( token ) . ConfigureAwait ( false ) ;
await _redrawManager . PenumbraRedrawInternalAsync ( logger , handler , applicationId , ( chara ) = >
{
logger . LogDebug ( "[{appid}] Calling on IPC: PenumbraRedraw" , applicationId ) ;
_penumbraRedraw ! . Invoke ( chara . ObjectIndex , setting : RedrawType . Redraw ) ;
} , token ) . ConfigureAwait ( false ) ;
}
finally
{
_redrawManager . RedrawSemaphore . Release ( ) ;
}
}
public void RedrawNow ( ILogger logger , Guid applicationId , int objectIndex )
{
if ( ! APIAvailable | | _dalamudUtil . IsZoning ) return ;
logger . LogTrace ( "[{applicationId}] Immediately redrawing object index {objId}" , applicationId , objectIndex ) ;
_penumbraRedraw . Invoke ( objectIndex ) ;
}
public async Task RemoveTemporaryCollectionAsync ( ILogger logger , Guid applicationId , Guid collId )
{
if ( ! APIAvailable ) return ;
await _dalamudUtil . RunOnFrameworkThread ( ( ) = >
{
logger . LogTrace ( "[{applicationId}] Removing temp collection for {collId}" , applicationId , collId ) ;
var ret2 = _penumbraRemoveTemporaryCollection . Invoke ( collId ) ;
logger . LogTrace ( "[{applicationId}] RemoveTemporaryCollection: {ret2}" , applicationId , ret2 ) ;
} ) . ConfigureAwait ( false ) ;
}
public async Task < ( string [ ] forward , string [ ] [ ] reverse ) > ResolvePathsAsync ( string [ ] forward , string [ ] reverse )
{
return await _penumbraResolvePaths . Invoke ( forward , reverse ) . ConfigureAwait ( false ) ;
}
public async Task SetManipulationDataAsync ( ILogger logger , Guid applicationId , Guid collId , string manipulationData )
{
if ( ! APIAvailable ) return ;
await _dalamudUtil . RunOnFrameworkThread ( ( ) = >
{
logger . LogTrace ( "[{applicationId}] Manip: {data}" , applicationId , manipulationData ) ;
var retAdd = _penumbraAddTemporaryMod . Invoke ( "MareChara_Meta" , collId , [ ] , manipulationData , 0 ) ;
logger . LogTrace ( "[{applicationId}] Setting temp meta mod for {collId}, Success: {ret}" , applicationId , collId , retAdd ) ;
} ) . ConfigureAwait ( false ) ;
}
public async Task SetTemporaryModsAsync ( ILogger logger , Guid applicationId , Guid collId , Dictionary < string , string > modPaths )
{
if ( ! APIAvailable ) return ;
await _dalamudUtil . RunOnFrameworkThread ( ( ) = >
{
foreach ( var mod in modPaths )
{
logger . LogTrace ( "[{applicationId}] Change: {from} => {to}" , applicationId , mod . Key , mod . Value ) ;
}
var retRemove = _penumbraRemoveTemporaryMod . Invoke ( "MareChara_Files" , collId , 0 ) ;
logger . LogTrace ( "[{applicationId}] Removing temp files mod for {collId}, Success: {ret}" , applicationId , collId , retRemove ) ;
var retAdd = _penumbraAddTemporaryMod . Invoke ( "MareChara_Files" , collId , modPaths , string . Empty , 0 ) ;
logger . LogTrace ( "[{applicationId}] Setting temp files mod for {collId}, Success: {ret}" , applicationId , collId , retAdd ) ;
} ) . ConfigureAwait ( false ) ;
}
private void RedrawEvent ( IntPtr objectAddress , int objectTableIndex )
{
bool wasRequested = false ;
if ( _penumbraRedrawRequests . TryGetValue ( objectAddress , out var redrawRequest ) & & redrawRequest )
{
_penumbraRedrawRequests [ objectAddress ] = false ;
}
else
{
_mareMediator . Publish ( new PenumbraRedrawMessage ( objectAddress , objectTableIndex , wasRequested ) ) ;
}
}
private void ResourceLoaded ( IntPtr ptr , string arg1 , string arg2 )
{
if ( ptr ! = IntPtr . Zero & & string . Compare ( arg1 , arg2 , ignoreCase : true , System . Globalization . CultureInfo . InvariantCulture ) ! = 0 )
{
_mareMediator . Publish ( new PenumbraResourceLoadMessage ( ptr , arg1 , arg2 ) ) ;
}
}
private void PenumbraDispose ( )
{
_redrawManager . Cancel ( ) ;
_mareMediator . Publish ( new PenumbraDisposedMessage ( ) ) ;
}
private void PenumbraInit ( )
{
APIAvailable = true ;
ModDirectory = _penumbraResolveModDir . Invoke ( ) ;
_mareMediator . Publish ( new PenumbraInitializedMessage ( ) ) ;
_penumbraRedraw ! . Invoke ( 0 , setting : RedrawType . Redraw ) ;
}
}