mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Add option to disable disabling sound streaming.
This commit is contained in:
parent
ac2f2cf3b9
commit
e18fcafc51
4 changed files with 584 additions and 533 deletions
|
|
@ -20,6 +20,7 @@ namespace Penumbra
|
|||
|
||||
public bool DisableFileSystemNotifications { get; set; }
|
||||
|
||||
public bool DisableSoundStreaming { get; set; } = true;
|
||||
public bool EnableHttpApi { get; set; }
|
||||
public bool EnablePlayerWatch { get; set; } = false;
|
||||
public int WaitFrames { get; set; } = 30;
|
||||
|
|
|
|||
|
|
@ -12,364 +12,384 @@ using Penumbra.Mod;
|
|||
using Penumbra.Structs;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
// The ModCollectionCache contains all required temporary data to use a collection.
|
||||
// It will only be setup if a collection gets activated in any way.
|
||||
public class ModCollectionCache
|
||||
{
|
||||
// The ModCollectionCache contains all required temporary data to use a collection.
|
||||
// It will only be setup if a collection gets activated in any way.
|
||||
public class ModCollectionCache
|
||||
// Shared caches to avoid allocations.
|
||||
private static readonly BitArray FileSeen = new(256);
|
||||
private static readonly Dictionary< GamePath, Mod.Mod > RegisteredFiles = new(256);
|
||||
|
||||
public readonly Dictionary< string, Mod.Mod > AvailableMods = new();
|
||||
|
||||
private readonly SortedList< string, object? > _changedItems = new();
|
||||
public readonly Dictionary< GamePath, FullPath > ResolvedFiles = new();
|
||||
public readonly Dictionary< GamePath, GamePath > SwappedFiles = new();
|
||||
public readonly HashSet< FullPath > MissingFiles = new();
|
||||
public readonly HashSet< ulong > Checksums = new();
|
||||
public readonly MetaManager MetaManipulations;
|
||||
|
||||
public IReadOnlyDictionary< string, object? > ChangedItems
|
||||
{
|
||||
// Shared caches to avoid allocations.
|
||||
private static readonly BitArray FileSeen = new( 256 );
|
||||
private static readonly Dictionary< GamePath, Mod.Mod > RegisteredFiles = new( 256 );
|
||||
|
||||
public readonly Dictionary< string, Mod.Mod > AvailableMods = new();
|
||||
|
||||
private readonly SortedList< string, object? > _changedItems = new();
|
||||
public readonly Dictionary< GamePath, FullPath > ResolvedFiles = new();
|
||||
public readonly Dictionary< GamePath, GamePath > SwappedFiles = new();
|
||||
public readonly HashSet< FullPath > MissingFiles = new();
|
||||
public readonly HashSet< ulong > Checksums = new();
|
||||
public readonly MetaManager MetaManipulations;
|
||||
|
||||
public IReadOnlyDictionary< string, object? > ChangedItems
|
||||
get
|
||||
{
|
||||
get
|
||||
SetChangedItems();
|
||||
return _changedItems;
|
||||
}
|
||||
}
|
||||
|
||||
public ModCollectionCache( string collectionName, DirectoryInfo tempDir )
|
||||
=> MetaManipulations = new MetaManager( collectionName, ResolvedFiles, tempDir );
|
||||
|
||||
private static void ResetFileSeen( int size )
|
||||
{
|
||||
if( size < FileSeen.Length )
|
||||
{
|
||||
FileSeen.Length = size;
|
||||
FileSeen.SetAll( false );
|
||||
}
|
||||
else
|
||||
{
|
||||
FileSeen.SetAll( false );
|
||||
FileSeen.Length = size;
|
||||
}
|
||||
}
|
||||
|
||||
public void CalculateEffectiveFileList()
|
||||
{
|
||||
ResolvedFiles.Clear();
|
||||
SwappedFiles.Clear();
|
||||
MissingFiles.Clear();
|
||||
RegisteredFiles.Clear();
|
||||
_changedItems.Clear();
|
||||
|
||||
foreach( var mod in AvailableMods.Values
|
||||
.Where( m => m.Settings.Enabled )
|
||||
.OrderByDescending( m => m.Settings.Priority ) )
|
||||
{
|
||||
mod.Cache.ClearFileConflicts();
|
||||
AddFiles( mod );
|
||||
AddSwaps( mod );
|
||||
}
|
||||
|
||||
AddMetaFiles();
|
||||
Checksums.Clear();
|
||||
foreach( var file in ResolvedFiles )
|
||||
{
|
||||
Checksums.Add( file.Value.Crc64 );
|
||||
}
|
||||
}
|
||||
|
||||
private void SetChangedItems()
|
||||
{
|
||||
if( _changedItems.Count > 0 || ResolvedFiles.Count + SwappedFiles.Count + MetaManipulations.Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Skip meta files because IMCs would result in far too many false-positive items,
|
||||
// since they are per set instead of per item-slot/item/variant.
|
||||
var metaFiles = MetaManipulations.Files.Select( p => p.Item1 ).ToHashSet();
|
||||
var identifier = GameData.GameData.GetIdentifier();
|
||||
foreach( var resolved in ResolvedFiles.Keys.Where( file => !metaFiles.Contains( file ) ) )
|
||||
{
|
||||
SetChangedItems();
|
||||
return _changedItems;
|
||||
identifier.Identify( _changedItems, resolved );
|
||||
}
|
||||
|
||||
foreach( var swapped in SwappedFiles.Keys )
|
||||
{
|
||||
identifier.Identify( _changedItems, swapped );
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Unknown Error:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void AddFiles( Mod.Mod mod )
|
||||
{
|
||||
ResetFileSeen( mod.Data.Resources.ModFiles.Count );
|
||||
// Iterate in reverse so that later groups take precedence before earlier ones.
|
||||
foreach( var group in mod.Data.Meta.Groups.Values.Reverse() )
|
||||
{
|
||||
switch( group.SelectionType )
|
||||
{
|
||||
case SelectType.Single:
|
||||
AddFilesForSingle( group, mod );
|
||||
break;
|
||||
case SelectType.Multi:
|
||||
AddFilesForMulti( group, mod );
|
||||
break;
|
||||
default: throw new InvalidEnumArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
public ModCollectionCache( string collectionName, DirectoryInfo tempDir )
|
||||
=> MetaManipulations = new MetaManager( collectionName, ResolvedFiles, tempDir );
|
||||
AddRemainingFiles( mod );
|
||||
}
|
||||
|
||||
private static void ResetFileSeen( int size )
|
||||
private bool FilterFile( GamePath gamePath )
|
||||
{
|
||||
// If audio streaming is not disabled, replacing .scd files crashes the game,
|
||||
// so only add those files if it is disabled.
|
||||
if( !Penumbra.Config.DisableSoundStreaming
|
||||
&& gamePath.ToString().EndsWith( ".scd", StringComparison.InvariantCultureIgnoreCase ) )
|
||||
{
|
||||
if( size < FileSeen.Length )
|
||||
{
|
||||
FileSeen.Length = size;
|
||||
FileSeen.SetAll( false );
|
||||
}
|
||||
else
|
||||
{
|
||||
FileSeen.SetAll( false );
|
||||
FileSeen.Length = size;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CalculateEffectiveFileList()
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private void AddFile( Mod.Mod mod, GamePath gamePath, FullPath file )
|
||||
{
|
||||
if( FilterFile( gamePath ) )
|
||||
{
|
||||
ResolvedFiles.Clear();
|
||||
SwappedFiles.Clear();
|
||||
MissingFiles.Clear();
|
||||
RegisteredFiles.Clear();
|
||||
_changedItems.Clear();
|
||||
|
||||
foreach( var mod in AvailableMods.Values
|
||||
.Where( m => m.Settings.Enabled )
|
||||
.OrderByDescending( m => m.Settings.Priority ) )
|
||||
{
|
||||
mod.Cache.ClearFileConflicts();
|
||||
AddFiles( mod );
|
||||
AddSwaps( mod );
|
||||
}
|
||||
|
||||
AddMetaFiles();
|
||||
Checksums.Clear();
|
||||
foreach( var file in ResolvedFiles )
|
||||
Checksums.Add( file.Value.Crc64 );
|
||||
return;
|
||||
}
|
||||
|
||||
private void SetChangedItems()
|
||||
if( !RegisteredFiles.TryGetValue( gamePath, out var oldMod ) )
|
||||
{
|
||||
if( _changedItems.Count > 0 || ResolvedFiles.Count + SwappedFiles.Count + MetaManipulations.Count == 0 )
|
||||
RegisteredFiles.Add( gamePath, mod );
|
||||
ResolvedFiles[ gamePath ] = file;
|
||||
}
|
||||
else
|
||||
{
|
||||
mod.Cache.AddConflict( oldMod, gamePath );
|
||||
if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority )
|
||||
{
|
||||
oldMod.Cache.AddConflict( mod, gamePath );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMissingFile( FullPath file )
|
||||
{
|
||||
switch( file.Extension.ToLowerInvariant() )
|
||||
{
|
||||
case ".meta":
|
||||
case ".rgsp":
|
||||
return;
|
||||
default:
|
||||
MissingFiles.Add( file );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPathsForOption( Option option, Mod.Mod mod, bool enabled )
|
||||
{
|
||||
foreach( var (file, paths) in option.OptionFiles )
|
||||
{
|
||||
var fullPath = new FullPath( mod.Data.BasePath, file );
|
||||
var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
|
||||
if( idx < 0 )
|
||||
{
|
||||
AddMissingFile( fullPath );
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
var registeredFile = mod.Data.Resources.ModFiles[ idx ];
|
||||
if( !registeredFile.Exists )
|
||||
{
|
||||
// Skip meta files because IMCs would result in far too many false-positive items,
|
||||
// since they are per set instead of per item-slot/item/variant.
|
||||
var metaFiles = MetaManipulations.Files.Select( p => p.Item1 ).ToHashSet();
|
||||
var identifier = GameData.GameData.GetIdentifier();
|
||||
foreach( var resolved in ResolvedFiles.Keys.Where( file => !metaFiles.Contains( file ) ) )
|
||||
{
|
||||
identifier.Identify( _changedItems, resolved );
|
||||
}
|
||||
|
||||
foreach( var swapped in SwappedFiles.Keys )
|
||||
{
|
||||
identifier.Identify( _changedItems, swapped );
|
||||
}
|
||||
AddMissingFile( registeredFile );
|
||||
continue;
|
||||
}
|
||||
catch( Exception e )
|
||||
|
||||
FileSeen.Set( idx, true );
|
||||
if( enabled )
|
||||
{
|
||||
PluginLog.Error( $"Unknown Error:\n{e}" );
|
||||
foreach( var path in paths )
|
||||
{
|
||||
AddFile( mod, path, registeredFile );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFilesForSingle( OptionGroup singleGroup, Mod.Mod mod )
|
||||
{
|
||||
Debug.Assert( singleGroup.SelectionType == SelectType.Single );
|
||||
|
||||
private void AddFiles( Mod.Mod mod )
|
||||
if( !mod.Settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) )
|
||||
{
|
||||
ResetFileSeen( mod.Data.Resources.ModFiles.Count );
|
||||
// Iterate in reverse so that later groups take precedence before earlier ones.
|
||||
foreach( var group in mod.Data.Meta.Groups.Values.Reverse() )
|
||||
{
|
||||
switch( group.SelectionType )
|
||||
{
|
||||
case SelectType.Single:
|
||||
AddFilesForSingle( group, mod );
|
||||
break;
|
||||
case SelectType.Multi:
|
||||
AddFilesForMulti( group, mod );
|
||||
break;
|
||||
default: throw new InvalidEnumArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
AddRemainingFiles( mod );
|
||||
setting = 0;
|
||||
}
|
||||
|
||||
private void AddFile( Mod.Mod mod, GamePath gamePath, FullPath file )
|
||||
for( var i = 0; i < singleGroup.Options.Count; ++i )
|
||||
{
|
||||
if( !RegisteredFiles.TryGetValue( gamePath, out var oldMod ) )
|
||||
AddPathsForOption( singleGroup.Options[ i ], mod, setting == i );
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFilesForMulti( OptionGroup multiGroup, Mod.Mod mod )
|
||||
{
|
||||
Debug.Assert( multiGroup.SelectionType == SelectType.Multi );
|
||||
|
||||
if( !mod.Settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Also iterate options in reverse so that later options take precedence before earlier ones.
|
||||
for( var i = multiGroup.Options.Count - 1; i >= 0; --i )
|
||||
{
|
||||
AddPathsForOption( multiGroup.Options[ i ], mod, ( setting & ( 1 << i ) ) != 0 );
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRemainingFiles( Mod.Mod mod )
|
||||
{
|
||||
for( var i = 0; i < mod.Data.Resources.ModFiles.Count; ++i )
|
||||
{
|
||||
if( FileSeen.Get( i ) )
|
||||
{
|
||||
RegisteredFiles.Add( gamePath, mod );
|
||||
ResolvedFiles[ gamePath ] = file;
|
||||
continue;
|
||||
}
|
||||
|
||||
var file = mod.Data.Resources.ModFiles[ i ];
|
||||
if( file.Exists )
|
||||
{
|
||||
AddFile( mod, file.ToGamePath( mod.Data.BasePath ), file );
|
||||
}
|
||||
else
|
||||
{
|
||||
mod.Cache.AddConflict( oldMod, gamePath );
|
||||
MissingFiles.Add( file );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMetaFiles()
|
||||
{
|
||||
foreach( var (gamePath, file) in MetaManipulations.Files )
|
||||
{
|
||||
if( RegisteredFiles.TryGetValue( gamePath, out var mod ) )
|
||||
{
|
||||
PluginLog.Warning(
|
||||
$"The meta manipulation file {gamePath} was already completely replaced by {mod.Data.Meta.Name}. This is probably a mistake. Using the custom file {file.FullName}." );
|
||||
}
|
||||
|
||||
ResolvedFiles[ gamePath ] = file;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSwaps( Mod.Mod mod )
|
||||
{
|
||||
foreach( var (key, value) in mod.Data.Meta.FileSwaps.Where( kvp => !FilterFile( kvp.Key ) ) )
|
||||
{
|
||||
if( !RegisteredFiles.TryGetValue( key, out var oldMod ) )
|
||||
{
|
||||
RegisteredFiles.Add( key, mod );
|
||||
SwappedFiles.Add( key, value );
|
||||
}
|
||||
else
|
||||
{
|
||||
mod.Cache.AddConflict( oldMod, key );
|
||||
if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority )
|
||||
{
|
||||
oldMod.Cache.AddConflict( mod, gamePath );
|
||||
oldMod.Cache.AddConflict( mod, key );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMissingFile( FullPath file )
|
||||
{
|
||||
switch( file.Extension.ToLowerInvariant() )
|
||||
{
|
||||
case ".meta":
|
||||
case ".rgsp":
|
||||
return;
|
||||
default:
|
||||
MissingFiles.Add( file );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPathsForOption( Option option, Mod.Mod mod, bool enabled )
|
||||
{
|
||||
foreach( var (file, paths) in option.OptionFiles )
|
||||
{
|
||||
var fullPath = new FullPath(mod.Data.BasePath, file);
|
||||
var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals(fullPath) );
|
||||
if( idx < 0 )
|
||||
{
|
||||
AddMissingFile( fullPath );
|
||||
continue;
|
||||
}
|
||||
|
||||
var registeredFile = mod.Data.Resources.ModFiles[ idx ];
|
||||
if( !registeredFile.Exists )
|
||||
{
|
||||
AddMissingFile( registeredFile );
|
||||
continue;
|
||||
}
|
||||
|
||||
FileSeen.Set( idx, true );
|
||||
if( enabled )
|
||||
{
|
||||
foreach( var path in paths )
|
||||
{
|
||||
AddFile( mod, path, registeredFile );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFilesForSingle( OptionGroup singleGroup, Mod.Mod mod )
|
||||
{
|
||||
Debug.Assert( singleGroup.SelectionType == SelectType.Single );
|
||||
|
||||
if( !mod.Settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) )
|
||||
{
|
||||
setting = 0;
|
||||
}
|
||||
|
||||
for( var i = 0; i < singleGroup.Options.Count; ++i )
|
||||
{
|
||||
AddPathsForOption( singleGroup.Options[ i ], mod, setting == i );
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFilesForMulti( OptionGroup multiGroup, Mod.Mod mod )
|
||||
{
|
||||
Debug.Assert( multiGroup.SelectionType == SelectType.Multi );
|
||||
|
||||
if( !mod.Settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Also iterate options in reverse so that later options take precedence before earlier ones.
|
||||
for( var i = multiGroup.Options.Count - 1; i >= 0; --i )
|
||||
{
|
||||
AddPathsForOption( multiGroup.Options[ i ], mod, ( setting & ( 1 << i ) ) != 0 );
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRemainingFiles( Mod.Mod mod )
|
||||
{
|
||||
for( var i = 0; i < mod.Data.Resources.ModFiles.Count; ++i )
|
||||
{
|
||||
if( FileSeen.Get( i ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var file = mod.Data.Resources.ModFiles[ i ];
|
||||
if( file.Exists )
|
||||
{
|
||||
AddFile( mod, file.ToGamePath( mod.Data.BasePath ), file );
|
||||
}
|
||||
else
|
||||
{
|
||||
MissingFiles.Add( file );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMetaFiles()
|
||||
{
|
||||
foreach( var (gamePath, file) in MetaManipulations.Files )
|
||||
{
|
||||
if( RegisteredFiles.TryGetValue( gamePath, out var mod ) )
|
||||
{
|
||||
PluginLog.Warning(
|
||||
$"The meta manipulation file {gamePath} was already completely replaced by {mod.Data.Meta.Name}. This is probably a mistake. Using the custom file {file.FullName}." );
|
||||
}
|
||||
|
||||
ResolvedFiles[ gamePath ] = file;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSwaps( Mod.Mod mod )
|
||||
{
|
||||
foreach( var swap in mod.Data.Meta.FileSwaps )
|
||||
{
|
||||
if( !RegisteredFiles.TryGetValue( swap.Key, out var oldMod ) )
|
||||
{
|
||||
RegisteredFiles.Add( swap.Key, mod );
|
||||
SwappedFiles.Add( swap.Key, swap.Value );
|
||||
}
|
||||
else
|
||||
{
|
||||
mod.Cache.AddConflict( oldMod, swap.Key );
|
||||
if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority )
|
||||
{
|
||||
oldMod.Cache.AddConflict( mod, swap.Key );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddManipulations( Mod.Mod mod )
|
||||
{
|
||||
foreach( var manip in mod.Data.Resources.MetaManipulations.GetManipulationsForConfig( mod.Settings, mod.Data.Meta ) )
|
||||
{
|
||||
if( !MetaManipulations.TryGetValue( manip, out var oldMod ) )
|
||||
{
|
||||
MetaManipulations.ApplyMod( manip, mod );
|
||||
}
|
||||
else
|
||||
{
|
||||
mod.Cache.AddConflict( oldMod, manip );
|
||||
if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority )
|
||||
{
|
||||
oldMod.Cache.AddConflict( mod, manip );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateMetaManipulations()
|
||||
{
|
||||
MetaManipulations.Reset( false );
|
||||
|
||||
foreach( var mod in AvailableMods.Values.Where( m => m.Settings.Enabled && m.Data.Resources.MetaManipulations.Count > 0 ) )
|
||||
{
|
||||
mod.Cache.ClearMetaConflicts();
|
||||
AddManipulations( mod );
|
||||
}
|
||||
|
||||
MetaManipulations.WriteNewFiles();
|
||||
}
|
||||
|
||||
public void RemoveMod( DirectoryInfo basePath )
|
||||
{
|
||||
if( AvailableMods.TryGetValue( basePath.Name, out var mod ) )
|
||||
{
|
||||
AvailableMods.Remove( basePath.Name );
|
||||
if( mod.Settings.Enabled )
|
||||
{
|
||||
CalculateEffectiveFileList();
|
||||
if( mod.Data.Resources.MetaManipulations.Count > 0 )
|
||||
{
|
||||
UpdateMetaManipulations();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PriorityComparer : IComparer< Mod.Mod >
|
||||
{
|
||||
public int Compare( Mod.Mod? x, Mod.Mod? y )
|
||||
=> ( x?.Settings.Priority ?? 0 ).CompareTo( y?.Settings.Priority ?? 0 );
|
||||
}
|
||||
|
||||
private static readonly PriorityComparer Comparer = new();
|
||||
|
||||
public void AddMod( ModSettings settings, ModData data, bool updateFileList = true )
|
||||
{
|
||||
if( !AvailableMods.TryGetValue( data.BasePath.Name, out var existingMod ) )
|
||||
{
|
||||
var newMod = new Mod.Mod( settings, data );
|
||||
AvailableMods[ data.BasePath.Name ] = newMod;
|
||||
|
||||
if( updateFileList && settings.Enabled )
|
||||
{
|
||||
CalculateEffectiveFileList();
|
||||
if( data.Resources.MetaManipulations.Count > 0 )
|
||||
{
|
||||
UpdateMetaManipulations();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FullPath? GetCandidateForGameFile( GamePath gameResourcePath )
|
||||
{
|
||||
if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if( candidate.FullName.Length >= 260 || !candidate.Exists )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
public GamePath? GetSwappedFilePath( GamePath gameResourcePath )
|
||||
=> SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null;
|
||||
|
||||
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
|
||||
=> GetCandidateForGameFile( gameResourcePath )?.FullName.Replace( '\\', '/' ) ?? GetSwappedFilePath( gameResourcePath ) ?? null;
|
||||
}
|
||||
|
||||
private void AddManipulations( Mod.Mod mod )
|
||||
{
|
||||
foreach( var manip in mod.Data.Resources.MetaManipulations.GetManipulationsForConfig( mod.Settings, mod.Data.Meta ) )
|
||||
{
|
||||
if( !MetaManipulations.TryGetValue( manip, out var oldMod ) )
|
||||
{
|
||||
MetaManipulations.ApplyMod( manip, mod );
|
||||
}
|
||||
else
|
||||
{
|
||||
mod.Cache.AddConflict( oldMod, manip );
|
||||
if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority )
|
||||
{
|
||||
oldMod.Cache.AddConflict( mod, manip );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateMetaManipulations()
|
||||
{
|
||||
MetaManipulations.Reset( false );
|
||||
|
||||
foreach( var mod in AvailableMods.Values.Where( m => m.Settings.Enabled && m.Data.Resources.MetaManipulations.Count > 0 ) )
|
||||
{
|
||||
mod.Cache.ClearMetaConflicts();
|
||||
AddManipulations( mod );
|
||||
}
|
||||
|
||||
MetaManipulations.WriteNewFiles();
|
||||
}
|
||||
|
||||
public void RemoveMod( DirectoryInfo basePath )
|
||||
{
|
||||
if( AvailableMods.TryGetValue( basePath.Name, out var mod ) )
|
||||
{
|
||||
AvailableMods.Remove( basePath.Name );
|
||||
if( mod.Settings.Enabled )
|
||||
{
|
||||
CalculateEffectiveFileList();
|
||||
if( mod.Data.Resources.MetaManipulations.Count > 0 )
|
||||
{
|
||||
UpdateMetaManipulations();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PriorityComparer : IComparer< Mod.Mod >
|
||||
{
|
||||
public int Compare( Mod.Mod? x, Mod.Mod? y )
|
||||
=> ( x?.Settings.Priority ?? 0 ).CompareTo( y?.Settings.Priority ?? 0 );
|
||||
}
|
||||
|
||||
private static readonly PriorityComparer Comparer = new();
|
||||
|
||||
public void AddMod( ModSettings settings, ModData data, bool updateFileList = true )
|
||||
{
|
||||
if( !AvailableMods.TryGetValue( data.BasePath.Name, out var existingMod ) )
|
||||
{
|
||||
var newMod = new Mod.Mod( settings, data );
|
||||
AvailableMods[ data.BasePath.Name ] = newMod;
|
||||
|
||||
if( updateFileList && settings.Enabled )
|
||||
{
|
||||
CalculateEffectiveFileList();
|
||||
if( data.Resources.MetaManipulations.Count > 0 )
|
||||
{
|
||||
UpdateMetaManipulations();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FullPath? GetCandidateForGameFile( GamePath gameResourcePath )
|
||||
{
|
||||
if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if( candidate.FullName.Length >= 260 || !candidate.Exists )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
public GamePath? GetSwappedFilePath( GamePath gameResourcePath )
|
||||
=> SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null;
|
||||
|
||||
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
|
||||
=> GetCandidateForGameFile( gameResourcePath )?.FullName.Replace( '\\', '/' ) ?? GetSwappedFilePath( gameResourcePath ) ?? null;
|
||||
}
|
||||
|
|
@ -14,244 +14,246 @@ using Penumbra.PlayerWatch;
|
|||
using Penumbra.UI;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra
|
||||
namespace Penumbra;
|
||||
|
||||
public class Penumbra : IDalamudPlugin
|
||||
{
|
||||
public class Penumbra : IDalamudPlugin
|
||||
public string Name { get; } = "Penumbra";
|
||||
public string PluginDebugTitleStr { get; } = "Penumbra - Debug Build";
|
||||
|
||||
private const string CommandName = "/penumbra";
|
||||
|
||||
public static Configuration Config { get; private set; } = null!;
|
||||
public static IPlayerWatcher PlayerWatcher { get; private set; } = null!;
|
||||
|
||||
public ResourceLoader ResourceLoader { get; }
|
||||
public SettingsInterface SettingsInterface { get; }
|
||||
public MusicManager MusicManager { get; }
|
||||
public ObjectReloader ObjectReloader { get; }
|
||||
|
||||
public PenumbraApi Api { get; }
|
||||
public PenumbraIpc Ipc { get; }
|
||||
|
||||
private WebServer? _webServer;
|
||||
|
||||
public Penumbra( DalamudPluginInterface pluginInterface )
|
||||
{
|
||||
public string Name { get; } = "Penumbra";
|
||||
public string PluginDebugTitleStr { get; } = "Penumbra - Debug Build";
|
||||
FFXIVClientStructs.Resolver.Initialize();
|
||||
Dalamud.Initialize( pluginInterface );
|
||||
GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage );
|
||||
Config = Configuration.Load();
|
||||
|
||||
private const string CommandName = "/penumbra";
|
||||
|
||||
public static Configuration Config { get; private set; } = null!;
|
||||
public static IPlayerWatcher PlayerWatcher { get; private set; } = null!;
|
||||
|
||||
public ResourceLoader ResourceLoader { get; }
|
||||
public SettingsInterface SettingsInterface { get; }
|
||||
public MusicManager MusicManager { get; }
|
||||
public ObjectReloader ObjectReloader { get; }
|
||||
|
||||
public PenumbraApi Api { get; }
|
||||
public PenumbraIpc Ipc { get; }
|
||||
|
||||
private WebServer? _webServer;
|
||||
|
||||
public Penumbra( DalamudPluginInterface pluginInterface )
|
||||
MusicManager = new MusicManager();
|
||||
if( Config.DisableSoundStreaming )
|
||||
{
|
||||
FFXIVClientStructs.Resolver.Initialize();
|
||||
Dalamud.Initialize( pluginInterface );
|
||||
GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage );
|
||||
Config = Configuration.Load();
|
||||
|
||||
MusicManager = new MusicManager();
|
||||
MusicManager.DisableStreaming();
|
||||
|
||||
var gameUtils = Service< ResidentResources >.Set();
|
||||
PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects );
|
||||
Service< MetaDefaults >.Set();
|
||||
var modManager = Service< ModManager >.Set();
|
||||
|
||||
modManager.DiscoverMods();
|
||||
|
||||
ObjectReloader = new ObjectReloader( modManager, Config.WaitFrames );
|
||||
|
||||
ResourceLoader = new ResourceLoader( this );
|
||||
|
||||
Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand )
|
||||
{
|
||||
HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods",
|
||||
} );
|
||||
|
||||
ResourceLoader.Init();
|
||||
ResourceLoader.Enable();
|
||||
|
||||
gameUtils.ReloadResidentResources();
|
||||
|
||||
SettingsInterface = new SettingsInterface( this );
|
||||
|
||||
if( Config.EnableHttpApi )
|
||||
{
|
||||
CreateWebServer();
|
||||
}
|
||||
|
||||
if( !Config.EnablePlayerWatch || !Config.IsEnabled )
|
||||
{
|
||||
PlayerWatcher.Disable();
|
||||
}
|
||||
|
||||
PlayerWatcher.PlayerChanged += p =>
|
||||
{
|
||||
PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name );
|
||||
ObjectReloader.RedrawObject( p, RedrawType.OnlyWithSettings );
|
||||
};
|
||||
|
||||
Api = new PenumbraApi( this );
|
||||
SubscribeItemLinks();
|
||||
Ipc = new PenumbraIpc( pluginInterface, Api );
|
||||
}
|
||||
|
||||
public bool Enable()
|
||||
var gameUtils = Service< ResidentResources >.Set();
|
||||
PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects );
|
||||
Service< MetaDefaults >.Set();
|
||||
var modManager = Service< ModManager >.Set();
|
||||
|
||||
modManager.DiscoverMods();
|
||||
|
||||
ObjectReloader = new ObjectReloader( modManager, Config.WaitFrames );
|
||||
|
||||
ResourceLoader = new ResourceLoader( this );
|
||||
|
||||
Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand )
|
||||
{
|
||||
if( Config.IsEnabled )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods",
|
||||
} );
|
||||
|
||||
Config.IsEnabled = true;
|
||||
Service< ResidentResources >.Get().ReloadResidentResources();
|
||||
if( Config.EnablePlayerWatch )
|
||||
{
|
||||
PlayerWatcher.SetStatus( true );
|
||||
}
|
||||
ResourceLoader.Init();
|
||||
ResourceLoader.Enable();
|
||||
|
||||
Config.Save();
|
||||
ObjectReloader.RedrawAll( RedrawType.WithSettings );
|
||||
return true;
|
||||
gameUtils.ReloadResidentResources();
|
||||
|
||||
SettingsInterface = new SettingsInterface( this );
|
||||
|
||||
if( Config.EnableHttpApi )
|
||||
{
|
||||
CreateWebServer();
|
||||
}
|
||||
|
||||
public bool Disable()
|
||||
if( !Config.EnablePlayerWatch || !Config.IsEnabled )
|
||||
{
|
||||
if( !Config.IsEnabled )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Config.IsEnabled = false;
|
||||
Service< ResidentResources >.Get().ReloadResidentResources();
|
||||
if( Config.EnablePlayerWatch )
|
||||
{
|
||||
PlayerWatcher.SetStatus( false );
|
||||
}
|
||||
|
||||
Config.Save();
|
||||
ObjectReloader.RedrawAll( RedrawType.WithoutSettings );
|
||||
return true;
|
||||
PlayerWatcher.Disable();
|
||||
}
|
||||
|
||||
public bool SetEnabled( bool enabled )
|
||||
=> enabled ? Enable() : Disable();
|
||||
|
||||
private void SubscribeItemLinks()
|
||||
PlayerWatcher.PlayerChanged += p =>
|
||||
{
|
||||
Api.ChangedItemTooltip += it =>
|
||||
PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name );
|
||||
ObjectReloader.RedrawObject( p, RedrawType.OnlyWithSettings );
|
||||
};
|
||||
|
||||
Api = new PenumbraApi( this );
|
||||
SubscribeItemLinks();
|
||||
Ipc = new PenumbraIpc( pluginInterface, Api );
|
||||
}
|
||||
|
||||
public bool Enable()
|
||||
{
|
||||
if( Config.IsEnabled )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Config.IsEnabled = true;
|
||||
Service< ResidentResources >.Get().ReloadResidentResources();
|
||||
if( Config.EnablePlayerWatch )
|
||||
{
|
||||
PlayerWatcher.SetStatus( true );
|
||||
}
|
||||
|
||||
Config.Save();
|
||||
ObjectReloader.RedrawAll( RedrawType.WithSettings );
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Disable()
|
||||
{
|
||||
if( !Config.IsEnabled )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Config.IsEnabled = false;
|
||||
Service< ResidentResources >.Get().ReloadResidentResources();
|
||||
if( Config.EnablePlayerWatch )
|
||||
{
|
||||
PlayerWatcher.SetStatus( false );
|
||||
}
|
||||
|
||||
Config.Save();
|
||||
ObjectReloader.RedrawAll( RedrawType.WithoutSettings );
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SetEnabled( bool enabled )
|
||||
=> enabled ? Enable() : Disable();
|
||||
|
||||
private void SubscribeItemLinks()
|
||||
{
|
||||
Api.ChangedItemTooltip += it =>
|
||||
{
|
||||
if( it is Item )
|
||||
{
|
||||
if( it is Item )
|
||||
ImGui.Text( "Left Click to create an item link in chat." );
|
||||
}
|
||||
};
|
||||
Api.ChangedItemClicked += ( button, it ) =>
|
||||
{
|
||||
if( button == MouseButton.Left && it is Item item )
|
||||
{
|
||||
ChatUtil.LinkItem( item );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void CreateWebServer()
|
||||
{
|
||||
const string prefix = "http://localhost:42069/";
|
||||
|
||||
ShutdownWebServer();
|
||||
|
||||
_webServer = new WebServer( o => o
|
||||
.WithUrlPrefix( prefix )
|
||||
.WithMode( HttpListenerMode.EmbedIO ) )
|
||||
.WithCors( prefix )
|
||||
.WithWebApi( "/api", m => m
|
||||
.WithController( () => new ModsController( this ) ) );
|
||||
|
||||
_webServer.StateChanged += ( s, e ) => PluginLog.Information( $"WebServer New State - {e.NewState}" );
|
||||
|
||||
_webServer.RunAsync();
|
||||
}
|
||||
|
||||
public void ShutdownWebServer()
|
||||
{
|
||||
_webServer?.Dispose();
|
||||
_webServer = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Ipc.Dispose();
|
||||
Api.Dispose();
|
||||
SettingsInterface.Dispose();
|
||||
ObjectReloader.Dispose();
|
||||
PlayerWatcher.Dispose();
|
||||
|
||||
Dalamud.Commands.RemoveHandler( CommandName );
|
||||
|
||||
ResourceLoader.Dispose();
|
||||
|
||||
ShutdownWebServer();
|
||||
}
|
||||
|
||||
private void OnCommand( string command, string rawArgs )
|
||||
{
|
||||
const string modsEnabled = "Your mods have now been enabled.";
|
||||
const string modsDisabled = "Your mods have now been disabled.";
|
||||
|
||||
var args = rawArgs.Split( new[] { ' ' }, 2 );
|
||||
if( args.Length > 0 && args[ 0 ].Length > 0 )
|
||||
{
|
||||
switch( args[ 0 ] )
|
||||
{
|
||||
case "reload":
|
||||
{
|
||||
ImGui.Text( "Left Click to create an item link in chat." );
|
||||
Service< ModManager >.Get().DiscoverMods();
|
||||
Dalamud.Chat.Print(
|
||||
$"Reloaded Penumbra mods. You have {Service< ModManager >.Get()?.Mods.Count} mods."
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
Api.ChangedItemClicked += ( button, it ) =>
|
||||
{
|
||||
if( button == MouseButton.Left && it is Item item )
|
||||
case "redraw":
|
||||
{
|
||||
ChatUtil.LinkItem( item );
|
||||
if( args.Length > 1 )
|
||||
{
|
||||
ObjectReloader.RedrawObject( args[ 1 ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
ObjectReloader.RedrawAll();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void CreateWebServer()
|
||||
{
|
||||
const string prefix = "http://localhost:42069/";
|
||||
|
||||
ShutdownWebServer();
|
||||
|
||||
_webServer = new WebServer( o => o
|
||||
.WithUrlPrefix( prefix )
|
||||
.WithMode( HttpListenerMode.EmbedIO ) )
|
||||
.WithCors( prefix )
|
||||
.WithWebApi( "/api", m => m
|
||||
.WithController( () => new ModsController( this ) ) );
|
||||
|
||||
_webServer.StateChanged += ( s, e ) => PluginLog.Information( $"WebServer New State - {e.NewState}" );
|
||||
|
||||
_webServer.RunAsync();
|
||||
}
|
||||
|
||||
public void ShutdownWebServer()
|
||||
{
|
||||
_webServer?.Dispose();
|
||||
_webServer = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Ipc.Dispose();
|
||||
Api.Dispose();
|
||||
SettingsInterface.Dispose();
|
||||
ObjectReloader.Dispose();
|
||||
PlayerWatcher.Dispose();
|
||||
|
||||
Dalamud.Commands.RemoveHandler( CommandName );
|
||||
|
||||
ResourceLoader.Dispose();
|
||||
|
||||
ShutdownWebServer();
|
||||
}
|
||||
|
||||
private void OnCommand( string command, string rawArgs )
|
||||
{
|
||||
const string modsEnabled = "Your mods have now been enabled.";
|
||||
const string modsDisabled = "Your mods have now been disabled.";
|
||||
|
||||
var args = rawArgs.Split( new[] { ' ' }, 2 );
|
||||
if( args.Length > 0 && args[ 0 ].Length > 0 )
|
||||
{
|
||||
switch( args[ 0 ] )
|
||||
case "debug":
|
||||
{
|
||||
case "reload":
|
||||
{
|
||||
Service< ModManager >.Get().DiscoverMods();
|
||||
Dalamud.Chat.Print(
|
||||
$"Reloaded Penumbra mods. You have {Service< ModManager >.Get()?.Mods.Count} mods."
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "redraw":
|
||||
{
|
||||
if( args.Length > 1 )
|
||||
{
|
||||
ObjectReloader.RedrawObject( args[ 1 ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
ObjectReloader.RedrawAll();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "debug":
|
||||
{
|
||||
SettingsInterface.MakeDebugTabVisible();
|
||||
break;
|
||||
}
|
||||
case "enable":
|
||||
{
|
||||
Dalamud.Chat.Print( Enable()
|
||||
? "Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable"
|
||||
: modsEnabled );
|
||||
break;
|
||||
}
|
||||
case "disable":
|
||||
{
|
||||
Dalamud.Chat.Print( Disable()
|
||||
? "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable"
|
||||
: modsDisabled );
|
||||
break;
|
||||
}
|
||||
case "toggle":
|
||||
{
|
||||
SetEnabled( !Config.IsEnabled );
|
||||
Dalamud.Chat.Print( Config.IsEnabled
|
||||
? modsEnabled
|
||||
: modsDisabled );
|
||||
break;
|
||||
}
|
||||
SettingsInterface.MakeDebugTabVisible();
|
||||
break;
|
||||
}
|
||||
case "enable":
|
||||
{
|
||||
Dalamud.Chat.Print( Enable()
|
||||
? "Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable"
|
||||
: modsEnabled );
|
||||
break;
|
||||
}
|
||||
case "disable":
|
||||
{
|
||||
Dalamud.Chat.Print( Disable()
|
||||
? "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable"
|
||||
: modsDisabled );
|
||||
break;
|
||||
}
|
||||
case "toggle":
|
||||
{
|
||||
SetEnabled( !Config.IsEnabled );
|
||||
Dalamud.Chat.Print( Config.IsEnabled
|
||||
? modsEnabled
|
||||
: modsDisabled );
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
SettingsInterface.FlipVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
SettingsInterface.FlipVisibility();
|
||||
}
|
||||
}
|
||||
|
|
@ -194,6 +194,33 @@ public partial class SettingsInterface
|
|||
"Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window." );
|
||||
}
|
||||
|
||||
private void DrawDisableSoundStreamingBox()
|
||||
{
|
||||
var tmp = Penumbra.Config.DisableSoundStreaming;
|
||||
if( ImGui.Checkbox( "Disable Audio Streaming", ref tmp ) && tmp != Penumbra.Config.DisableSoundStreaming )
|
||||
{
|
||||
Penumbra.Config.DisableSoundStreaming = tmp;
|
||||
_configChanged = true;
|
||||
if( tmp )
|
||||
{
|
||||
_base._penumbra.MusicManager.DisableStreaming();
|
||||
}
|
||||
else
|
||||
{
|
||||
_base._penumbra.MusicManager.EnableStreaming();
|
||||
}
|
||||
|
||||
_base.ReloadMods();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGuiComponents.HelpMarker(
|
||||
"Disable streaming in the games audio engine.\n"
|
||||
+ "If you do not disable streaming, you can not replace sound files in the game (*.scd files), they will be ignored by Penumbra.\n\n"
|
||||
+ "Only touch this if you experience sound problems.\n"
|
||||
+ "If you toggle this, make sure no modified or to-be-modified sound file is currently playing or was recently playing, else you might crash." );
|
||||
}
|
||||
|
||||
private void DrawLogLoadedFilesBox()
|
||||
{
|
||||
ImGui.Checkbox( "Log Loaded Files", ref _base._penumbra.ResourceLoader.LogAllFiles );
|
||||
|
|
@ -306,6 +333,7 @@ public partial class SettingsInterface
|
|||
private void DrawAdvancedSettings()
|
||||
{
|
||||
DrawTempFolder();
|
||||
DrawDisableSoundStreamingBox();
|
||||
DrawLogLoadedFilesBox();
|
||||
DrawDisableNotificationsBox();
|
||||
DrawEnableHttpApiBox();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue