mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-14 20:54:16 +01:00
A lot of interface stuff, some more cleanup and fixes. Main functionality should be mostly fine, importing works. Missing a lot of mod edit options.
This commit is contained in:
parent
8dd681bdda
commit
dbb9931189
77 changed files with 3332 additions and 2066 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
||||||
Subproject commit a832fb6ca5e7c6cb4e35a51a08d30d1800f405da
|
Subproject commit 1a3cd1f881f3b6c2c4d9d4b20f054d1ab5ccc014
|
||||||
|
|
@ -76,7 +76,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
||||||
_penumbra!.ObjectReloader.RedrawAll( setting );
|
_penumbra!.ObjectReloader.RedrawAll( setting );
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolvePath( string path, Mods.Mod2.Manager _, ModCollection collection )
|
private static string ResolvePath( string path, Mods.Mod.Manager _, ModCollection collection )
|
||||||
{
|
{
|
||||||
if( !Penumbra.Config.EnableMods )
|
if( !Penumbra.Config.EnableMods )
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ public class SimpleRedirectManager
|
||||||
return RedirectResult.NoPermission;
|
return RedirectResult.NoPermission;
|
||||||
}
|
}
|
||||||
|
|
||||||
if( Mod2.FilterFile( path ) )
|
if( Mod.FilterFile( path ) )
|
||||||
{
|
{
|
||||||
return RedirectResult.FilteredGamePath;
|
return RedirectResult.FilteredGamePath;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,7 @@ public partial class ModCollection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnModRemovedActive( bool meta, IEnumerable< ModSettings2? > settings )
|
private void OnModRemovedActive( bool meta, IEnumerable< ModSettings? > settings )
|
||||||
{
|
{
|
||||||
foreach( var (collection, _) in this.Zip( settings ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) )
|
foreach( var (collection, _) in this.Zip( settings ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) )
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
|
using OtterGui.Filesystem;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
|
|
||||||
|
|
@ -27,7 +28,7 @@ public partial class ModCollection
|
||||||
public delegate void CollectionChangeDelegate( Type type, ModCollection? oldCollection, ModCollection? newCollection,
|
public delegate void CollectionChangeDelegate( Type type, ModCollection? oldCollection, ModCollection? newCollection,
|
||||||
string? characterName = null );
|
string? characterName = null );
|
||||||
|
|
||||||
private readonly Mod2.Manager _modManager;
|
private readonly Mod.Manager _modManager;
|
||||||
|
|
||||||
// The empty collection is always available and always has index 0.
|
// The empty collection is always available and always has index 0.
|
||||||
// It can not be deleted or moved.
|
// It can not be deleted or moved.
|
||||||
|
|
@ -59,7 +60,7 @@ public partial class ModCollection
|
||||||
public IEnumerable< ModCollection > GetEnumeratorWithEmpty()
|
public IEnumerable< ModCollection > GetEnumeratorWithEmpty()
|
||||||
=> _collections;
|
=> _collections;
|
||||||
|
|
||||||
public Manager( Mod2.Manager manager )
|
public Manager( Mod.Manager manager )
|
||||||
{
|
{
|
||||||
_modManager = manager;
|
_modManager = manager;
|
||||||
|
|
||||||
|
|
@ -207,7 +208,7 @@ public partial class ModCollection
|
||||||
|
|
||||||
|
|
||||||
// A changed mod path forces changes for all collections, active and inactive.
|
// A changed mod path forces changes for all collections, active and inactive.
|
||||||
private void OnModPathChanged( ModPathChangeType type, Mod2 mod, DirectoryInfo? oldDirectory,
|
private void OnModPathChanged( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
||||||
DirectoryInfo? newDirectory )
|
DirectoryInfo? newDirectory )
|
||||||
{
|
{
|
||||||
switch( type )
|
switch( type )
|
||||||
|
|
@ -221,10 +222,10 @@ public partial class ModCollection
|
||||||
OnModAddedActive( mod.TotalManipulations > 0 );
|
OnModAddedActive( mod.TotalManipulations > 0 );
|
||||||
break;
|
break;
|
||||||
case ModPathChangeType.Deleted:
|
case ModPathChangeType.Deleted:
|
||||||
var settings = new List< ModSettings2? >( _collections.Count );
|
var settings = new List< ModSettings? >( _collections.Count );
|
||||||
foreach( var collection in this )
|
foreach( var collection in this )
|
||||||
{
|
{
|
||||||
settings.Add( collection[ mod.Index ].Settings );
|
settings.Add( collection._settings[ mod.Index ] );
|
||||||
collection.RemoveMod( mod, mod.Index );
|
collection.RemoveMod( mod, mod.Index );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,26 +243,50 @@ public partial class ModCollection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automatically update all relevant collections when a mod is changed.
|
||||||
private void OnModOptionsChanged( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx )
|
// This means saving if options change in a way where the settings may change and the collection has settings for this mod.
|
||||||
|
// And also updating effective file and meta manipulation lists if necessary.
|
||||||
|
private void OnModOptionsChanged( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx )
|
||||||
{
|
{
|
||||||
if( type == ModOptionChangeType.DisplayChange )
|
var (handleChanges, recomputeList, withMeta) = type switch
|
||||||
{
|
{
|
||||||
return;
|
ModOptionChangeType.GroupRenamed => ( true, false, false ),
|
||||||
|
ModOptionChangeType.GroupAdded => ( true, false, false ),
|
||||||
|
ModOptionChangeType.GroupDeleted => ( true, true, true ),
|
||||||
|
ModOptionChangeType.GroupMoved => ( true, false, false ),
|
||||||
|
ModOptionChangeType.GroupTypeChanged => ( true, true, true ),
|
||||||
|
ModOptionChangeType.PriorityChanged => ( true, true, true ),
|
||||||
|
ModOptionChangeType.OptionAdded => ( true, true, true ),
|
||||||
|
ModOptionChangeType.OptionDeleted => ( true, true, true ),
|
||||||
|
ModOptionChangeType.OptionMoved => ( true, false, false ),
|
||||||
|
ModOptionChangeType.OptionFilesChanged => ( false, true, false ),
|
||||||
|
ModOptionChangeType.OptionSwapsChanged => ( false, true, false ),
|
||||||
|
ModOptionChangeType.OptionMetaChanged => ( false, true, true ),
|
||||||
|
ModOptionChangeType.OptionUpdated => ( false, true, true ),
|
||||||
|
ModOptionChangeType.DisplayChange => ( false, false, false ),
|
||||||
|
_ => ( false, false, false ),
|
||||||
|
};
|
||||||
|
|
||||||
|
if( handleChanges )
|
||||||
|
{
|
||||||
|
foreach( var collection in this )
|
||||||
|
{
|
||||||
|
if( collection._settings[ mod.Index ]?.HandleChanges( type, mod, groupIdx, optionIdx, movedToIdx ) ?? false )
|
||||||
|
{
|
||||||
|
collection.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
if( recomputeList )
|
||||||
switch( type )
|
|
||||||
{
|
{
|
||||||
case ModOptionChangeType.GroupRenamed:
|
foreach( var collection in this.Where( c => c.HasCache ) )
|
||||||
case ModOptionChangeType.GroupAdded:
|
{
|
||||||
case ModOptionChangeType.GroupDeleted:
|
if( collection[ mod.Index ].Settings is { Enabled: true } )
|
||||||
case ModOptionChangeType.PriorityChanged:
|
{
|
||||||
case ModOptionChangeType.OptionAdded:
|
collection.CalculateEffectiveFileList( withMeta, collection == Penumbra.CollectionManager.Default );
|
||||||
case ModOptionChangeType.OptionDeleted:
|
}
|
||||||
case ModOptionChangeType.OptionChanged:
|
}
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException( nameof( type ), type, null );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using OtterGui.Classes;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
|
|
||||||
|
|
@ -73,9 +73,16 @@ public struct ConflictCache
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all mod conflicts concerning the specified mod (in both directions).
|
// Find all mod conflicts concerning the specified mod (in both directions).
|
||||||
public IEnumerable< Conflict > ModConflicts( int modIdx )
|
public SubList< Conflict > ModConflicts( int modIdx )
|
||||||
{
|
{
|
||||||
return _conflicts.SkipWhile( c => c.Mod1 < modIdx ).TakeWhile( c => c.Mod1 == modIdx );
|
var start = _conflicts.FindIndex( c => c.Mod1 == modIdx );
|
||||||
|
if( start < 0 )
|
||||||
|
{
|
||||||
|
return SubList< Conflict >.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var end = _conflicts.FindIndex( start, c => c.Mod1 != modIdx );
|
||||||
|
return new SubList< Conflict >( _conflicts, start, end - start );
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Sort()
|
private void Sort()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
|
using OtterGui.Classes;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.Meta.Manager;
|
using Penumbra.Meta.Manager;
|
||||||
|
|
||||||
|
|
@ -64,8 +65,8 @@ public partial class ModCollection
|
||||||
internal IReadOnlyList< ConflictCache.Conflict > Conflicts
|
internal IReadOnlyList< ConflictCache.Conflict > Conflicts
|
||||||
=> _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >();
|
=> _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >();
|
||||||
|
|
||||||
internal IEnumerable< ConflictCache.Conflict > ModConflicts( int modIdx )
|
internal SubList< ConflictCache.Conflict > ModConflicts( int modIdx )
|
||||||
=> _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.Conflict >();
|
=> _cache?.Conflicts.ModConflicts( modIdx ) ?? SubList< ConflictCache.Conflict >.Empty;
|
||||||
|
|
||||||
// Update the effective file list for the given cache.
|
// Update the effective file list for the given cache.
|
||||||
// Creates a cache if necessary.
|
// Creates a cache if necessary.
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ public partial class ModCollection
|
||||||
// Shared caches to avoid allocations.
|
// Shared caches to avoid allocations.
|
||||||
private static readonly Dictionary< Utf8GamePath, FileRegister > RegisteredFiles = new(1024);
|
private static readonly Dictionary< Utf8GamePath, FileRegister > RegisteredFiles = new(1024);
|
||||||
private static readonly Dictionary< MetaManipulation, FileRegister > RegisteredManipulations = new(1024);
|
private static readonly Dictionary< MetaManipulation, FileRegister > RegisteredManipulations = new(1024);
|
||||||
private static readonly List< ModSettings2? > ResolvedSettings = new(128);
|
private static readonly List< ModSettings? > ResolvedSettings = new(128);
|
||||||
|
|
||||||
private readonly ModCollection _collection;
|
private readonly ModCollection _collection;
|
||||||
private readonly SortedList< string, object? > _changedItems = new();
|
private readonly SortedList< string, object? > _changedItems = new();
|
||||||
|
|
@ -225,7 +225,7 @@ public partial class ModCollection
|
||||||
foreach( var (path, file) in mod.Files.Concat( mod.FileSwaps ) )
|
foreach( var (path, file) in mod.Files.Concat( mod.FileSwaps ) )
|
||||||
{
|
{
|
||||||
// Skip all filtered files
|
// Skip all filtered files
|
||||||
if( Mod2.FilterFile( path ) )
|
if( Mod.FilterFile( path ) )
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -257,6 +257,11 @@ public partial class ModCollection
|
||||||
{
|
{
|
||||||
var config = settings.Settings[ idx ];
|
var config = settings.Settings[ idx ];
|
||||||
var group = mod.Groups[ idx ];
|
var group = mod.Groups[ idx ];
|
||||||
|
if( group.Count == 0 )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
switch( group.Type )
|
switch( group.Type )
|
||||||
{
|
{
|
||||||
case SelectType.Single:
|
case SelectType.Single:
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ public partial class ModCollection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable or disable the mod inheritance of every mod in mods.
|
// Enable or disable the mod inheritance of every mod in mods.
|
||||||
public void SetMultipleModInheritances( IEnumerable< Mod2 > mods, bool inherit )
|
public void SetMultipleModInheritances( IEnumerable< Mod > mods, bool inherit )
|
||||||
{
|
{
|
||||||
if( mods.Aggregate( false, ( current, mod ) => current | FixInheritance( mod.Index, inherit ) ) )
|
if( mods.Aggregate( false, ( current, mod ) => current | FixInheritance( mod.Index, inherit ) ) )
|
||||||
{
|
{
|
||||||
|
|
@ -56,7 +56,7 @@ public partial class ModCollection
|
||||||
|
|
||||||
// Set the enabled state of every mod in mods to the new value.
|
// Set the enabled state of every mod in mods to the new value.
|
||||||
// If the mod is currently inherited, stop the inheritance.
|
// If the mod is currently inherited, stop the inheritance.
|
||||||
public void SetMultipleModStates( IEnumerable< Mod2 > mods, bool newValue )
|
public void SetMultipleModStates( IEnumerable< Mod > mods, bool newValue )
|
||||||
{
|
{
|
||||||
var changes = false;
|
var changes = false;
|
||||||
foreach( var mod in mods )
|
foreach( var mod in mods )
|
||||||
|
|
@ -137,7 +137,7 @@ public partial class ModCollection
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_settings[ idx ] = inherit ? null : this[ idx ].Settings?.DeepCopy() ?? ModSettings2.DefaultSettings( Penumbra.ModManager.Mods[ idx ] );
|
_settings[ idx ] = inherit ? null : this[ idx ].Settings?.DeepCopy() ?? ModSettings.DefaultSettings( Penumbra.ModManager.Mods[ idx ] );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ using System.Text;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using OtterGui.Filesystem;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.Util;
|
|
||||||
|
|
||||||
namespace Penumbra.Collections;
|
namespace Penumbra.Collections;
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ public partial class ModCollection
|
||||||
if( settings != null )
|
if( settings != null )
|
||||||
{
|
{
|
||||||
j.WritePropertyName( Penumbra.ModManager[ i ].BasePath.Name );
|
j.WritePropertyName( Penumbra.ModManager[ i ].BasePath.Name );
|
||||||
x.Serialize( j, new ModSettings2.SavedSettings( settings, Penumbra.ModManager[ i ] ) );
|
x.Serialize( j, new ModSettings.SavedSettings( settings, Penumbra.ModManager[ i ] ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,8 +111,8 @@ public partial class ModCollection
|
||||||
var name = obj[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty;
|
var name = obj[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty;
|
||||||
var version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0;
|
var version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0;
|
||||||
// Custom deserialization that is converted with the constructor.
|
// Custom deserialization that is converted with the constructor.
|
||||||
var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings2.SavedSettings > >()
|
var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings.SavedSettings > >()
|
||||||
?? new Dictionary< string, ModSettings2.SavedSettings >();
|
?? new Dictionary< string, ModSettings.SavedSettings >();
|
||||||
inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >();
|
inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >();
|
||||||
|
|
||||||
return new ModCollection( name, version, settings );
|
return new ModCollection( name, version, settings );
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using OtterGui.Filesystem;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
|
|
||||||
|
|
@ -119,7 +120,7 @@ public partial class ModCollection
|
||||||
// Obtain the actual settings for a given mod via index.
|
// Obtain the actual settings for a given mod via index.
|
||||||
// Also returns the collection the settings are taken from.
|
// Also returns the collection the settings are taken from.
|
||||||
// If no collection provides settings for this mod, this collection is returned together with null.
|
// If no collection provides settings for this mod, this collection is returned together with null.
|
||||||
public (ModSettings2? Settings, ModCollection Collection) this[ Index idx ]
|
public (ModSettings? Settings, ModCollection Collection) this[ Index idx ]
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,13 @@ public sealed partial class ModCollection
|
||||||
}
|
}
|
||||||
|
|
||||||
// We treat every completely defaulted setting as inheritance-ready.
|
// We treat every completely defaulted setting as inheritance-ready.
|
||||||
private static bool SettingIsDefaultV0( ModSettings2.SavedSettings setting )
|
private static bool SettingIsDefaultV0( ModSettings.SavedSettings setting )
|
||||||
=> setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All( s => s == 0 );
|
=> setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All( s => s == 0 );
|
||||||
|
|
||||||
private static bool SettingIsDefaultV0( ModSettings2? setting )
|
private static bool SettingIsDefaultV0( ModSettings? setting )
|
||||||
=> setting is { Enabled: false, Priority: 0 } && setting.Settings.All( s => s == 0 );
|
=> setting is { Enabled: false, Priority: 0 } && setting.Settings.All( s => s == 0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings2.SavedSettings > allSettings )
|
internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings.SavedSettings > allSettings )
|
||||||
=> new(name, 0, allSettings);
|
=> new(name, 0, allSettings);
|
||||||
}
|
}
|
||||||
|
|
@ -27,17 +27,17 @@ public partial class ModCollection
|
||||||
|
|
||||||
// If a ModSetting is null, it can be inherited from other collections.
|
// If a ModSetting is null, it can be inherited from other collections.
|
||||||
// If no collection provides a setting for the mod, it is just disabled.
|
// If no collection provides a setting for the mod, it is just disabled.
|
||||||
private readonly List< ModSettings2? > _settings;
|
private readonly List< ModSettings? > _settings;
|
||||||
|
|
||||||
public IReadOnlyList< ModSettings2? > Settings
|
public IReadOnlyList< ModSettings? > Settings
|
||||||
=> _settings;
|
=> _settings;
|
||||||
|
|
||||||
// Evaluates the settings along the whole inheritance tree.
|
// Evaluates the settings along the whole inheritance tree.
|
||||||
public IEnumerable< ModSettings2? > ActualSettings
|
public IEnumerable< ModSettings? > ActualSettings
|
||||||
=> Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings );
|
=> Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings );
|
||||||
|
|
||||||
// Settings for deleted mods will be kept via directory name.
|
// Settings for deleted mods will be kept via directory name.
|
||||||
private readonly Dictionary< string, ModSettings2.SavedSettings > _unusedSettings;
|
private readonly Dictionary< string, ModSettings.SavedSettings > _unusedSettings;
|
||||||
|
|
||||||
// Constructor for duplication.
|
// Constructor for duplication.
|
||||||
private ModCollection( string name, ModCollection duplicate )
|
private ModCollection( string name, ModCollection duplicate )
|
||||||
|
|
@ -52,13 +52,13 @@ public partial class ModCollection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructor for reading from files.
|
// Constructor for reading from files.
|
||||||
private ModCollection( string name, int version, Dictionary< string, ModSettings2.SavedSettings > allSettings )
|
private ModCollection( string name, int version, Dictionary< string, ModSettings.SavedSettings > allSettings )
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
Version = version;
|
Version = version;
|
||||||
_unusedSettings = allSettings;
|
_unusedSettings = allSettings;
|
||||||
|
|
||||||
_settings = new List< ModSettings2? >();
|
_settings = new List< ModSettings? >();
|
||||||
ApplyModSettings();
|
ApplyModSettings();
|
||||||
|
|
||||||
Migration.Migrate( this );
|
Migration.Migrate( this );
|
||||||
|
|
@ -68,7 +68,7 @@ public partial class ModCollection
|
||||||
|
|
||||||
// Create a new, unique empty collection of a given name.
|
// Create a new, unique empty collection of a given name.
|
||||||
public static ModCollection CreateNewEmpty( string name )
|
public static ModCollection CreateNewEmpty( string name )
|
||||||
=> new(name, CurrentVersion, new Dictionary< string, ModSettings2.SavedSettings >());
|
=> new(name, CurrentVersion, new Dictionary< string, ModSettings.SavedSettings >());
|
||||||
|
|
||||||
// Duplicate the calling collection to a new, unique collection of a given name.
|
// Duplicate the calling collection to a new, unique collection of a given name.
|
||||||
public ModCollection Duplicate( string name )
|
public ModCollection Duplicate( string name )
|
||||||
|
|
@ -86,7 +86,7 @@ public partial class ModCollection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion.
|
// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion.
|
||||||
private bool AddMod( Mod2 mod )
|
private bool AddMod( Mod mod )
|
||||||
{
|
{
|
||||||
if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var save ) )
|
if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var save ) )
|
||||||
{
|
{
|
||||||
|
|
@ -101,12 +101,12 @@ public partial class ModCollection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move settings from the current mod list to the unused mod settings.
|
// Move settings from the current mod list to the unused mod settings.
|
||||||
private void RemoveMod( Mod2 mod, int idx )
|
private void RemoveMod( Mod mod, int idx )
|
||||||
{
|
{
|
||||||
var settings = _settings[ idx ];
|
var settings = _settings[ idx ];
|
||||||
if( settings != null )
|
if( settings != null )
|
||||||
{
|
{
|
||||||
_unusedSettings.Add( mod.BasePath.Name, new ModSettings2.SavedSettings( settings, mod ) );
|
_unusedSettings.Add( mod.BasePath.Name, new ModSettings.SavedSettings( settings, mod ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
_settings.RemoveAt( idx );
|
_settings.RemoveAt( idx );
|
||||||
|
|
@ -127,7 +127,7 @@ public partial class ModCollection
|
||||||
{
|
{
|
||||||
foreach( var (mod, setting) in Penumbra.ModManager.Zip( _settings ).Where( s => s.Second != null ) )
|
foreach( var (mod, setting) in Penumbra.ModManager.Zip( _settings ).Where( s => s.Second != null ) )
|
||||||
{
|
{
|
||||||
_unusedSettings[ mod.BasePath.Name ] = new ModSettings2.SavedSettings( setting!, mod );
|
_unusedSettings[ mod.BasePath.Name ] = new ModSettings.SavedSettings( setting!, mod );
|
||||||
}
|
}
|
||||||
|
|
||||||
_settings.Clear();
|
_settings.Clear();
|
||||||
|
|
|
||||||
9
Penumbra/Import/ImporterState.cs
Normal file
9
Penumbra/Import/ImporterState.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace Penumbra.Import;
|
||||||
|
|
||||||
|
public enum ImporterState
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
WritingPackToDisk,
|
||||||
|
ExtractingModFiles,
|
||||||
|
Done,
|
||||||
|
}
|
||||||
126
Penumbra/Import/MetaFileInfo.cs
Normal file
126
Penumbra/Import/MetaFileInfo.cs
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Util;
|
||||||
|
|
||||||
|
namespace Penumbra.Import;
|
||||||
|
|
||||||
|
// Obtain information what type of object is manipulated
|
||||||
|
// by the given .meta file from TexTools, using its name.
|
||||||
|
public class MetaFileInfo
|
||||||
|
{
|
||||||
|
private const string Pt = @"(?'PrimaryType'[a-z]*)"; // language=regex
|
||||||
|
private const string Pp = @"(?'PrimaryPrefix'[a-z])"; // language=regex
|
||||||
|
private const string Pi = @"(?'PrimaryId'\d{4})"; // language=regex
|
||||||
|
private const string Pir = @"\k'PrimaryId'"; // language=regex
|
||||||
|
private const string St = @"(?'SecondaryType'[a-z]*)"; // language=regex
|
||||||
|
private const string Sp = @"(?'SecondaryPrefix'[a-z])"; // language=regex
|
||||||
|
private const string Si = @"(?'SecondaryId'\d{4})"; // language=regex
|
||||||
|
private const string File = @"\k'PrimaryPrefix'\k'PrimaryId'(\k'SecondaryPrefix'\k'SecondaryId')?"; // language=regex
|
||||||
|
private const string Slot = @"(_(?'Slot'[a-z]{3}))?"; // language=regex
|
||||||
|
private const string Ext = @"\.meta";
|
||||||
|
|
||||||
|
// These are the valid regexes for .meta files that we are able to support at the moment.
|
||||||
|
private static readonly Regex HousingMeta = new($"bgcommon/hou/{Pt}/general/{Pi}/{Pir}{Ext}", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex CharaMeta = new($"chara/{Pt}/{Pp}{Pi}(/obj/{St}/{Sp}{Si})?/{File}{Slot}{Ext}", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public readonly ObjectType PrimaryType;
|
||||||
|
public readonly BodySlot SecondaryType;
|
||||||
|
public readonly ushort PrimaryId;
|
||||||
|
public readonly ushort SecondaryId;
|
||||||
|
public readonly EquipSlot EquipSlot = EquipSlot.Unknown;
|
||||||
|
public readonly CustomizationType CustomizationType = CustomizationType.Unknown;
|
||||||
|
|
||||||
|
private static bool ValidType( ObjectType type )
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
ObjectType.Accessory => true,
|
||||||
|
ObjectType.Character => true,
|
||||||
|
ObjectType.Equipment => true,
|
||||||
|
ObjectType.DemiHuman => true,
|
||||||
|
ObjectType.Housing => true,
|
||||||
|
ObjectType.Monster => true,
|
||||||
|
ObjectType.Weapon => true,
|
||||||
|
ObjectType.Icon => false,
|
||||||
|
ObjectType.Font => false,
|
||||||
|
ObjectType.Interface => false,
|
||||||
|
ObjectType.LoadingScreen => false,
|
||||||
|
ObjectType.Map => false,
|
||||||
|
ObjectType.Vfx => false,
|
||||||
|
ObjectType.Unknown => false,
|
||||||
|
ObjectType.World => false,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public MetaFileInfo( string fileName )
|
||||||
|
: this( new GamePath( fileName ) )
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public MetaFileInfo( GamePath fileName )
|
||||||
|
{
|
||||||
|
// Set the primary type from the gamePath start.
|
||||||
|
PrimaryType = GameData.GameData.GetGamePathParser().PathToObjectType( fileName );
|
||||||
|
PrimaryId = 0;
|
||||||
|
SecondaryType = BodySlot.Unknown;
|
||||||
|
SecondaryId = 0;
|
||||||
|
// Not all types of objects can have valid meta data manipulation.
|
||||||
|
if( !ValidType( PrimaryType ) )
|
||||||
|
{
|
||||||
|
PrimaryType = ObjectType.Unknown;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Housing files have a separate regex that just contains the primary id.
|
||||||
|
if( PrimaryType == ObjectType.Housing )
|
||||||
|
{
|
||||||
|
var housingMatch = HousingMeta.Match( fileName );
|
||||||
|
if( housingMatch.Success )
|
||||||
|
{
|
||||||
|
PrimaryId = ushort.Parse( housingMatch.Groups[ "PrimaryId" ].Value );
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-housing is in chara/.
|
||||||
|
var match = CharaMeta.Match( fileName );
|
||||||
|
if( !match.Success )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The primary ID has to be available for every object.
|
||||||
|
PrimaryId = ushort.Parse( match.Groups[ "PrimaryId" ].Value );
|
||||||
|
|
||||||
|
// Depending on slot, we can set equip slot or customization type.
|
||||||
|
if( match.Groups[ "Slot" ].Success )
|
||||||
|
{
|
||||||
|
switch( PrimaryType )
|
||||||
|
{
|
||||||
|
case ObjectType.Equipment:
|
||||||
|
case ObjectType.Accessory:
|
||||||
|
if( Names.SuffixToEquipSlot.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpSlot ) )
|
||||||
|
{
|
||||||
|
EquipSlot = tmpSlot;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case ObjectType.Character:
|
||||||
|
if( Names.SuffixToCustomizationType.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpCustom ) )
|
||||||
|
{
|
||||||
|
CustomizationType = tmpCustom;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secondary type and secondary id are for weapons and demihumans.
|
||||||
|
if( match.Groups[ "SecondaryType" ].Success
|
||||||
|
&& Names.StringToBodySlot.TryGetValue( match.Groups[ "SecondaryType" ].Value, out SecondaryType ) )
|
||||||
|
{
|
||||||
|
SecondaryId = ushort.Parse( match.Groups[ "SecondaryId" ].Value );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
Penumbra/Import/StreamDisposer.cs
Normal file
25
Penumbra/Import/StreamDisposer.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Penumbra.Util;
|
||||||
|
|
||||||
|
namespace Penumbra.Import;
|
||||||
|
|
||||||
|
// Create an automatically disposing SqPack stream.
|
||||||
|
public class StreamDisposer : PenumbraSqPackStream, IDisposable
|
||||||
|
{
|
||||||
|
private readonly FileStream _fileStream;
|
||||||
|
|
||||||
|
public StreamDisposer( FileStream stream )
|
||||||
|
: base( stream )
|
||||||
|
=> _fileStream = stream;
|
||||||
|
|
||||||
|
public new void Dispose()
|
||||||
|
{
|
||||||
|
var filePath = _fileStream.Name;
|
||||||
|
|
||||||
|
base.Dispose();
|
||||||
|
_fileStream.Dispose();
|
||||||
|
|
||||||
|
File.Delete( filePath );
|
||||||
|
}
|
||||||
|
}
|
||||||
154
Penumbra/Import/TexToolsImport.cs
Normal file
154
Penumbra/Import/TexToolsImport.cs
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dalamud.Logging;
|
||||||
|
using ICSharpCode.SharpZipLib.Zip;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Penumbra.Util;
|
||||||
|
using FileMode = System.IO.FileMode;
|
||||||
|
|
||||||
|
namespace Penumbra.Import;
|
||||||
|
|
||||||
|
public partial class TexToolsImporter
|
||||||
|
{
|
||||||
|
private const string TempFileName = "textools-import";
|
||||||
|
private static readonly JsonSerializerSettings JsonSettings = new() { NullValueHandling = NullValueHandling.Ignore };
|
||||||
|
|
||||||
|
private readonly DirectoryInfo _baseDirectory;
|
||||||
|
private readonly string _tmpFile;
|
||||||
|
|
||||||
|
private readonly IEnumerable< FileInfo > _modPackFiles;
|
||||||
|
private readonly int _modPackCount;
|
||||||
|
|
||||||
|
public ImporterState State { get; private set; }
|
||||||
|
public readonly List< (FileInfo File, DirectoryInfo? Mod, Exception? Error) > ExtractedMods;
|
||||||
|
|
||||||
|
public TexToolsImporter( DirectoryInfo baseDirectory, ICollection< FileInfo > files )
|
||||||
|
: this( baseDirectory, files.Count, files )
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles )
|
||||||
|
{
|
||||||
|
_baseDirectory = baseDirectory;
|
||||||
|
_tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName );
|
||||||
|
_modPackFiles = modPackFiles;
|
||||||
|
_modPackCount = count;
|
||||||
|
ExtractedMods = new List< (FileInfo, DirectoryInfo?, Exception?) >( count );
|
||||||
|
Task.Run( ImportFiles );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ImportFiles()
|
||||||
|
{
|
||||||
|
State = ImporterState.None;
|
||||||
|
_currentModPackIdx = 0;
|
||||||
|
foreach( var file in _modPackFiles )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var directory = VerifyVersionAndImport( file );
|
||||||
|
ExtractedMods.Add( ( file, directory, null ) );
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
ExtractedMods.Add( ( file, null, e ) );
|
||||||
|
_currentNumOptions = 0;
|
||||||
|
_currentOptionIdx = 0;
|
||||||
|
_currentFileIdx = 0;
|
||||||
|
_currentNumFiles = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
++_currentModPackIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
State = ImporterState.Done;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rudimentary analysis of a TTMP file by extension and version.
|
||||||
|
// Puts out warnings if extension does not correspond to data.
|
||||||
|
private DirectoryInfo VerifyVersionAndImport( FileInfo modPackFile )
|
||||||
|
{
|
||||||
|
using var zfs = modPackFile.OpenRead();
|
||||||
|
using var extractedModPack = new ZipFile( zfs );
|
||||||
|
|
||||||
|
var mpl = FindZipEntry( extractedModPack, "TTMPL.mpl" );
|
||||||
|
if( mpl == null )
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException( "ZIP does not contain a TTMPL.mpl file." );
|
||||||
|
}
|
||||||
|
|
||||||
|
var modRaw = GetStringFromZipEntry( extractedModPack, mpl, Encoding.UTF8 );
|
||||||
|
|
||||||
|
// At least a better validation than going by the extension.
|
||||||
|
if( modRaw.Contains( "\"TTMPVersion\":" ) )
|
||||||
|
{
|
||||||
|
if( modPackFile.Extension != ".ttmp2" )
|
||||||
|
{
|
||||||
|
PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension." );
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImportV2ModPack( _: modPackFile, extractedModPack, modRaw );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( modPackFile.Extension != ".ttmp" )
|
||||||
|
{
|
||||||
|
PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension." );
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImportV1ModPack( modPackFile, extractedModPack, modRaw );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// You can in no way rely on any file paths in TTMPs so we need to just do this, sorry
|
||||||
|
private static ZipEntry? FindZipEntry( ZipFile file, string fileName )
|
||||||
|
{
|
||||||
|
for( var i = 0; i < file.Count; i++ )
|
||||||
|
{
|
||||||
|
var entry = file[ i ];
|
||||||
|
|
||||||
|
if( entry.Name.Contains( fileName ) )
|
||||||
|
{
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream GetStreamFromZipEntry( ZipFile file, ZipEntry entry )
|
||||||
|
=> file.GetInputStream( entry );
|
||||||
|
|
||||||
|
private static string GetStringFromZipEntry( ZipFile file, ZipEntry entry, Encoding encoding )
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
using var s = GetStreamFromZipEntry( file, entry );
|
||||||
|
s.CopyTo( ms );
|
||||||
|
return encoding.GetString( ms.ToArray() );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteZipEntryToTempFile( Stream s )
|
||||||
|
{
|
||||||
|
using var fs = new FileStream( _tmpFile, FileMode.Create );
|
||||||
|
s.CopyTo( fs );
|
||||||
|
}
|
||||||
|
|
||||||
|
private PenumbraSqPackStream GetSqPackStreamStream( ZipFile file, string entryName )
|
||||||
|
{
|
||||||
|
State = ImporterState.WritingPackToDisk;
|
||||||
|
|
||||||
|
// write shitty zip garbage to disk
|
||||||
|
var entry = FindZipEntry( file, entryName );
|
||||||
|
if( entry == null )
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException( $"ZIP does not contain a file named {entryName}." );
|
||||||
|
}
|
||||||
|
|
||||||
|
using var s = file.GetInputStream( entry );
|
||||||
|
|
||||||
|
WriteZipEntryToTempFile( s );
|
||||||
|
|
||||||
|
var fs = new FileStream( _tmpFile, FileMode.Open );
|
||||||
|
return new StreamDisposer( fs );
|
||||||
|
}
|
||||||
|
}
|
||||||
94
Penumbra/Import/TexToolsImporter.Gui.cs
Normal file
94
Penumbra/Import/TexToolsImporter.Gui.cs
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using ImGuiNET;
|
||||||
|
using OtterGui;
|
||||||
|
using OtterGui.Raii;
|
||||||
|
using Penumbra.UI.Classes;
|
||||||
|
|
||||||
|
namespace Penumbra.Import;
|
||||||
|
|
||||||
|
public partial class TexToolsImporter
|
||||||
|
{
|
||||||
|
// Progress Data
|
||||||
|
private int _currentModPackIdx;
|
||||||
|
private int _currentOptionIdx;
|
||||||
|
private int _currentFileIdx;
|
||||||
|
|
||||||
|
private int _currentNumOptions;
|
||||||
|
private int _currentNumFiles;
|
||||||
|
private string _currentModName = string.Empty;
|
||||||
|
private string _currentGroupName = string.Empty;
|
||||||
|
private string _currentOptionName = string.Empty;
|
||||||
|
private string _currentFileName = string.Empty;
|
||||||
|
|
||||||
|
|
||||||
|
public void DrawProgressInfo( Vector2 size )
|
||||||
|
{
|
||||||
|
if( _modPackCount == 0 )
|
||||||
|
{
|
||||||
|
ImGuiUtil.Center( "Nothing to extract." );
|
||||||
|
}
|
||||||
|
else if( _modPackCount == _currentModPackIdx )
|
||||||
|
{
|
||||||
|
DrawEndState();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.NewLine();
|
||||||
|
var percentage = _modPackCount / ( float )_currentModPackIdx;
|
||||||
|
ImGui.ProgressBar( percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}" );
|
||||||
|
ImGui.NewLine();
|
||||||
|
ImGui.Text( $"Extracting {_currentModName}..." );
|
||||||
|
|
||||||
|
if( _currentNumOptions > 1 )
|
||||||
|
{
|
||||||
|
ImGui.NewLine();
|
||||||
|
ImGui.NewLine();
|
||||||
|
percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / ( float )_currentNumOptions;
|
||||||
|
ImGui.ProgressBar( percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}" );
|
||||||
|
ImGui.NewLine();
|
||||||
|
ImGui.Text(
|
||||||
|
$"Extracting option {( _currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - " )}{_currentOptionName}..." );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.NewLine();
|
||||||
|
ImGui.NewLine();
|
||||||
|
percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / ( float )_currentNumFiles;
|
||||||
|
ImGui.ProgressBar( percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}" );
|
||||||
|
ImGui.NewLine();
|
||||||
|
ImGui.Text( $"Extracting file {_currentFileName}..." );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void DrawEndState()
|
||||||
|
{
|
||||||
|
var success = ExtractedMods.Count( t => t.Mod != null );
|
||||||
|
|
||||||
|
ImGui.Text( $"Successfully extracted {success} / {ExtractedMods.Count} files." );
|
||||||
|
ImGui.NewLine();
|
||||||
|
using var table = ImRaii.Table( "##files", 2 );
|
||||||
|
if( !table )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach( var (file, dir, ex) in ExtractedMods )
|
||||||
|
{
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text( file.Name );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
if( dir != null )
|
||||||
|
{
|
||||||
|
using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value() );
|
||||||
|
ImGui.Text( dir.FullName[ ( _baseDirectory.FullName.Length + 1 ).. ] );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ConflictingMod.Value() );
|
||||||
|
ImGui.Text( ex!.Message );
|
||||||
|
ImGuiUtil.HoverTooltip( ex.ToString() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
235
Penumbra/Import/TexToolsImporter.ModPack.cs
Normal file
235
Penumbra/Import/TexToolsImporter.ModPack.cs
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Dalamud.Logging;
|
||||||
|
using ICSharpCode.SharpZipLib.Zip;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Penumbra.Mods;
|
||||||
|
using Penumbra.Util;
|
||||||
|
|
||||||
|
namespace Penumbra.Import;
|
||||||
|
|
||||||
|
public partial class TexToolsImporter
|
||||||
|
{
|
||||||
|
// Version 1 mod packs are a simple collection of files without much information.
|
||||||
|
private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw )
|
||||||
|
{
|
||||||
|
_currentOptionIdx = 0;
|
||||||
|
_currentNumOptions = 1;
|
||||||
|
_currentModName = modPackFile.Name.Length > 0 ? modPackFile.Name : DefaultTexToolsData.Name;
|
||||||
|
_currentGroupName = string.Empty;
|
||||||
|
_currentOptionName = DefaultTexToolsData.DefaultOption;
|
||||||
|
|
||||||
|
PluginLog.Log( " -> Importing V1 ModPack" );
|
||||||
|
|
||||||
|
var modListRaw = modRaw.Split(
|
||||||
|
new[] { "\r\n", "\r", "\n" },
|
||||||
|
StringSplitOptions.RemoveEmptyEntries
|
||||||
|
);
|
||||||
|
|
||||||
|
var modList = modListRaw.Select( m => JsonConvert.DeserializeObject< SimpleMod >( m, JsonSettings )! ).ToList();
|
||||||
|
|
||||||
|
// Open the mod data file from the mod pack as a SqPackStream
|
||||||
|
using var modData = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
|
||||||
|
|
||||||
|
var ret = Mod.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) );
|
||||||
|
// Create a new ModMeta from the TTMP mod list info
|
||||||
|
Mod.CreateMeta( ret, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null );
|
||||||
|
|
||||||
|
ExtractSimpleModList( ret, modList, modData );
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version 2 mod packs can either be simple or extended, import accordingly.
|
||||||
|
private DirectoryInfo ImportV2ModPack( FileInfo _, ZipFile extractedModPack, string modRaw )
|
||||||
|
{
|
||||||
|
var modList = JsonConvert.DeserializeObject< SimpleModPack >( modRaw, JsonSettings )!;
|
||||||
|
|
||||||
|
if( modList.TtmpVersion.EndsWith( "s" ) )
|
||||||
|
{
|
||||||
|
return ImportSimpleV2ModPack( extractedModPack, modList );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( modList.TtmpVersion.EndsWith( "w" ) )
|
||||||
|
{
|
||||||
|
return ImportExtendedV2ModPack( extractedModPack, modRaw );
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PluginLog.Warning( $"Unknown TTMPVersion <{modList.TtmpVersion}> given, trying to export as simple mod pack." );
|
||||||
|
return ImportSimpleV2ModPack( extractedModPack, modList );
|
||||||
|
}
|
||||||
|
catch( Exception e1 )
|
||||||
|
{
|
||||||
|
PluginLog.Warning( $"Exporting as simple mod pack failed with following error, retrying as extended mod pack:\n{e1}" );
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return ImportExtendedV2ModPack( extractedModPack, modRaw );
|
||||||
|
}
|
||||||
|
catch( Exception e2 )
|
||||||
|
{
|
||||||
|
throw new IOException( "Exporting as extended mod pack failed, too. Version unsupported or file defect.", e2 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple V2 mod packs are basically the same as V1 mod packs.
|
||||||
|
private DirectoryInfo ImportSimpleV2ModPack( ZipFile extractedModPack, SimpleModPack modList )
|
||||||
|
{
|
||||||
|
_currentOptionIdx = 0;
|
||||||
|
_currentNumOptions = 1;
|
||||||
|
_currentModName = modList.Name;
|
||||||
|
_currentGroupName = string.Empty;
|
||||||
|
_currentOptionName = DefaultTexToolsData.DefaultOption;
|
||||||
|
PluginLog.Log( " -> Importing Simple V2 ModPack" );
|
||||||
|
|
||||||
|
// Open the mod data file from the mod pack as a SqPackStream
|
||||||
|
using var modData = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
|
||||||
|
|
||||||
|
var ret = Mod.CreateModFolder( _baseDirectory, _currentModName );
|
||||||
|
Mod.CreateMeta( ret, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description )
|
||||||
|
? "Mod imported from TexTools mod pack"
|
||||||
|
: modList.Description, null, null );
|
||||||
|
|
||||||
|
ExtractSimpleModList( ret, modList.SimpleModsList, modData );
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain the number of relevant options to extract.
|
||||||
|
private static int GetOptionCount( ExtendedModPack pack )
|
||||||
|
=> ( pack.SimpleModsList.Length > 0 ? 1 : 0 )
|
||||||
|
+ pack.ModPackPages
|
||||||
|
.Sum( page => page.ModGroups
|
||||||
|
.Where( g => g.GroupName.Length > 0 && g.OptionList.Length > 0 )
|
||||||
|
.Sum( group => group.OptionList
|
||||||
|
.Count( o => o.Name.Length > 0 && o.ModsJsons.Length > 0 ) ) );
|
||||||
|
|
||||||
|
// Extended V2 mod packs contain multiple options that need to be handled separately.
|
||||||
|
private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw )
|
||||||
|
{
|
||||||
|
_currentOptionIdx = 0;
|
||||||
|
PluginLog.Log( " -> Importing Extended V2 ModPack" );
|
||||||
|
|
||||||
|
var modList = JsonConvert.DeserializeObject< ExtendedModPack >( modRaw, JsonSettings )!;
|
||||||
|
_currentNumOptions = GetOptionCount( modList );
|
||||||
|
_currentModName = modList.Name;
|
||||||
|
// Open the mod data file from the mod pack as a SqPackStream
|
||||||
|
using var modData = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
|
||||||
|
|
||||||
|
var ret = Mod.CreateModFolder( _baseDirectory, _currentModName );
|
||||||
|
Mod.CreateMeta( ret, _currentModName, modList.Author, modList.Description, modList.Version, null );
|
||||||
|
|
||||||
|
if( _currentNumOptions == 0 )
|
||||||
|
{
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// It can contain a simple list, still.
|
||||||
|
if( modList.SimpleModsList.Length > 0 )
|
||||||
|
{
|
||||||
|
_currentGroupName = string.Empty;
|
||||||
|
_currentOptionName = "Default";
|
||||||
|
ExtractSimpleModList( ret, modList.SimpleModsList, modData );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through all pages
|
||||||
|
var options = new List< ISubMod >();
|
||||||
|
var groupPriority = 0;
|
||||||
|
foreach( var page in modList.ModPackPages )
|
||||||
|
{
|
||||||
|
foreach( var group in page.ModGroups.Where( group => group.GroupName.Length > 0 && group.OptionList.Length > 0 ) )
|
||||||
|
{
|
||||||
|
_currentGroupName = group.GroupName;
|
||||||
|
options.Clear();
|
||||||
|
var description = new StringBuilder();
|
||||||
|
var groupFolder = Mod.NewSubFolderName( ret, group.GroupName )
|
||||||
|
?? new DirectoryInfo( Path.Combine( ret.FullName, $"Group {groupPriority + 1}" ) );
|
||||||
|
|
||||||
|
var optionIdx = 1;
|
||||||
|
|
||||||
|
foreach( var option in group.OptionList.Where( option => option.Name.Length > 0 && option.ModsJsons.Length > 0 ) )
|
||||||
|
{
|
||||||
|
_currentOptionName = option.Name;
|
||||||
|
var optionFolder = Mod.NewSubFolderName( groupFolder, option.Name )
|
||||||
|
?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {optionIdx}" ) );
|
||||||
|
ExtractSimpleModList( optionFolder, option.ModsJsons, modData );
|
||||||
|
options.Add( Mod.CreateSubMod( ret, optionFolder, option ) );
|
||||||
|
description.Append( option.Description );
|
||||||
|
if( !string.IsNullOrEmpty( option.Description ) )
|
||||||
|
{
|
||||||
|
description.Append( '\n' );
|
||||||
|
}
|
||||||
|
|
||||||
|
++optionIdx;
|
||||||
|
++_currentOptionIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mod.CreateOptionGroup( ret, group, groupPriority++, description.ToString(), options );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mod.CreateDefaultFiles( ret );
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExtractSimpleModList( DirectoryInfo outDirectory, ICollection< SimpleMod > mods, PenumbraSqPackStream dataStream )
|
||||||
|
{
|
||||||
|
State = ImporterState.ExtractingModFiles;
|
||||||
|
|
||||||
|
_currentFileIdx = 0;
|
||||||
|
_currentNumFiles = mods.Count;
|
||||||
|
|
||||||
|
// Extract each SimpleMod into the new mod folder
|
||||||
|
foreach( var simpleMod in mods )
|
||||||
|
{
|
||||||
|
ExtractMod( outDirectory, simpleMod, dataStream );
|
||||||
|
++_currentFileIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExtractMod( DirectoryInfo outDirectory, SimpleMod mod, PenumbraSqPackStream dataStream )
|
||||||
|
{
|
||||||
|
PluginLog.Log( " -> Extracting {0} at {1}", mod.FullPath, mod.ModOffset.ToString( "X" ) );
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = dataStream.ReadFile< PenumbraSqPackStream.PenumbraFileResource >( mod.ModOffset );
|
||||||
|
|
||||||
|
_currentFileName = mod.FullPath;
|
||||||
|
var extractedFile = new FileInfo( Path.Combine( outDirectory.FullName, mod.FullPath ) );
|
||||||
|
|
||||||
|
extractedFile.Directory?.Create();
|
||||||
|
|
||||||
|
if( extractedFile.FullName.EndsWith( ".mdl" ) )
|
||||||
|
{
|
||||||
|
ProcessMdl( data.Data );
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllBytes( extractedFile.FullName, data.Data );
|
||||||
|
}
|
||||||
|
catch( Exception ex )
|
||||||
|
{
|
||||||
|
PluginLog.LogError( ex, "Could not extract mod." );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ProcessMdl( byte[] mdl )
|
||||||
|
{
|
||||||
|
const int modelHeaderLodOffset = 22;
|
||||||
|
|
||||||
|
// Model file header LOD num
|
||||||
|
mdl[ 64 ] = 1;
|
||||||
|
|
||||||
|
// Model header LOD num
|
||||||
|
var stackSize = BitConverter.ToUInt32( mdl, 4 );
|
||||||
|
var runtimeBegin = stackSize + 0x44;
|
||||||
|
var stringsLengthOffset = runtimeBegin + 4;
|
||||||
|
var stringsLength = BitConverter.ToUInt32( mdl, ( int )stringsLengthOffset );
|
||||||
|
var modelHeaderStart = stringsLengthOffset + stringsLength + 4;
|
||||||
|
mdl[ modelHeaderStart + modelHeaderLodOffset ] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
174
Penumbra/Import/TexToolsMeta.Deserialization.cs
Normal file
174
Penumbra/Import/TexToolsMeta.Deserialization.cs
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Dalamud.Logging;
|
||||||
|
using Lumina.Extensions;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
using Penumbra.Meta.Files;
|
||||||
|
using Penumbra.Meta.Manipulations;
|
||||||
|
|
||||||
|
namespace Penumbra.Import;
|
||||||
|
|
||||||
|
public partial class TexToolsMeta
|
||||||
|
{
|
||||||
|
// Deserialize and check Eqp Entries and add them to the list if they are non-default.
|
||||||
|
private void DeserializeEqpEntry( MetaFileInfo metaFileInfo, byte[]? data )
|
||||||
|
{
|
||||||
|
// Eqp can only be valid for equipment.
|
||||||
|
if( data == null || !metaFileInfo.EquipSlot.IsEquipment() )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = Eqp.FromSlotAndBytes( metaFileInfo.EquipSlot, data );
|
||||||
|
var def = new EqpManipulation( ExpandedEqpFile.GetDefault( metaFileInfo.PrimaryId ), metaFileInfo.EquipSlot, metaFileInfo.PrimaryId );
|
||||||
|
var manip = new EqpManipulation( value, metaFileInfo.EquipSlot, metaFileInfo.PrimaryId );
|
||||||
|
if( def.Entry != manip.Entry )
|
||||||
|
{
|
||||||
|
MetaManipulations.Add( manip );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize and check Eqdp Entries and add them to the list if they are non-default.
|
||||||
|
private void DeserializeEqdpEntries( MetaFileInfo metaFileInfo, byte[]? data )
|
||||||
|
{
|
||||||
|
if( data == null )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var num = data.Length / 5;
|
||||||
|
using var reader = new BinaryReader( new MemoryStream( data ) );
|
||||||
|
for( var i = 0; i < num; ++i )
|
||||||
|
{
|
||||||
|
// Use the SE gender/race code.
|
||||||
|
var gr = ( GenderRace )reader.ReadUInt32();
|
||||||
|
var byteValue = reader.ReadByte();
|
||||||
|
if( !gr.IsValid() || !metaFileInfo.EquipSlot.IsEquipment() && !metaFileInfo.EquipSlot.IsAccessory() )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = Eqdp.FromSlotAndBits( metaFileInfo.EquipSlot, ( byteValue & 1 ) == 1, ( byteValue & 2 ) == 2 );
|
||||||
|
var def = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId ),
|
||||||
|
metaFileInfo.EquipSlot,
|
||||||
|
gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId );
|
||||||
|
var manip = new EqdpManipulation( value, metaFileInfo.EquipSlot, gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId );
|
||||||
|
if( def.Entry != manip.Entry )
|
||||||
|
{
|
||||||
|
MetaManipulations.Add( manip );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize and check Gmp Entries and add them to the list if they are non-default.
|
||||||
|
private void DeserializeGmpEntry( MetaFileInfo metaFileInfo, byte[]? data )
|
||||||
|
{
|
||||||
|
if( data == null )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var reader = new BinaryReader( new MemoryStream( data ) );
|
||||||
|
var value = ( GmpEntry )reader.ReadUInt32();
|
||||||
|
value.UnknownTotal = reader.ReadByte();
|
||||||
|
var def = ExpandedGmpFile.GetDefault( metaFileInfo.PrimaryId );
|
||||||
|
if( value != def )
|
||||||
|
{
|
||||||
|
MetaManipulations.Add( new GmpManipulation( value, metaFileInfo.PrimaryId ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize and check Est Entries and add them to the list if they are non-default.
|
||||||
|
private void DeserializeEstEntries( MetaFileInfo metaFileInfo, byte[]? data )
|
||||||
|
{
|
||||||
|
if( data == null )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var num = data.Length / 6;
|
||||||
|
using var reader = new BinaryReader( new MemoryStream( data ) );
|
||||||
|
for( var i = 0; i < num; ++i )
|
||||||
|
{
|
||||||
|
var gr = ( GenderRace )reader.ReadUInt16();
|
||||||
|
var id = reader.ReadUInt16();
|
||||||
|
var value = reader.ReadUInt16();
|
||||||
|
var type = ( metaFileInfo.SecondaryType, metaFileInfo.EquipSlot ) switch
|
||||||
|
{
|
||||||
|
(BodySlot.Face, _) => EstManipulation.EstType.Face,
|
||||||
|
(BodySlot.Hair, _) => EstManipulation.EstType.Hair,
|
||||||
|
(_, EquipSlot.Head) => EstManipulation.EstType.Head,
|
||||||
|
(_, EquipSlot.Body) => EstManipulation.EstType.Body,
|
||||||
|
_ => ( EstManipulation.EstType )0,
|
||||||
|
};
|
||||||
|
if( !gr.IsValid() || type == 0 )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var def = EstFile.GetDefault( type, gr, id );
|
||||||
|
if( def != value )
|
||||||
|
{
|
||||||
|
MetaManipulations.Add( new EstManipulation( gr.Split().Item1, gr.Split().Item2, type, id, value ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize and check IMC Entries and add them to the list if they are non-default.
|
||||||
|
// This requires requesting a file from Lumina, which may fail due to TexTools corruption or just not existing.
|
||||||
|
// TexTools creates IMC files for off-hand weapon models which may not exist in the game files.
|
||||||
|
private void DeserializeImcEntries( MetaFileInfo metaFileInfo, byte[]? data )
|
||||||
|
{
|
||||||
|
if( data == null )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var num = data.Length / 6;
|
||||||
|
using var reader = new BinaryReader( new MemoryStream( data ) );
|
||||||
|
var values = reader.ReadStructures< ImcEntry >( num );
|
||||||
|
ushort i = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if( metaFileInfo.PrimaryType is ObjectType.Equipment or ObjectType.Accessory )
|
||||||
|
{
|
||||||
|
var def = new ImcFile( new ImcManipulation( metaFileInfo.EquipSlot, i, metaFileInfo.PrimaryId, new ImcEntry() ).GamePath() );
|
||||||
|
var partIdx = ImcFile.PartIndex( metaFileInfo.EquipSlot );
|
||||||
|
foreach( var value in values )
|
||||||
|
{
|
||||||
|
if( !value.Equals( def.GetEntry( partIdx, i ) ) )
|
||||||
|
{
|
||||||
|
MetaManipulations.Add( new ImcManipulation( metaFileInfo.EquipSlot, i, metaFileInfo.PrimaryId, value ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var def = new ImcFile( new ImcManipulation( metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId,
|
||||||
|
metaFileInfo.SecondaryId, i,
|
||||||
|
new ImcEntry() ).GamePath() );
|
||||||
|
foreach( var value in values )
|
||||||
|
{
|
||||||
|
if( !value.Equals( def.GetEntry( 0, i ) ) )
|
||||||
|
{
|
||||||
|
MetaManipulations.Add( new ImcManipulation( metaFileInfo.PrimaryType, metaFileInfo.SecondaryType,
|
||||||
|
metaFileInfo.PrimaryId,
|
||||||
|
metaFileInfo.SecondaryId, i,
|
||||||
|
value ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
PluginLog.Warning(
|
||||||
|
$"Could not compute IMC manipulation for {metaFileInfo.PrimaryType} {metaFileInfo.PrimaryId}. This is in all likelihood due to TexTools corrupting your index files.\n"
|
||||||
|
+ $"If the following error looks like Lumina is having trouble to read an IMC file, please do a do-over in TexTools:\n{e}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
Penumbra/Import/TexToolsMeta.Rgsp.cs
Normal file
81
Penumbra/Import/TexToolsMeta.Rgsp.cs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Dalamud.Logging;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.Meta.Files;
|
||||||
|
using Penumbra.Meta.Manipulations;
|
||||||
|
|
||||||
|
namespace Penumbra.Import;
|
||||||
|
|
||||||
|
public partial class TexToolsMeta
|
||||||
|
{
|
||||||
|
// Parse a single rgsp file.
|
||||||
|
public static TexToolsMeta FromRgspFile( string filePath, byte[] data )
|
||||||
|
{
|
||||||
|
if( data.Length != 45 && data.Length != 42 )
|
||||||
|
{
|
||||||
|
PluginLog.Error( "Error while parsing .rgsp file:\n\tInvalid number of bytes." );
|
||||||
|
return Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var s = new MemoryStream( data );
|
||||||
|
using var br = new BinaryReader( s );
|
||||||
|
// The first value is a flag that signifies version.
|
||||||
|
// If it is byte.max, the following two bytes are the version,
|
||||||
|
// otherwise it is version 1 and signifies the sub race instead.
|
||||||
|
var flag = br.ReadByte();
|
||||||
|
var version = flag != 255 ? ( uint )1 : br.ReadUInt16();
|
||||||
|
|
||||||
|
var ret = new TexToolsMeta( filePath, version );
|
||||||
|
|
||||||
|
// SubRace is offset by one due to Unknown.
|
||||||
|
var subRace = ( SubRace )( version == 1 ? flag + 1 : br.ReadByte() + 1 );
|
||||||
|
if( !Enum.IsDefined( typeof( SubRace ), subRace ) || subRace == SubRace.Unknown )
|
||||||
|
{
|
||||||
|
PluginLog.Error( $"Error while parsing .rgsp file:\n\t{subRace} is not a valid SubRace." );
|
||||||
|
return Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next byte is Gender. 1 is Female, 0 is Male.
|
||||||
|
var gender = br.ReadByte();
|
||||||
|
if( gender != 1 && gender != 0 )
|
||||||
|
{
|
||||||
|
PluginLog.Error( $"Error while parsing .rgsp file:\n\t{gender} is neither Male nor Female." );
|
||||||
|
return Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the given values to the manipulations if they are not default.
|
||||||
|
void Add( RspAttribute attribute, float value )
|
||||||
|
{
|
||||||
|
var def = CmpFile.GetDefault( subRace, attribute );
|
||||||
|
if( value != def )
|
||||||
|
{
|
||||||
|
ret.MetaManipulations.Add( new RspManipulation( subRace, attribute, value ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( gender == 1 )
|
||||||
|
{
|
||||||
|
Add( RspAttribute.FemaleMinSize, br.ReadSingle() );
|
||||||
|
Add( RspAttribute.FemaleMaxSize, br.ReadSingle() );
|
||||||
|
Add( RspAttribute.FemaleMinTail, br.ReadSingle() );
|
||||||
|
Add( RspAttribute.FemaleMaxTail, br.ReadSingle() );
|
||||||
|
|
||||||
|
Add( RspAttribute.BustMinX, br.ReadSingle() );
|
||||||
|
Add( RspAttribute.BustMinY, br.ReadSingle() );
|
||||||
|
Add( RspAttribute.BustMinZ, br.ReadSingle() );
|
||||||
|
Add( RspAttribute.BustMaxX, br.ReadSingle() );
|
||||||
|
Add( RspAttribute.BustMaxY, br.ReadSingle() );
|
||||||
|
Add( RspAttribute.BustMaxZ, br.ReadSingle() );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Add( RspAttribute.MaleMinSize, br.ReadSingle() );
|
||||||
|
Add( RspAttribute.MaleMaxSize, br.ReadSingle() );
|
||||||
|
Add( RspAttribute.MaleMinTail, br.ReadSingle() );
|
||||||
|
Add( RspAttribute.MaleMaxTail, br.ReadSingle() );
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
96
Penumbra/Import/TexToolsMeta.cs
Normal file
96
Penumbra/Import/TexToolsMeta.cs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using Dalamud.Logging;
|
||||||
|
using Penumbra.Meta.Manipulations;
|
||||||
|
|
||||||
|
namespace Penumbra.Import;
|
||||||
|
|
||||||
|
// TexTools provices custom generated *.meta files for its modpacks, that contain changes to
|
||||||
|
// - imc files
|
||||||
|
// - eqp files
|
||||||
|
// - gmp files
|
||||||
|
// - est files
|
||||||
|
// - eqdp files
|
||||||
|
// made by the mod. The filename determines to what the changes are applied, and the binary file itself contains changes.
|
||||||
|
// We parse every *.meta file in a mod and combine all actual changes that do not keep data on default values and that can be applied to the game in a .json.
|
||||||
|
// TexTools may also generate files that contain non-existing changes, e.g. *.imc files for weapon offhands, which will be ignored.
|
||||||
|
// TexTools also provides .rgsp files, that contain changes to the racial scaling parameters in the human.cmp file.
|
||||||
|
public partial class TexToolsMeta
|
||||||
|
{
|
||||||
|
// An empty TexToolsMeta.
|
||||||
|
public static readonly TexToolsMeta Invalid = new( string.Empty, 0 );
|
||||||
|
|
||||||
|
// The info class determines the files or table locations the changes need to apply to from the filename.
|
||||||
|
|
||||||
|
public readonly uint Version;
|
||||||
|
public readonly string FilePath;
|
||||||
|
public readonly List< MetaManipulation > MetaManipulations = new();
|
||||||
|
|
||||||
|
public TexToolsMeta( byte[] data )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var reader = new BinaryReader( new MemoryStream( data ) );
|
||||||
|
Version = reader.ReadUInt32();
|
||||||
|
FilePath = ReadNullTerminated( reader );
|
||||||
|
var metaInfo = new MetaFileInfo( FilePath );
|
||||||
|
var numHeaders = reader.ReadUInt32();
|
||||||
|
var headerSize = reader.ReadUInt32();
|
||||||
|
var headerStart = reader.ReadUInt32();
|
||||||
|
reader.BaseStream.Seek( headerStart, SeekOrigin.Begin );
|
||||||
|
|
||||||
|
List< (MetaManipulation.Type type, uint offset, int size) > entries = new();
|
||||||
|
for( var i = 0; i < numHeaders; ++i )
|
||||||
|
{
|
||||||
|
var currentOffset = reader.BaseStream.Position;
|
||||||
|
var type = ( MetaManipulation.Type )reader.ReadUInt32();
|
||||||
|
var offset = reader.ReadUInt32();
|
||||||
|
var size = reader.ReadInt32();
|
||||||
|
entries.Add( ( type, offset, size ) );
|
||||||
|
reader.BaseStream.Seek( currentOffset + headerSize, SeekOrigin.Begin );
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[]? ReadEntry( MetaManipulation.Type type )
|
||||||
|
{
|
||||||
|
var idx = entries.FindIndex( t => t.type == type );
|
||||||
|
if( idx < 0 )
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.BaseStream.Seek( entries[ idx ].offset, SeekOrigin.Begin );
|
||||||
|
return reader.ReadBytes( entries[ idx ].size );
|
||||||
|
}
|
||||||
|
|
||||||
|
DeserializeEqpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Eqp ) );
|
||||||
|
DeserializeGmpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Gmp ) );
|
||||||
|
DeserializeEqdpEntries( metaInfo, ReadEntry( MetaManipulation.Type.Eqdp ) );
|
||||||
|
DeserializeEstEntries( metaInfo, ReadEntry( MetaManipulation.Type.Est ) );
|
||||||
|
DeserializeImcEntries( metaInfo, ReadEntry( MetaManipulation.Type.Imc ) );
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
FilePath = "";
|
||||||
|
PluginLog.Error( $"Error while parsing .meta file:\n{e}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TexToolsMeta( string filePath, uint version )
|
||||||
|
{
|
||||||
|
FilePath = filePath;
|
||||||
|
Version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a null terminated string from a binary reader.
|
||||||
|
private static string ReadNullTerminated( BinaryReader reader )
|
||||||
|
{
|
||||||
|
var builder = new System.Text.StringBuilder();
|
||||||
|
for( var c = reader.ReadChar(); c != 0; c = reader.ReadChar() )
|
||||||
|
{
|
||||||
|
builder.Append( c );
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
74
Penumbra/Import/TexToolsStructs.cs
Normal file
74
Penumbra/Import/TexToolsStructs.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
using System;
|
||||||
|
using Penumbra.Mods;
|
||||||
|
|
||||||
|
namespace Penumbra.Import;
|
||||||
|
|
||||||
|
internal static class DefaultTexToolsData
|
||||||
|
{
|
||||||
|
public const string Name = "New Mod";
|
||||||
|
public const string Author = "Unknown";
|
||||||
|
public const string Description = "Mod imported from TexTools mod pack.";
|
||||||
|
public const string DefaultOption = "Default";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
internal class SimpleMod
|
||||||
|
{
|
||||||
|
public string Name = string.Empty;
|
||||||
|
public string Category = string.Empty;
|
||||||
|
public string FullPath = string.Empty;
|
||||||
|
public string DatFile = string.Empty;
|
||||||
|
public long ModOffset = 0;
|
||||||
|
public long ModSize = 0;
|
||||||
|
public object? ModPackEntry = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
internal class ModPackPage
|
||||||
|
{
|
||||||
|
public int PageIndex = 0;
|
||||||
|
public ModGroup[] ModGroups = Array.Empty< ModGroup >();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
internal class ModGroup
|
||||||
|
{
|
||||||
|
public string GroupName = string.Empty;
|
||||||
|
public SelectType SelectionType = SelectType.Single;
|
||||||
|
public OptionList[] OptionList = Array.Empty< OptionList >();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
internal class OptionList
|
||||||
|
{
|
||||||
|
public string Name = string.Empty;
|
||||||
|
public string Description = string.Empty;
|
||||||
|
public string ImagePath = string.Empty;
|
||||||
|
public SimpleMod[] ModsJsons = Array.Empty< SimpleMod >();
|
||||||
|
public string GroupName = string.Empty;
|
||||||
|
public SelectType SelectionType = SelectType.Single;
|
||||||
|
public bool IsChecked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
internal class ExtendedModPack
|
||||||
|
{
|
||||||
|
public string PackVersion = string.Empty;
|
||||||
|
public string Name = DefaultTexToolsData.Name;
|
||||||
|
public string Author = DefaultTexToolsData.Author;
|
||||||
|
public string Version = string.Empty;
|
||||||
|
public string Description = DefaultTexToolsData.Description;
|
||||||
|
public ModPackPage[] ModPackPages = Array.Empty< ModPackPage >();
|
||||||
|
public SimpleMod[] SimpleModsList = Array.Empty< SimpleMod >();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
internal class SimpleModPack
|
||||||
|
{
|
||||||
|
public string TtmpVersion = string.Empty;
|
||||||
|
public string Name = DefaultTexToolsData.Name;
|
||||||
|
public string Author = DefaultTexToolsData.Author;
|
||||||
|
public string Version = string.Empty;
|
||||||
|
public string Description = DefaultTexToolsData.Description;
|
||||||
|
public SimpleMod[] SimpleModsList = Array.Empty< SimpleMod >();
|
||||||
|
}
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
namespace Penumbra.Importer
|
|
||||||
{
|
|
||||||
public enum ImporterState
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
WritingPackToDisk,
|
|
||||||
ExtractingModFiles,
|
|
||||||
Done,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using Penumbra.Util;
|
|
||||||
|
|
||||||
namespace Penumbra.Importer
|
|
||||||
{
|
|
||||||
public class MagicTempFileStreamManagerAndDeleter : PenumbraSqPackStream, IDisposable
|
|
||||||
{
|
|
||||||
private readonly FileStream _fileStream;
|
|
||||||
|
|
||||||
public MagicTempFileStreamManagerAndDeleter( FileStream stream )
|
|
||||||
: base( stream )
|
|
||||||
=> _fileStream = stream;
|
|
||||||
|
|
||||||
public new void Dispose()
|
|
||||||
{
|
|
||||||
var filePath = _fileStream.Name;
|
|
||||||
|
|
||||||
base.Dispose();
|
|
||||||
_fileStream.Dispose();
|
|
||||||
|
|
||||||
File.Delete( filePath );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using Penumbra.Mods;
|
|
||||||
|
|
||||||
namespace Penumbra.Importer.Models
|
|
||||||
{
|
|
||||||
internal class OptionList
|
|
||||||
{
|
|
||||||
public string? Name { get; set; }
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public string? ImagePath { get; set; }
|
|
||||||
public List< SimpleMod >? ModsJsons { get; set; }
|
|
||||||
public string? GroupName { get; set; }
|
|
||||||
public SelectType SelectionType { get; set; }
|
|
||||||
public bool IsChecked { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class ModGroup
|
|
||||||
{
|
|
||||||
public string? GroupName { get; set; }
|
|
||||||
public SelectType SelectionType { get; set; }
|
|
||||||
public List< OptionList >? OptionList { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class ModPackPage
|
|
||||||
{
|
|
||||||
public int PageIndex { get; set; }
|
|
||||||
public List< ModGroup >? ModGroups { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class ExtendedModPack
|
|
||||||
{
|
|
||||||
public string? TTMPVersion { get; set; }
|
|
||||||
public string? Name { get; set; }
|
|
||||||
public string? Author { get; set; }
|
|
||||||
public string? Version { get; set; }
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public List< ModPackPage >? ModPackPages { get; set; }
|
|
||||||
public List< SimpleMod >? SimpleModsList { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Penumbra.Importer.Models
|
|
||||||
{
|
|
||||||
internal class SimpleModPack
|
|
||||||
{
|
|
||||||
public string? TTMPVersion { get; set; }
|
|
||||||
public string? Name { get; set; }
|
|
||||||
public string? Author { get; set; }
|
|
||||||
public string? Version { get; set; }
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public List< SimpleMod >? SimpleModsList { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class SimpleMod
|
|
||||||
{
|
|
||||||
public string? Name { get; set; }
|
|
||||||
public string? Category { get; set; }
|
|
||||||
public string? FullPath { get; set; }
|
|
||||||
public long ModOffset { get; set; }
|
|
||||||
public long ModSize { get; set; }
|
|
||||||
public string? DatFile { get; set; }
|
|
||||||
public object? ModPackEntry { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,371 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using Dalamud.Logging;
|
|
||||||
using ICSharpCode.SharpZipLib.Zip;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Penumbra.Importer.Models;
|
|
||||||
using Penumbra.Mods;
|
|
||||||
using Penumbra.Util;
|
|
||||||
using FileMode = System.IO.FileMode;
|
|
||||||
|
|
||||||
namespace Penumbra.Importer;
|
|
||||||
|
|
||||||
internal class TexToolsImport
|
|
||||||
{
|
|
||||||
private readonly DirectoryInfo _outDirectory;
|
|
||||||
|
|
||||||
private const string TempFileName = "textools-import";
|
|
||||||
private readonly string _resolvedTempFilePath;
|
|
||||||
|
|
||||||
public DirectoryInfo? ExtractedDirectory { get; private set; }
|
|
||||||
|
|
||||||
public ImporterState State { get; private set; }
|
|
||||||
|
|
||||||
public long TotalProgress { get; private set; }
|
|
||||||
public long CurrentProgress { get; private set; }
|
|
||||||
|
|
||||||
public float Progress
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if( CurrentProgress != 0 )
|
|
||||||
{
|
|
||||||
// ReSharper disable twice RedundantCast
|
|
||||||
return ( float )CurrentProgress / ( float )TotalProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? CurrentModPack { get; private set; }
|
|
||||||
|
|
||||||
public TexToolsImport( DirectoryInfo outDirectory )
|
|
||||||
{
|
|
||||||
_outDirectory = outDirectory;
|
|
||||||
_resolvedTempFilePath = Path.Combine( _outDirectory.FullName, TempFileName );
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName )
|
|
||||||
=> new(Path.Combine( baseDir.FullName, optionName.ReplaceBadXivSymbols() ));
|
|
||||||
|
|
||||||
public DirectoryInfo ImportModPack( FileInfo modPackFile )
|
|
||||||
{
|
|
||||||
CurrentModPack = modPackFile.Name;
|
|
||||||
|
|
||||||
var dir = VerifyVersionAndImport( modPackFile );
|
|
||||||
|
|
||||||
State = ImporterState.Done;
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void WriteZipEntryToTempFile( Stream s )
|
|
||||||
{
|
|
||||||
var fs = new FileStream( _resolvedTempFilePath, FileMode.Create );
|
|
||||||
s.CopyTo( fs );
|
|
||||||
fs.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// You can in no way rely on any file paths in TTMPs so we need to just do this, sorry
|
|
||||||
private static ZipEntry? FindZipEntry( ZipFile file, string fileName )
|
|
||||||
{
|
|
||||||
for( var i = 0; i < file.Count; i++ )
|
|
||||||
{
|
|
||||||
var entry = file[ i ];
|
|
||||||
|
|
||||||
if( entry.Name.Contains( fileName ) )
|
|
||||||
{
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PenumbraSqPackStream GetMagicSqPackDeleterStream( ZipFile file, string entryName )
|
|
||||||
{
|
|
||||||
State = ImporterState.WritingPackToDisk;
|
|
||||||
|
|
||||||
// write shitty zip garbage to disk
|
|
||||||
var entry = FindZipEntry( file, entryName );
|
|
||||||
if( entry == null )
|
|
||||||
{
|
|
||||||
throw new FileNotFoundException( $"ZIP does not contain a file named {entryName}." );
|
|
||||||
}
|
|
||||||
|
|
||||||
using var s = file.GetInputStream( entry );
|
|
||||||
|
|
||||||
WriteZipEntryToTempFile( s );
|
|
||||||
|
|
||||||
var fs = new FileStream( _resolvedTempFilePath, FileMode.Open );
|
|
||||||
return new MagicTempFileStreamManagerAndDeleter( fs );
|
|
||||||
}
|
|
||||||
|
|
||||||
private DirectoryInfo VerifyVersionAndImport( FileInfo modPackFile )
|
|
||||||
{
|
|
||||||
using var zfs = modPackFile.OpenRead();
|
|
||||||
using var extractedModPack = new ZipFile( zfs );
|
|
||||||
|
|
||||||
var mpl = FindZipEntry( extractedModPack, "TTMPL.mpl" );
|
|
||||||
if( mpl == null )
|
|
||||||
{
|
|
||||||
throw new FileNotFoundException( "ZIP does not contain a TTMPL.mpl file." );
|
|
||||||
}
|
|
||||||
|
|
||||||
var modRaw = GetStringFromZipEntry( extractedModPack, mpl, Encoding.UTF8 );
|
|
||||||
|
|
||||||
// At least a better validation than going by the extension.
|
|
||||||
if( modRaw.Contains( "\"TTMPVersion\":" ) )
|
|
||||||
{
|
|
||||||
if( modPackFile.Extension != ".ttmp2" )
|
|
||||||
{
|
|
||||||
PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V2 TTMP, but has the wrong extension." );
|
|
||||||
}
|
|
||||||
|
|
||||||
return ImportV2ModPack( modPackFile, extractedModPack, modRaw );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( modPackFile.Extension != ".ttmp" )
|
|
||||||
{
|
|
||||||
PluginLog.Warning( $"File {modPackFile.FullName} seems to be a V1 TTMP, but has the wrong extension." );
|
|
||||||
}
|
|
||||||
|
|
||||||
return ImportV1ModPack( modPackFile, extractedModPack, modRaw );
|
|
||||||
}
|
|
||||||
|
|
||||||
private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw )
|
|
||||||
{
|
|
||||||
PluginLog.Log( " -> Importing V1 ModPack" );
|
|
||||||
|
|
||||||
var modListRaw = modRaw.Split(
|
|
||||||
new[] { "\r\n", "\r", "\n" },
|
|
||||||
StringSplitOptions.None
|
|
||||||
);
|
|
||||||
|
|
||||||
var modList = modListRaw.Select( JsonConvert.DeserializeObject< SimpleMod > );
|
|
||||||
|
|
||||||
// Open the mod data file from the modpack as a SqPackStream
|
|
||||||
using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" );
|
|
||||||
|
|
||||||
ExtractedDirectory = CreateModFolder( _outDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) );
|
|
||||||
// Create a new ModMeta from the TTMP modlist info
|
|
||||||
Mod2.CreateMeta( ExtractedDirectory, string.IsNullOrEmpty( modPackFile.Name ) ? "New Mod" : modPackFile.Name, "Unknown",
|
|
||||||
"Mod imported from TexTools mod pack.", null, null );
|
|
||||||
|
|
||||||
ExtractSimpleModList( ExtractedDirectory, modList, modData );
|
|
||||||
|
|
||||||
return ExtractedDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DirectoryInfo ImportV2ModPack( FileInfo _, ZipFile extractedModPack, string modRaw )
|
|
||||||
{
|
|
||||||
var modList = JsonConvert.DeserializeObject< SimpleModPack >( modRaw );
|
|
||||||
|
|
||||||
if( modList.TTMPVersion?.EndsWith( "s" ) ?? false )
|
|
||||||
{
|
|
||||||
return ImportSimpleV2ModPack( extractedModPack, modList );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( modList.TTMPVersion?.EndsWith( "w" ) ?? false )
|
|
||||||
{
|
|
||||||
return ImportExtendedV2ModPack( extractedModPack, modRaw );
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
PluginLog.Warning( $"Unknown TTMPVersion {modList.TTMPVersion ?? "NULL"} given, trying to export as simple Modpack." );
|
|
||||||
return ImportSimpleV2ModPack( extractedModPack, modList );
|
|
||||||
}
|
|
||||||
catch( Exception e1 )
|
|
||||||
{
|
|
||||||
PluginLog.Warning( $"Exporting as simple Modpack failed with following error, retrying as extended Modpack:\n{e1}" );
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return ImportExtendedV2ModPack( extractedModPack, modRaw );
|
|
||||||
}
|
|
||||||
catch( Exception e2 )
|
|
||||||
{
|
|
||||||
throw new IOException( "Exporting as extended Modpack failed, too. Version unsupported or file defect.", e2 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName )
|
|
||||||
{
|
|
||||||
var name = Path.GetFileName( modListName );
|
|
||||||
if( !name.Any() )
|
|
||||||
{
|
|
||||||
name = "_";
|
|
||||||
}
|
|
||||||
|
|
||||||
var newModFolderBase = NewOptionDirectory( outDirectory, name );
|
|
||||||
var newModFolder = newModFolderBase;
|
|
||||||
var i = 2;
|
|
||||||
while( newModFolder.Exists && i < 12 )
|
|
||||||
{
|
|
||||||
newModFolder = new DirectoryInfo( newModFolderBase.FullName + $" ({i++})" );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( newModFolder.Exists )
|
|
||||||
{
|
|
||||||
throw new IOException( "Could not create mod folder: too many folders of the same name exist." );
|
|
||||||
}
|
|
||||||
|
|
||||||
newModFolder.Create();
|
|
||||||
return newModFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DirectoryInfo ImportSimpleV2ModPack( ZipFile extractedModPack, SimpleModPack modList )
|
|
||||||
{
|
|
||||||
PluginLog.Log( " -> Importing Simple V2 ModPack" );
|
|
||||||
|
|
||||||
// Open the mod data file from the modpack as a SqPackStream
|
|
||||||
using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" );
|
|
||||||
|
|
||||||
ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" );
|
|
||||||
Mod2.CreateMeta( ExtractedDirectory, modList.Name ?? "New Mod", modList.Author ?? "Unknown", string.IsNullOrEmpty( modList.Description )
|
|
||||||
? "Mod imported from TexTools mod pack"
|
|
||||||
: modList.Description, null, null );
|
|
||||||
|
|
||||||
ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList ?? Enumerable.Empty< SimpleMod >(), modData );
|
|
||||||
return ExtractedDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw )
|
|
||||||
{
|
|
||||||
PluginLog.Log( " -> Importing Extended V2 ModPack" );
|
|
||||||
|
|
||||||
var modList = JsonConvert.DeserializeObject< ExtendedModPack >( modRaw );
|
|
||||||
|
|
||||||
// Open the mod data file from the modpack as a SqPackStream
|
|
||||||
using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" );
|
|
||||||
|
|
||||||
ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" );
|
|
||||||
Mod2.CreateMeta( ExtractedDirectory, modList.Name ?? "New Mod", modList.Author ?? "Unknown",
|
|
||||||
string.IsNullOrEmpty( modList.Description ) ? "Mod imported from TexTools mod pack" : modList.Description, modList.Version, null );
|
|
||||||
|
|
||||||
if( modList.SimpleModsList != null )
|
|
||||||
{
|
|
||||||
ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList, modData );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( modList.ModPackPages == null )
|
|
||||||
{
|
|
||||||
return ExtractedDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate through all pages
|
|
||||||
var options = new List< ISubMod >();
|
|
||||||
var groupPriority = 0;
|
|
||||||
foreach( var page in modList.ModPackPages )
|
|
||||||
{
|
|
||||||
if( page.ModGroups == null )
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach( var group in page.ModGroups.Where( group => group.GroupName != null && group.OptionList != null ) )
|
|
||||||
{
|
|
||||||
options.Clear();
|
|
||||||
var description = new StringBuilder();
|
|
||||||
var groupFolder = NewOptionDirectory( ExtractedDirectory, group.GroupName! );
|
|
||||||
if( groupFolder.Exists )
|
|
||||||
{
|
|
||||||
groupFolder = new DirectoryInfo( groupFolder.FullName + $" ({page.PageIndex})" );
|
|
||||||
group.GroupName += $" ({page.PageIndex})";
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach( var option in group.OptionList!.Where( option => option.Name != null && option.ModsJsons != null ) )
|
|
||||||
{
|
|
||||||
var optionFolder = NewOptionDirectory( groupFolder, option.Name! );
|
|
||||||
ExtractSimpleModList( optionFolder, option.ModsJsons!, modData );
|
|
||||||
options.Add( Mod2.CreateSubMod( ExtractedDirectory, optionFolder, option ) );
|
|
||||||
description.Append( option.Description );
|
|
||||||
if( !string.IsNullOrEmpty( option.Description ) )
|
|
||||||
{
|
|
||||||
description.Append( '\n' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Mod2.CreateOptionGroup( ExtractedDirectory, group, groupPriority++, description.ToString(), options );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Mod2.CreateDefaultFiles( ExtractedDirectory );
|
|
||||||
return ExtractedDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ImportMetaModPack( FileInfo file )
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ExtractSimpleModList( DirectoryInfo outDirectory, IEnumerable< SimpleMod > mods, PenumbraSqPackStream dataStream )
|
|
||||||
{
|
|
||||||
State = ImporterState.ExtractingModFiles;
|
|
||||||
|
|
||||||
// haha allocation go brr
|
|
||||||
var wtf = mods.ToList();
|
|
||||||
|
|
||||||
TotalProgress += wtf.LongCount();
|
|
||||||
|
|
||||||
// Extract each SimpleMod into the new mod folder
|
|
||||||
foreach( var simpleMod in wtf.Where( m => m != null ) )
|
|
||||||
{
|
|
||||||
ExtractMod( outDirectory, simpleMod, dataStream );
|
|
||||||
CurrentProgress++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ExtractMod( DirectoryInfo outDirectory, SimpleMod mod, PenumbraSqPackStream dataStream )
|
|
||||||
{
|
|
||||||
PluginLog.Log( " -> Extracting {0} at {1}", mod.FullPath!, mod.ModOffset.ToString( "X" ) );
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var data = dataStream.ReadFile< PenumbraSqPackStream.PenumbraFileResource >( mod.ModOffset );
|
|
||||||
|
|
||||||
var extractedFile = new FileInfo( Path.Combine( outDirectory.FullName, mod.FullPath! ) );
|
|
||||||
extractedFile.Directory?.Create();
|
|
||||||
|
|
||||||
if( extractedFile.FullName.EndsWith( "mdl" ) )
|
|
||||||
{
|
|
||||||
ProcessMdl( data.Data );
|
|
||||||
}
|
|
||||||
|
|
||||||
File.WriteAllBytes( extractedFile.FullName, data.Data );
|
|
||||||
}
|
|
||||||
catch( Exception ex )
|
|
||||||
{
|
|
||||||
PluginLog.LogError( ex, "Could not extract mod." );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ProcessMdl( byte[] mdl )
|
|
||||||
{
|
|
||||||
// Model file header LOD num
|
|
||||||
mdl[ 64 ] = 1;
|
|
||||||
|
|
||||||
// Model header LOD num
|
|
||||||
var stackSize = BitConverter.ToUInt32( mdl, 4 );
|
|
||||||
var runtimeBegin = stackSize + 0x44;
|
|
||||||
var stringsLengthOffset = runtimeBegin + 4;
|
|
||||||
var stringsLength = BitConverter.ToUInt32( mdl, ( int )stringsLengthOffset );
|
|
||||||
var modelHeaderStart = stringsLengthOffset + stringsLength + 4;
|
|
||||||
var modelHeaderLodOffset = 22;
|
|
||||||
mdl[ modelHeaderStart + modelHeaderLodOffset ] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Stream GetStreamFromZipEntry( ZipFile file, ZipEntry entry )
|
|
||||||
=> file.GetInputStream( entry );
|
|
||||||
|
|
||||||
private static string GetStringFromZipEntry( ZipFile file, ZipEntry entry, Encoding encoding )
|
|
||||||
{
|
|
||||||
using var ms = new MemoryStream();
|
|
||||||
using var s = GetStreamFromZipEntry( file, entry );
|
|
||||||
s.CopyTo( ms );
|
|
||||||
return encoding.GetString( ms.ToArray() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,427 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Dalamud.Logging;
|
|
||||||
using Penumbra.GameData.Enums;
|
|
||||||
using Penumbra.GameData.Structs;
|
|
||||||
using Penumbra.GameData.Util;
|
|
||||||
using Penumbra.Meta.Files;
|
|
||||||
using Penumbra.Meta.Manipulations;
|
|
||||||
using Penumbra.Util;
|
|
||||||
using ImcFile = Penumbra.Meta.Files.ImcFile;
|
|
||||||
|
|
||||||
namespace Penumbra.Importer;
|
|
||||||
|
|
||||||
// TexTools provices custom generated *.meta files for its modpacks, that contain changes to
|
|
||||||
// - imc files
|
|
||||||
// - eqp files
|
|
||||||
// - gmp files
|
|
||||||
// - est files
|
|
||||||
// - eqdp files
|
|
||||||
// made by the mod. The filename determines to what the changes are applied, and the binary file itself contains changes.
|
|
||||||
// We parse every *.meta file in a mod and combine all actual changes that do not keep data on default values and that can be applied to the game in a .json.
|
|
||||||
// TexTools may also generate files that contain non-existing changes, e.g. *.imc files for weapon offhands, which will be ignored.
|
|
||||||
// TexTools also provides .rgsp files, that contain changes to the racial scaling parameters in the human.cmp file.
|
|
||||||
public class TexToolsMeta
|
|
||||||
{
|
|
||||||
// The info class determines the files or table locations the changes need to apply to from the filename.
|
|
||||||
public class Info
|
|
||||||
{
|
|
||||||
private const string Pt = @"(?'PrimaryType'[a-z]*)"; // language=regex
|
|
||||||
private const string Pp = @"(?'PrimaryPrefix'[a-z])"; // language=regex
|
|
||||||
private const string Pi = @"(?'PrimaryId'\d{4})"; // language=regex
|
|
||||||
private const string Pir = @"\k'PrimaryId'"; // language=regex
|
|
||||||
private const string St = @"(?'SecondaryType'[a-z]*)"; // language=regex
|
|
||||||
private const string Sp = @"(?'SecondaryPrefix'[a-z])"; // language=regex
|
|
||||||
private const string Si = @"(?'SecondaryId'\d{4})"; // language=regex
|
|
||||||
private const string File = @"\k'PrimaryPrefix'\k'PrimaryId'(\k'SecondaryPrefix'\k'SecondaryId')?"; // language=regex
|
|
||||||
private const string Slot = @"(_(?'Slot'[a-z]{3}))?"; // language=regex
|
|
||||||
private const string Ext = @"\.meta";
|
|
||||||
|
|
||||||
// These are the valid regexes for .meta files that we are able to support at the moment.
|
|
||||||
private static readonly Regex HousingMeta = new($"bgcommon/hou/{Pt}/general/{Pi}/{Pir}{Ext}", RegexOptions.Compiled);
|
|
||||||
private static readonly Regex CharaMeta = new($"chara/{Pt}/{Pp}{Pi}(/obj/{St}/{Sp}{Si})?/{File}{Slot}{Ext}", RegexOptions.Compiled);
|
|
||||||
|
|
||||||
public readonly ObjectType PrimaryType;
|
|
||||||
public readonly BodySlot SecondaryType;
|
|
||||||
public readonly ushort PrimaryId;
|
|
||||||
public readonly ushort SecondaryId;
|
|
||||||
public readonly EquipSlot EquipSlot = EquipSlot.Unknown;
|
|
||||||
public readonly CustomizationType CustomizationType = CustomizationType.Unknown;
|
|
||||||
|
|
||||||
private static bool ValidType( ObjectType type )
|
|
||||||
{
|
|
||||||
return type switch
|
|
||||||
{
|
|
||||||
ObjectType.Accessory => true,
|
|
||||||
ObjectType.Character => true,
|
|
||||||
ObjectType.Equipment => true,
|
|
||||||
ObjectType.DemiHuman => true,
|
|
||||||
ObjectType.Housing => true,
|
|
||||||
ObjectType.Monster => true,
|
|
||||||
ObjectType.Weapon => true,
|
|
||||||
ObjectType.Icon => false,
|
|
||||||
ObjectType.Font => false,
|
|
||||||
ObjectType.Interface => false,
|
|
||||||
ObjectType.LoadingScreen => false,
|
|
||||||
ObjectType.Map => false,
|
|
||||||
ObjectType.Vfx => false,
|
|
||||||
ObjectType.Unknown => false,
|
|
||||||
ObjectType.World => false,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public Info( string fileName )
|
|
||||||
: this( new GamePath( fileName ) )
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public Info( GamePath fileName )
|
|
||||||
{
|
|
||||||
PrimaryType = GameData.GameData.GetGamePathParser().PathToObjectType( fileName );
|
|
||||||
PrimaryId = 0;
|
|
||||||
SecondaryType = BodySlot.Unknown;
|
|
||||||
SecondaryId = 0;
|
|
||||||
if( !ValidType( PrimaryType ) )
|
|
||||||
{
|
|
||||||
PrimaryType = ObjectType.Unknown;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( PrimaryType == ObjectType.Housing )
|
|
||||||
{
|
|
||||||
var housingMatch = HousingMeta.Match( fileName );
|
|
||||||
if( housingMatch.Success )
|
|
||||||
{
|
|
||||||
PrimaryId = ushort.Parse( housingMatch.Groups[ "PrimaryId" ].Value );
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var match = CharaMeta.Match( fileName );
|
|
||||||
if( !match.Success )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PrimaryId = ushort.Parse( match.Groups[ "PrimaryId" ].Value );
|
|
||||||
if( match.Groups[ "Slot" ].Success )
|
|
||||||
{
|
|
||||||
switch( PrimaryType )
|
|
||||||
{
|
|
||||||
case ObjectType.Equipment:
|
|
||||||
case ObjectType.Accessory:
|
|
||||||
if( Names.SuffixToEquipSlot.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpSlot ) )
|
|
||||||
{
|
|
||||||
EquipSlot = tmpSlot;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case ObjectType.Character:
|
|
||||||
if( Names.SuffixToCustomizationType.TryGetValue( match.Groups[ "Slot" ].Value, out var tmpCustom ) )
|
|
||||||
{
|
|
||||||
CustomizationType = tmpCustom;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( match.Groups[ "SecondaryType" ].Success
|
|
||||||
&& Names.StringToBodySlot.TryGetValue( match.Groups[ "SecondaryType" ].Value, out SecondaryType ) )
|
|
||||||
{
|
|
||||||
SecondaryId = ushort.Parse( match.Groups[ "SecondaryId" ].Value );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly uint Version;
|
|
||||||
public readonly string FilePath;
|
|
||||||
public readonly List< EqpManipulation > EqpManipulations = new();
|
|
||||||
public readonly List< GmpManipulation > GmpManipulations = new();
|
|
||||||
public readonly List< EqdpManipulation > EqdpManipulations = new();
|
|
||||||
public readonly List< EstManipulation > EstManipulations = new();
|
|
||||||
public readonly List< RspManipulation > RspManipulations = new();
|
|
||||||
public readonly List< ImcManipulation > ImcManipulations = new();
|
|
||||||
|
|
||||||
private void DeserializeEqpEntry( Info info, byte[]? data )
|
|
||||||
{
|
|
||||||
if( data == null || !info.EquipSlot.IsEquipment() )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var value = Eqp.FromSlotAndBytes( info.EquipSlot, data );
|
|
||||||
var def = new EqpManipulation( ExpandedEqpFile.GetDefault( info.PrimaryId ), info.EquipSlot, info.PrimaryId );
|
|
||||||
var manip = new EqpManipulation( value, info.EquipSlot, info.PrimaryId );
|
|
||||||
if( def.Entry != manip.Entry )
|
|
||||||
{
|
|
||||||
EqpManipulations.Add( manip );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeserializeEqdpEntries( Info info, byte[]? data )
|
|
||||||
{
|
|
||||||
if( data == null )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var num = data.Length / 5;
|
|
||||||
using var reader = new BinaryReader( new MemoryStream( data ) );
|
|
||||||
for( var i = 0; i < num; ++i )
|
|
||||||
{
|
|
||||||
var gr = ( GenderRace )reader.ReadUInt32();
|
|
||||||
var byteValue = reader.ReadByte();
|
|
||||||
if( !gr.IsValid() || !info.EquipSlot.IsEquipment() && !info.EquipSlot.IsAccessory() )
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var value = Eqdp.FromSlotAndBits( info.EquipSlot, ( byteValue & 1 ) == 1, ( byteValue & 2 ) == 2 );
|
|
||||||
var def = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, info.EquipSlot.IsAccessory(), info.PrimaryId ), info.EquipSlot,
|
|
||||||
gr.Split().Item1, gr.Split().Item2, info.PrimaryId );
|
|
||||||
var manip = new EqdpManipulation( value, info.EquipSlot, gr.Split().Item1, gr.Split().Item2, info.PrimaryId );
|
|
||||||
if( def.Entry != manip.Entry )
|
|
||||||
{
|
|
||||||
EqdpManipulations.Add( manip );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeserializeGmpEntry( Info info, byte[]? data )
|
|
||||||
{
|
|
||||||
if( data == null )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var reader = new BinaryReader( new MemoryStream( data ) );
|
|
||||||
var value = ( GmpEntry )reader.ReadUInt32();
|
|
||||||
value.UnknownTotal = reader.ReadByte();
|
|
||||||
var def = ExpandedGmpFile.GetDefault( info.PrimaryId );
|
|
||||||
if( value != def )
|
|
||||||
{
|
|
||||||
GmpManipulations.Add( new GmpManipulation( value, info.PrimaryId ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeserializeEstEntries( Info info, byte[]? data )
|
|
||||||
{
|
|
||||||
if( data == null )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var num = data.Length / 6;
|
|
||||||
using var reader = new BinaryReader( new MemoryStream( data ) );
|
|
||||||
for( var i = 0; i < num; ++i )
|
|
||||||
{
|
|
||||||
var gr = ( GenderRace )reader.ReadUInt16();
|
|
||||||
var id = reader.ReadUInt16();
|
|
||||||
var value = reader.ReadUInt16();
|
|
||||||
var type = ( info.SecondaryType, info.EquipSlot ) switch
|
|
||||||
{
|
|
||||||
(BodySlot.Face, _) => EstManipulation.EstType.Face,
|
|
||||||
(BodySlot.Hair, _) => EstManipulation.EstType.Hair,
|
|
||||||
(_, EquipSlot.Head) => EstManipulation.EstType.Head,
|
|
||||||
(_, EquipSlot.Body) => EstManipulation.EstType.Body,
|
|
||||||
_ => ( EstManipulation.EstType )0,
|
|
||||||
};
|
|
||||||
if( !gr.IsValid() || type == 0 )
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var def = EstFile.GetDefault( type, gr, id );
|
|
||||||
if( def != value )
|
|
||||||
{
|
|
||||||
EstManipulations.Add( new EstManipulation( gr.Split().Item1, gr.Split().Item2, type, id, value ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeserializeImcEntries( Info info, byte[]? data )
|
|
||||||
{
|
|
||||||
if( data == null )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var num = data.Length / 6;
|
|
||||||
using var reader = new BinaryReader( new MemoryStream( data ) );
|
|
||||||
var values = reader.ReadStructures< ImcEntry >( num );
|
|
||||||
ushort i = 0;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if( info.PrimaryType is ObjectType.Equipment or ObjectType.Accessory )
|
|
||||||
{
|
|
||||||
var def = new ImcFile( new ImcManipulation( info.EquipSlot, i, info.PrimaryId, new ImcEntry() ).GamePath() );
|
|
||||||
var partIdx = ImcFile.PartIndex( info.EquipSlot );
|
|
||||||
foreach( var value in values )
|
|
||||||
{
|
|
||||||
if( !value.Equals( def.GetEntry( partIdx, i ) ) )
|
|
||||||
{
|
|
||||||
ImcManipulations.Add( new ImcManipulation( info.EquipSlot, i, info.PrimaryId, value ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var def = new ImcFile( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i,
|
|
||||||
new ImcEntry() ).GamePath() );
|
|
||||||
foreach( var value in values )
|
|
||||||
{
|
|
||||||
if( !value.Equals( def.GetEntry( 0, i ) ) )
|
|
||||||
{
|
|
||||||
ImcManipulations.Add( new ImcManipulation( info.PrimaryType, info.SecondaryType, info.PrimaryId, info.SecondaryId, i,
|
|
||||||
value ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch( Exception e )
|
|
||||||
{
|
|
||||||
PluginLog.Warning( $"Could not compute IMC manipulation for {info.PrimaryType} {info.PrimaryId}. This is in all likelihood due to TexTools corrupting your index files.\n"
|
|
||||||
+ $"If the following error looks like Lumina is having trouble to read an IMC file, please do a do-over in TexTools:\n{e}" );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ReadNullTerminated( BinaryReader reader )
|
|
||||||
{
|
|
||||||
var builder = new System.Text.StringBuilder();
|
|
||||||
for( var c = reader.ReadChar(); c != 0; c = reader.ReadChar() )
|
|
||||||
{
|
|
||||||
builder.Append( c );
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public TexToolsMeta( byte[] data )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var reader = new BinaryReader( new MemoryStream( data ) );
|
|
||||||
Version = reader.ReadUInt32();
|
|
||||||
FilePath = ReadNullTerminated( reader );
|
|
||||||
var metaInfo = new Info( FilePath );
|
|
||||||
var numHeaders = reader.ReadUInt32();
|
|
||||||
var headerSize = reader.ReadUInt32();
|
|
||||||
var headerStart = reader.ReadUInt32();
|
|
||||||
reader.BaseStream.Seek( headerStart, SeekOrigin.Begin );
|
|
||||||
|
|
||||||
List< (MetaManipulation.Type type, uint offset, int size) > entries = new();
|
|
||||||
for( var i = 0; i < numHeaders; ++i )
|
|
||||||
{
|
|
||||||
var currentOffset = reader.BaseStream.Position;
|
|
||||||
var type = ( MetaManipulation.Type )reader.ReadUInt32();
|
|
||||||
var offset = reader.ReadUInt32();
|
|
||||||
var size = reader.ReadInt32();
|
|
||||||
entries.Add( ( type, offset, size ) );
|
|
||||||
reader.BaseStream.Seek( currentOffset + headerSize, SeekOrigin.Begin );
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[]? ReadEntry( MetaManipulation.Type type )
|
|
||||||
{
|
|
||||||
var idx = entries.FindIndex( t => t.type == type );
|
|
||||||
if( idx < 0 )
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.BaseStream.Seek( entries[ idx ].offset, SeekOrigin.Begin );
|
|
||||||
return reader.ReadBytes( entries[ idx ].size );
|
|
||||||
}
|
|
||||||
|
|
||||||
DeserializeEqpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Eqp ) );
|
|
||||||
DeserializeGmpEntry( metaInfo, ReadEntry( MetaManipulation.Type.Gmp ) );
|
|
||||||
DeserializeEqdpEntries( metaInfo, ReadEntry( MetaManipulation.Type.Eqdp ) );
|
|
||||||
DeserializeEstEntries( metaInfo, ReadEntry( MetaManipulation.Type.Est ) );
|
|
||||||
DeserializeImcEntries( metaInfo, ReadEntry( MetaManipulation.Type.Imc ) );
|
|
||||||
}
|
|
||||||
catch( Exception e )
|
|
||||||
{
|
|
||||||
FilePath = "";
|
|
||||||
PluginLog.Error( $"Error while parsing .meta file:\n{e}" );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private TexToolsMeta( string filePath, uint version )
|
|
||||||
{
|
|
||||||
FilePath = filePath;
|
|
||||||
Version = version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TexToolsMeta Invalid = new(string.Empty, 0);
|
|
||||||
|
|
||||||
public static TexToolsMeta FromRgspFile( string filePath, byte[] data )
|
|
||||||
{
|
|
||||||
if( data.Length != 45 && data.Length != 42 )
|
|
||||||
{
|
|
||||||
PluginLog.Error( "Error while parsing .rgsp file:\n\tInvalid number of bytes." );
|
|
||||||
return Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var s = new MemoryStream( data );
|
|
||||||
using var br = new BinaryReader( s );
|
|
||||||
var flag = br.ReadByte();
|
|
||||||
var version = flag != 255 ? ( uint )1 : br.ReadUInt16();
|
|
||||||
|
|
||||||
var ret = new TexToolsMeta( filePath, version );
|
|
||||||
|
|
||||||
var subRace = ( SubRace )( version == 1 ? flag + 1 : br.ReadByte() + 1 );
|
|
||||||
if( !Enum.IsDefined( typeof( SubRace ), subRace ) || subRace == SubRace.Unknown )
|
|
||||||
{
|
|
||||||
PluginLog.Error( $"Error while parsing .rgsp file:\n\t{subRace} is not a valid SubRace." );
|
|
||||||
return Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
var gender = br.ReadByte();
|
|
||||||
if( gender != 1 && gender != 0 )
|
|
||||||
{
|
|
||||||
PluginLog.Error( $"Error while parsing .rgsp file:\n\t{gender} is neither Male nor Female." );
|
|
||||||
return Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Add( RspAttribute attribute, float value )
|
|
||||||
{
|
|
||||||
var def = CmpFile.GetDefault( subRace, attribute );
|
|
||||||
if( value != def )
|
|
||||||
{
|
|
||||||
ret!.RspManipulations.Add( new RspManipulation( subRace, attribute, value ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( gender == 1 )
|
|
||||||
{
|
|
||||||
Add( RspAttribute.FemaleMinSize, br.ReadSingle() );
|
|
||||||
Add( RspAttribute.FemaleMaxSize, br.ReadSingle() );
|
|
||||||
Add( RspAttribute.FemaleMinTail, br.ReadSingle() );
|
|
||||||
Add( RspAttribute.FemaleMaxTail, br.ReadSingle() );
|
|
||||||
|
|
||||||
Add( RspAttribute.BustMinX, br.ReadSingle() );
|
|
||||||
Add( RspAttribute.BustMinY, br.ReadSingle() );
|
|
||||||
Add( RspAttribute.BustMinZ, br.ReadSingle() );
|
|
||||||
Add( RspAttribute.BustMaxX, br.ReadSingle() );
|
|
||||||
Add( RspAttribute.BustMaxY, br.ReadSingle() );
|
|
||||||
Add( RspAttribute.BustMaxZ, br.ReadSingle() );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Add( RspAttribute.MaleMinSize, br.ReadSingle() );
|
|
||||||
Add( RspAttribute.MaleMaxSize, br.ReadSingle() );
|
|
||||||
Add( RspAttribute.MaleMinTail, br.ReadSingle() );
|
|
||||||
Add( RspAttribute.MaleMaxTail, br.ReadSingle() );
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.Interop.Structs;
|
using Penumbra.Interop.Structs;
|
||||||
using Penumbra.Mods;
|
|
||||||
using FileMode = Penumbra.Interop.Structs.FileMode;
|
using FileMode = Penumbra.Interop.Structs.FileMode;
|
||||||
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
|
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ public partial class MetaManager
|
||||||
private readonly ModCollection _collection;
|
private readonly ModCollection _collection;
|
||||||
private static int _imcManagerCount;
|
private static int _imcManagerCount;
|
||||||
|
|
||||||
|
|
||||||
public MetaManagerImc( ModCollection collection )
|
public MetaManagerImc( ModCollection collection )
|
||||||
{
|
{
|
||||||
_collection = collection;
|
_collection = collection;
|
||||||
|
|
|
||||||
|
|
@ -12,91 +12,10 @@ public interface IMetaManipulation
|
||||||
public int FileIndex();
|
public int FileIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IMetaManipulation< T > : IMetaManipulation, IComparable< T >, IEquatable< T > where T : struct
|
public interface IMetaManipulation< T >
|
||||||
|
: IMetaManipulation, IComparable< T >, IEquatable< T > where T : struct
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public struct ManipulationSet< T > where T : struct, IMetaManipulation< T >
|
|
||||||
{
|
|
||||||
private List< T >? _data = null;
|
|
||||||
|
|
||||||
public IReadOnlyList< T > Data
|
|
||||||
=> ( IReadOnlyList< T >? )_data ?? Array.Empty< T >();
|
|
||||||
|
|
||||||
public int Count
|
|
||||||
=> _data?.Count ?? 0;
|
|
||||||
|
|
||||||
public ManipulationSet( int count = 0 )
|
|
||||||
{
|
|
||||||
if( count > 0 )
|
|
||||||
{
|
|
||||||
_data = new List< T >( count );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryAdd( T manip )
|
|
||||||
{
|
|
||||||
if( _data == null )
|
|
||||||
{
|
|
||||||
_data = new List< T > { manip };
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var idx = _data.BinarySearch( manip );
|
|
||||||
if( idx >= 0 )
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_data.Insert( ~idx, manip );
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Set( T manip )
|
|
||||||
{
|
|
||||||
if( _data == null )
|
|
||||||
{
|
|
||||||
_data = new List< T > { manip };
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var idx = _data.BinarySearch( manip );
|
|
||||||
if( idx >= 0 )
|
|
||||||
{
|
|
||||||
_data[ idx ] = manip;
|
|
||||||
return idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
idx = ~idx;
|
|
||||||
_data.Insert( idx, manip );
|
|
||||||
return idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGet( T manip, out T value )
|
|
||||||
{
|
|
||||||
var idx = _data?.BinarySearch( manip ) ?? -1;
|
|
||||||
if( idx < 0 )
|
|
||||||
{
|
|
||||||
value = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = _data![ idx ];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Remove( T manip )
|
|
||||||
{
|
|
||||||
var idx = _data?.BinarySearch( manip ) ?? -1;
|
|
||||||
if( idx < 0 )
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_data!.RemoveAt( idx );
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout( LayoutKind.Explicit, Pack = 1, Size = 16 )]
|
[StructLayout( LayoutKind.Explicit, Pack = 1, Size = 16 )]
|
||||||
public readonly struct MetaManipulation : IEquatable< MetaManipulation >, IComparable< MetaManipulation >
|
public readonly struct MetaManipulation : IEquatable< MetaManipulation >, IComparable< MetaManipulation >
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ public partial class Configuration
|
||||||
private void ResettleSortOrder()
|
private void ResettleSortOrder()
|
||||||
{
|
{
|
||||||
ModSortOrder = _data[ nameof( ModSortOrder ) ]?.ToObject< Dictionary< string, string > >() ?? ModSortOrder;
|
ModSortOrder = _data[ nameof( ModSortOrder ) ]?.ToObject< Dictionary< string, string > >() ?? ModSortOrder;
|
||||||
var file = Mod2.Manager.ModFileSystemFile;
|
var file = ModFileSystem.ModFileSystemFile;
|
||||||
using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew );
|
using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew );
|
||||||
using var writer = new StreamWriter( stream );
|
using var writer = new StreamWriter( stream );
|
||||||
using var j = new JsonTextWriter( writer );
|
using var j = new JsonTextWriter( writer );
|
||||||
|
|
@ -169,7 +169,7 @@ public partial class Configuration
|
||||||
var data = JArray.Parse( text );
|
var data = JArray.Parse( text );
|
||||||
|
|
||||||
var maxPriority = 0;
|
var maxPriority = 0;
|
||||||
var dict = new Dictionary< string, ModSettings2.SavedSettings >();
|
var dict = new Dictionary< string, ModSettings.SavedSettings >();
|
||||||
foreach( var setting in data.Cast< JObject >() )
|
foreach( var setting in data.Cast< JObject >() )
|
||||||
{
|
{
|
||||||
var modName = ( string )setting[ "FolderName" ]!;
|
var modName = ( string )setting[ "FolderName" ]!;
|
||||||
|
|
@ -178,7 +178,7 @@ public partial class Configuration
|
||||||
var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, uint > >()
|
var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, uint > >()
|
||||||
?? setting[ "Conf" ]!.ToObject< Dictionary< string, uint > >();
|
?? setting[ "Conf" ]!.ToObject< Dictionary< string, uint > >();
|
||||||
|
|
||||||
dict[ modName ] = new ModSettings2.SavedSettings()
|
dict[ modName ] = new ModSettings.SavedSettings()
|
||||||
{
|
{
|
||||||
Enabled = enabled,
|
Enabled = enabled,
|
||||||
Priority = priority,
|
Priority = priority,
|
||||||
|
|
|
||||||
65
Penumbra/Mods/Manager/Mod.Manager.BasePath.cs
Normal file
65
Penumbra/Mods/Manager/Mod.Manager.BasePath.cs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Logging;
|
||||||
|
|
||||||
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
|
public partial class Mod
|
||||||
|
{
|
||||||
|
public partial class Manager
|
||||||
|
{
|
||||||
|
public delegate void ModPathChangeDelegate( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
||||||
|
DirectoryInfo? newDirectory );
|
||||||
|
|
||||||
|
public event ModPathChangeDelegate? ModPathChanged;
|
||||||
|
|
||||||
|
public void MoveModDirectory( Index idx, DirectoryInfo newDirectory )
|
||||||
|
{
|
||||||
|
var mod = this[ idx ];
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteMod( int idx )
|
||||||
|
{
|
||||||
|
var mod = this[ idx ];
|
||||||
|
if( Directory.Exists( mod.BasePath.FullName ) )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete( mod.BasePath.FullName, true );
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
PluginLog.Error( $"Could not delete the mod {mod.BasePath.Name}:\n{e}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_mods.RemoveAt( idx );
|
||||||
|
foreach( var remainingMod in _mods.Skip( idx ) )
|
||||||
|
{
|
||||||
|
--remainingMod.Index;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModPathChanged?.Invoke( ModPathChangeType.Deleted, mod, mod.BasePath, null );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddMod( DirectoryInfo modFolder )
|
||||||
|
{
|
||||||
|
if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mod = LoadMod( modFolder );
|
||||||
|
if( mod == null )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod.Index = _mods.Count;
|
||||||
|
_mods.Add( mod );
|
||||||
|
ModPathChanged?.Invoke( ModPathChangeType.Added, mod, null, mod.BasePath );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,11 @@ using System;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public sealed partial class Mod2
|
public sealed partial class Mod
|
||||||
{
|
{
|
||||||
public partial class Manager
|
public partial class Manager
|
||||||
{
|
{
|
||||||
public delegate void ModMetaChangeDelegate( MetaChangeType type, Mod2 mod );
|
public delegate void ModMetaChangeDelegate( MetaChangeType type, Mod mod, string? oldName );
|
||||||
public event ModMetaChangeDelegate? ModMetaChanged;
|
public event ModMetaChangeDelegate? ModMetaChanged;
|
||||||
|
|
||||||
public void ChangeModName( Index idx, string newName )
|
public void ChangeModName( Index idx, string newName )
|
||||||
|
|
@ -14,9 +14,10 @@ public sealed partial class Mod2
|
||||||
var mod = this[ idx ];
|
var mod = this[ idx ];
|
||||||
if( mod.Name != newName )
|
if( mod.Name != newName )
|
||||||
{
|
{
|
||||||
|
var oldName = mod.Name;
|
||||||
mod.Name = newName;
|
mod.Name = newName;
|
||||||
mod.SaveMeta();
|
mod.SaveMeta();
|
||||||
ModMetaChanged?.Invoke( MetaChangeType.Name, mod );
|
ModMetaChanged?.Invoke( MetaChangeType.Name, mod, oldName.Text );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,7 +28,7 @@ public sealed partial class Mod2
|
||||||
{
|
{
|
||||||
mod.Author = newAuthor;
|
mod.Author = newAuthor;
|
||||||
mod.SaveMeta();
|
mod.SaveMeta();
|
||||||
ModMetaChanged?.Invoke( MetaChangeType.Author, mod );
|
ModMetaChanged?.Invoke( MetaChangeType.Author, mod, null );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,7 +39,7 @@ public sealed partial class Mod2
|
||||||
{
|
{
|
||||||
mod.Description = newDescription;
|
mod.Description = newDescription;
|
||||||
mod.SaveMeta();
|
mod.SaveMeta();
|
||||||
ModMetaChanged?.Invoke( MetaChangeType.Description, mod );
|
ModMetaChanged?.Invoke( MetaChangeType.Description, mod, null );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,7 +50,7 @@ public sealed partial class Mod2
|
||||||
{
|
{
|
||||||
mod.Version = newVersion;
|
mod.Version = newVersion;
|
||||||
mod.SaveMeta();
|
mod.SaveMeta();
|
||||||
ModMetaChanged?.Invoke( MetaChangeType.Version, mod );
|
ModMetaChanged?.Invoke( MetaChangeType.Version, mod, null );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,7 +61,7 @@ public sealed partial class Mod2
|
||||||
{
|
{
|
||||||
mod.Website = newWebsite;
|
mod.Website = newWebsite;
|
||||||
mod.SaveMeta();
|
mod.SaveMeta();
|
||||||
ModMetaChanged?.Invoke( MetaChangeType.Website, mod );
|
ModMetaChanged?.Invoke( MetaChangeType.Website, mod, null );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
|
using OtterGui.Filesystem;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
|
|
@ -13,25 +14,43 @@ public enum ModOptionChangeType
|
||||||
GroupRenamed,
|
GroupRenamed,
|
||||||
GroupAdded,
|
GroupAdded,
|
||||||
GroupDeleted,
|
GroupDeleted,
|
||||||
|
GroupMoved,
|
||||||
|
GroupTypeChanged,
|
||||||
PriorityChanged,
|
PriorityChanged,
|
||||||
OptionAdded,
|
OptionAdded,
|
||||||
OptionDeleted,
|
OptionDeleted,
|
||||||
OptionChanged,
|
OptionMoved,
|
||||||
|
OptionFilesChanged,
|
||||||
|
OptionSwapsChanged,
|
||||||
|
OptionMetaChanged,
|
||||||
|
OptionUpdated,
|
||||||
DisplayChange,
|
DisplayChange,
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed partial class Mod2
|
public sealed partial class Mod
|
||||||
{
|
{
|
||||||
public sealed partial class Manager
|
public sealed partial class Manager
|
||||||
{
|
{
|
||||||
public delegate void ModOptionChangeDelegate( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx );
|
public delegate void ModOptionChangeDelegate( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx );
|
||||||
public event ModOptionChangeDelegate ModOptionChanged;
|
public event ModOptionChangeDelegate ModOptionChanged;
|
||||||
|
|
||||||
public void RenameModGroup( Mod2 mod, int groupIdx, string newName )
|
public void ChangeModGroupType( Mod mod, int groupIdx, SelectType type )
|
||||||
|
{
|
||||||
|
var group = mod._groups[ groupIdx ];
|
||||||
|
if( group.Type == type )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod._groups[ groupIdx ] = group.Convert( type );
|
||||||
|
ModOptionChanged.Invoke( ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RenameModGroup( Mod mod, int groupIdx, string newName )
|
||||||
{
|
{
|
||||||
var group = mod._groups[ groupIdx ];
|
var group = mod._groups[ groupIdx ];
|
||||||
var oldName = group.Name;
|
var oldName = group.Name;
|
||||||
if( oldName == newName || !VerifyFileName( mod, group, newName ) )
|
if( oldName == newName || !VerifyFileName( mod, group, newName, true ) )
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -43,33 +62,41 @@ public sealed partial class Mod2
|
||||||
_ => newName,
|
_ => newName,
|
||||||
};
|
};
|
||||||
|
|
||||||
ModOptionChanged.Invoke( ModOptionChangeType.GroupRenamed, mod, groupIdx, 0 );
|
ModOptionChanged.Invoke( ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddModGroup( Mod2 mod, SelectType type, string newName )
|
public void AddModGroup( Mod mod, SelectType type, string newName )
|
||||||
{
|
{
|
||||||
if( !VerifyFileName( mod, null, newName ) )
|
if( !VerifyFileName( mod, null, newName, true ) )
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxPriority = mod._groups.Max( o => o.Priority ) + 1;
|
var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max( o => o.Priority ) + 1;
|
||||||
|
|
||||||
mod._groups.Add( type == SelectType.Multi
|
mod._groups.Add( type == SelectType.Multi
|
||||||
? new MultiModGroup { Name = newName, Priority = maxPriority }
|
? new MultiModGroup { Name = newName, Priority = maxPriority }
|
||||||
: new SingleModGroup { Name = newName, Priority = maxPriority } );
|
: new SingleModGroup { Name = newName, Priority = maxPriority } );
|
||||||
ModOptionChanged.Invoke( ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, 0 );
|
ModOptionChanged.Invoke( ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteModGroup( Mod2 mod, int groupIdx )
|
public void DeleteModGroup( Mod mod, int groupIdx )
|
||||||
{
|
{
|
||||||
var group = mod._groups[ groupIdx ];
|
var group = mod._groups[ groupIdx ];
|
||||||
mod._groups.RemoveAt( groupIdx );
|
mod._groups.RemoveAt( groupIdx );
|
||||||
group.DeleteFile( BasePath );
|
group.DeleteFile( mod.BasePath );
|
||||||
ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, 0 );
|
ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ChangeGroupDescription( Mod2 mod, int groupIdx, string newDescription )
|
public void MoveModGroup( Mod mod, int groupIdxFrom, int groupIdxTo )
|
||||||
|
{
|
||||||
|
if( mod._groups.Move( groupIdxFrom, groupIdxTo ) )
|
||||||
|
{
|
||||||
|
ModOptionChanged.Invoke( ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeGroupDescription( Mod mod, int groupIdx, string newDescription )
|
||||||
{
|
{
|
||||||
var group = mod._groups[ groupIdx ];
|
var group = mod._groups[ groupIdx ];
|
||||||
if( group.Description == newDescription )
|
if( group.Description == newDescription )
|
||||||
|
|
@ -83,10 +110,10 @@ public sealed partial class Mod2
|
||||||
MultiModGroup m => m.Description = newDescription,
|
MultiModGroup m => m.Description = newDescription,
|
||||||
_ => newDescription,
|
_ => newDescription,
|
||||||
};
|
};
|
||||||
ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, 0 );
|
ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ChangeGroupPriority( Mod2 mod, int groupIdx, int newPriority )
|
public void ChangeGroupPriority( Mod mod, int groupIdx, int newPriority )
|
||||||
{
|
{
|
||||||
var group = mod._groups[ groupIdx ];
|
var group = mod._groups[ groupIdx ];
|
||||||
if( group.Priority == newPriority )
|
if( group.Priority == newPriority )
|
||||||
|
|
@ -100,14 +127,14 @@ public sealed partial class Mod2
|
||||||
MultiModGroup m => m.Priority = newPriority,
|
MultiModGroup m => m.Priority = newPriority,
|
||||||
_ => newPriority,
|
_ => newPriority,
|
||||||
};
|
};
|
||||||
ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, -1 );
|
ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ChangeOptionPriority( Mod2 mod, int groupIdx, int optionIdx, int newPriority )
|
public void ChangeOptionPriority( Mod mod, int groupIdx, int optionIdx, int newPriority )
|
||||||
{
|
{
|
||||||
switch( mod._groups[ groupIdx ] )
|
switch( mod._groups[ groupIdx ] )
|
||||||
{
|
{
|
||||||
case SingleModGroup s:
|
case SingleModGroup:
|
||||||
ChangeGroupPriority( mod, groupIdx, newPriority );
|
ChangeGroupPriority( mod, groupIdx, newPriority );
|
||||||
break;
|
break;
|
||||||
case MultiModGroup m:
|
case MultiModGroup m:
|
||||||
|
|
@ -117,12 +144,12 @@ public sealed partial class Mod2
|
||||||
}
|
}
|
||||||
|
|
||||||
m.PrioritizedOptions[ optionIdx ] = ( m.PrioritizedOptions[ optionIdx ].Mod, newPriority );
|
m.PrioritizedOptions[ optionIdx ] = ( m.PrioritizedOptions[ optionIdx ].Mod, newPriority );
|
||||||
ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx );
|
ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1 );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RenameOption( Mod2 mod, int groupIdx, int optionIdx, string newName )
|
public void RenameOption( Mod mod, int groupIdx, int optionIdx, string newName )
|
||||||
{
|
{
|
||||||
switch( mod._groups[ groupIdx ] )
|
switch( mod._groups[ groupIdx ] )
|
||||||
{
|
{
|
||||||
|
|
@ -145,10 +172,10 @@ public sealed partial class Mod2
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx );
|
ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddOption( Mod2 mod, int groupIdx, string newName )
|
public void AddOption( Mod mod, int groupIdx, string newName )
|
||||||
{
|
{
|
||||||
switch( mod._groups[ groupIdx ] )
|
switch( mod._groups[ groupIdx ] )
|
||||||
{
|
{
|
||||||
|
|
@ -160,10 +187,30 @@ public sealed partial class Mod2
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._groups[ groupIdx ].Count - 1 );
|
ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._groups[ groupIdx ].Count - 1, -1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteOption( Mod2 mod, int groupIdx, int optionIdx )
|
public void AddOption( Mod mod, int groupIdx, ISubMod option, int priority = 0 )
|
||||||
|
{
|
||||||
|
if( option is not SubMod o )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch( mod._groups[ groupIdx ] )
|
||||||
|
{
|
||||||
|
case SingleModGroup s:
|
||||||
|
s.OptionData.Add( o );
|
||||||
|
break;
|
||||||
|
case MultiModGroup m:
|
||||||
|
m.PrioritizedOptions.Add( ( o, priority ) );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._groups[ groupIdx ].Count - 1, -1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteOption( Mod mod, int groupIdx, int optionIdx )
|
||||||
{
|
{
|
||||||
switch( mod._groups[ groupIdx ] )
|
switch( mod._groups[ groupIdx ] )
|
||||||
{
|
{
|
||||||
|
|
@ -175,10 +222,19 @@ public sealed partial class Mod2
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModOptionChanged.Invoke( ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx );
|
ModOptionChanged.Invoke( ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OptionSetManipulation( Mod2 mod, int groupIdx, int optionIdx, MetaManipulation manip, bool delete = false )
|
public void MoveOption( Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo )
|
||||||
|
{
|
||||||
|
var group = mod._groups[ groupIdx ];
|
||||||
|
if( group.MoveOption( optionIdxFrom, optionIdxTo ) )
|
||||||
|
{
|
||||||
|
ModOptionChanged.Invoke( ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OptionSetManipulation( Mod mod, int groupIdx, int optionIdx, MetaManipulation manip, bool delete = false )
|
||||||
{
|
{
|
||||||
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
||||||
if( delete )
|
if( delete )
|
||||||
|
|
@ -206,41 +262,94 @@ public sealed partial class Mod2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx );
|
ModOptionChanged.Invoke( ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OptionSetFile( Mod2 mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath )
|
public void OptionSetManipulations( Mod mod, int groupIdx, int optionIdx, HashSet< MetaManipulation > manipulations )
|
||||||
|
{
|
||||||
|
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
||||||
|
if( subMod.Manipulations.SetEquals( manipulations ) )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
subMod.ManipulationData.Clear();
|
||||||
|
subMod.ManipulationData.UnionWith( manipulations );
|
||||||
|
ModOptionChanged.Invoke( ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OptionSetFile( Mod mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath )
|
||||||
{
|
{
|
||||||
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
||||||
if( OptionSetFile( subMod.FileData, gamePath, newPath ) )
|
if( OptionSetFile( subMod.FileData, gamePath, newPath ) )
|
||||||
{
|
{
|
||||||
ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx );
|
ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OptionSetFileSwap( Mod2 mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath )
|
public void OptionSetFiles( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > replacements )
|
||||||
|
{
|
||||||
|
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
||||||
|
if( subMod.FileData.Equals( replacements ) )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
subMod.FileData.SetTo( replacements );
|
||||||
|
ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OptionSetFileSwap( Mod mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath )
|
||||||
{
|
{
|
||||||
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
||||||
if( OptionSetFile( subMod.FileSwapData, gamePath, newPath ) )
|
if( OptionSetFile( subMod.FileSwapData, gamePath, newPath ) )
|
||||||
{
|
{
|
||||||
ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx );
|
ModOptionChanged.Invoke( ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1 );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool VerifyFileName( Mod2 mod, IModGroup? group, string newName )
|
public void OptionSetFileSwaps( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > swaps )
|
||||||
|
{
|
||||||
|
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
||||||
|
if( subMod.FileSwapData.Equals( swaps ) )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
subMod.FileSwapData.SetTo( swaps );
|
||||||
|
ModOptionChanged.Invoke( ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OptionUpdate( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > replacements,
|
||||||
|
HashSet< MetaManipulation > manipulations, Dictionary< Utf8GamePath, FullPath > swaps )
|
||||||
|
{
|
||||||
|
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
||||||
|
subMod.FileData.SetTo( replacements );
|
||||||
|
subMod.ManipulationData.Clear();
|
||||||
|
subMod.ManipulationData.UnionWith( manipulations );
|
||||||
|
subMod.FileSwapData.SetTo( swaps );
|
||||||
|
ModOptionChanged.Invoke( ModOptionChangeType.OptionUpdated, mod, groupIdx, optionIdx, -1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool VerifyFileName( Mod mod, IModGroup? group, string newName, bool message )
|
||||||
{
|
{
|
||||||
var path = newName.RemoveInvalidPathSymbols();
|
var path = newName.RemoveInvalidPathSymbols();
|
||||||
if( mod.Groups.Any( o => !ReferenceEquals( o, group )
|
if( path.Length == 0
|
||||||
|
|| mod.Groups.Any( o => !ReferenceEquals( o, group )
|
||||||
&& string.Equals( o.Name.RemoveInvalidPathSymbols(), path, StringComparison.InvariantCultureIgnoreCase ) ) )
|
&& string.Equals( o.Name.RemoveInvalidPathSymbols(), path, StringComparison.InvariantCultureIgnoreCase ) ) )
|
||||||
{
|
{
|
||||||
PluginLog.Warning( $"Could not name option {newName} because option with same filename {path} already exists." );
|
if( message )
|
||||||
|
{
|
||||||
|
PluginLog.Warning( $"Could not name option {newName} because option with same filename {path} already exists." );
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SubMod GetSubMod( Mod2 mod, int groupIdx, int optionIdx )
|
private static SubMod GetSubMod( Mod mod, int groupIdx, int optionIdx )
|
||||||
{
|
{
|
||||||
return mod._groups[ groupIdx ] switch
|
return mod._groups[ groupIdx ] switch
|
||||||
{
|
{
|
||||||
|
|
@ -278,7 +387,7 @@ public sealed partial class Mod2
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnModOptionChange( ModOptionChangeType type, Mod2 mod, int groupIdx, int _ )
|
private static void OnModOptionChange( ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2 )
|
||||||
{
|
{
|
||||||
// File deletion is handled in the actual function.
|
// File deletion is handled in the actual function.
|
||||||
if( type != ModOptionChangeType.GroupDeleted )
|
if( type != ModOptionChangeType.GroupDeleted )
|
||||||
|
|
@ -289,10 +398,11 @@ public sealed partial class Mod2
|
||||||
// State can not change on adding groups, as they have no immediate options.
|
// State can not change on adding groups, as they have no immediate options.
|
||||||
mod.HasOptions = type switch
|
mod.HasOptions = type switch
|
||||||
{
|
{
|
||||||
ModOptionChangeType.GroupDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ),
|
ModOptionChangeType.GroupDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ),
|
||||||
ModOptionChangeType.OptionAdded => mod.HasOptions |= mod._groups[ groupIdx ].IsOption,
|
ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any( o => o.IsOption ),
|
||||||
ModOptionChangeType.OptionDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ),
|
ModOptionChangeType.OptionAdded => mod.HasOptions |= mod._groups[ groupIdx ].IsOption,
|
||||||
_ => mod.HasOptions,
|
ModOptionChangeType.OptionDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ),
|
||||||
|
_ => mod.HasOptions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ using Dalamud.Logging;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public sealed partial class Mod2
|
public sealed partial class Mod
|
||||||
{
|
{
|
||||||
public sealed partial class Manager
|
public sealed partial class Manager
|
||||||
{
|
{
|
||||||
|
|
@ -50,7 +50,7 @@ public sealed partial class Mod2
|
||||||
}
|
}
|
||||||
|
|
||||||
BasePath = newDir;
|
BasePath = newDir;
|
||||||
Valid = true;
|
Valid = Directory.Exists( newDir.FullName );
|
||||||
if( Penumbra.Config.ModDirectory != BasePath.FullName )
|
if( Penumbra.Config.ModDirectory != BasePath.FullName )
|
||||||
{
|
{
|
||||||
Penumbra.Config.ModDirectory = BasePath.FullName;
|
Penumbra.Config.ModDirectory = BasePath.FullName;
|
||||||
|
|
@ -4,22 +4,22 @@ using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public sealed partial class Mod2
|
public sealed partial class Mod
|
||||||
{
|
{
|
||||||
public sealed partial class Manager : IEnumerable< Mod2 >
|
public sealed partial class Manager : IEnumerable< Mod >
|
||||||
{
|
{
|
||||||
private readonly List< Mod2 > _mods = new();
|
private readonly List< Mod > _mods = new();
|
||||||
|
|
||||||
public Mod2 this[ Index idx ]
|
public Mod this[ Index idx ]
|
||||||
=> _mods[ idx ];
|
=> _mods[ idx ];
|
||||||
|
|
||||||
public IReadOnlyList< Mod2 > Mods
|
public IReadOnlyList< Mod > Mods
|
||||||
=> _mods;
|
=> _mods;
|
||||||
|
|
||||||
public int Count
|
public int Count
|
||||||
=> _mods.Count;
|
=> _mods.Count;
|
||||||
|
|
||||||
public IEnumerator< Mod2 > GetEnumerator()
|
public IEnumerator< Mod > GetEnumerator()
|
||||||
=> _mods.GetEnumerator();
|
=> _mods.GetEnumerator();
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using Dalamud.Logging;
|
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
|
||||||
|
|
||||||
public partial class Mod2
|
|
||||||
{
|
|
||||||
public partial class Manager
|
|
||||||
{
|
|
||||||
public delegate void ModPathChangeDelegate( ModPathChangeType type, Mod2 mod, DirectoryInfo? oldDirectory,
|
|
||||||
DirectoryInfo? newDirectory );
|
|
||||||
|
|
||||||
public event ModPathChangeDelegate? ModPathChanged;
|
|
||||||
|
|
||||||
public void MoveModDirectory( Index idx, DirectoryInfo newDirectory )
|
|
||||||
{
|
|
||||||
var mod = this[ idx ];
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DeleteMod( Index idx )
|
|
||||||
{
|
|
||||||
var mod = this[ idx ];
|
|
||||||
if( Directory.Exists( mod.BasePath.FullName ) )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete( mod.BasePath.FullName, true );
|
|
||||||
}
|
|
||||||
catch( Exception e )
|
|
||||||
{
|
|
||||||
PluginLog.Error( $"Could not delete the mod {mod.BasePath.Name}:\n{e}" );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// mod.Order.ParentFolder.RemoveMod( mod );
|
|
||||||
// _mods.RemoveAt( idx );
|
|
||||||
//for( var i = idx; i < _mods.Count; ++i )
|
|
||||||
//{
|
|
||||||
// --_mods[i].Index;
|
|
||||||
//}
|
|
||||||
|
|
||||||
ModPathChanged?.Invoke( ModPathChangeType.Deleted, mod, mod.BasePath, null );
|
|
||||||
}
|
|
||||||
|
|
||||||
public Mod2 AddMod( DirectoryInfo modFolder )
|
|
||||||
{
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
//var mod = LoadMod( StructuredMods, modFolder );
|
|
||||||
//if( mod == null )
|
|
||||||
//{
|
|
||||||
// return -1;
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) )
|
|
||||||
//{
|
|
||||||
// if( SetSortOrderPath( mod, sortOrder ) )
|
|
||||||
// {
|
|
||||||
// Config.Save();
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) )
|
|
||||||
//{
|
|
||||||
// return -1;
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//_mods.Add( mod );
|
|
||||||
//ModChange?.Invoke( ChangeType.Added, _mods.Count - 1, mod );
|
|
||||||
//
|
|
||||||
return this[^1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
|
||||||
|
|
||||||
public sealed partial class Mod2
|
|
||||||
{
|
|
||||||
public sealed partial class Manager
|
|
||||||
{
|
|
||||||
public static string ModFileSystemFile
|
|
||||||
=> Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "sort_order.json" );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -10,15 +10,15 @@ public enum ModPathChangeType
|
||||||
Moved,
|
Moved,
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class Mod2
|
public partial class Mod
|
||||||
{
|
{
|
||||||
public DirectoryInfo BasePath { get; private set; }
|
public DirectoryInfo BasePath { get; private set; }
|
||||||
public int Index { get; private set; } = -1;
|
public int Index { get; private set; } = -1;
|
||||||
|
|
||||||
private Mod2( DirectoryInfo basePath )
|
private Mod( DirectoryInfo basePath )
|
||||||
=> BasePath = basePath;
|
=> BasePath = basePath;
|
||||||
|
|
||||||
public static Mod2? LoadMod( DirectoryInfo basePath )
|
public static Mod? LoadMod( DirectoryInfo basePath )
|
||||||
{
|
{
|
||||||
basePath.Refresh();
|
basePath.Refresh();
|
||||||
if( !basePath.Exists )
|
if( !basePath.Exists )
|
||||||
|
|
@ -27,7 +27,7 @@ public partial class Mod2
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var mod = new Mod2( basePath );
|
var mod = new Mod( basePath );
|
||||||
mod.LoadMeta();
|
mod.LoadMeta();
|
||||||
if( mod.Name.Length == 0 )
|
if( mod.Name.Length == 0 )
|
||||||
{
|
{
|
||||||
|
|
@ -3,7 +3,7 @@ using System.Linq;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public sealed partial class Mod2
|
public sealed partial class Mod
|
||||||
{
|
{
|
||||||
public SortedList< string, object? > ChangedItems { get; } = new();
|
public SortedList< string, object? > ChangedItems { get; } = new();
|
||||||
public string LowerChangedItemsString { get; private set; } = string.Empty;
|
public string LowerChangedItemsString { get; private set; } = string.Empty;
|
||||||
154
Penumbra/Mods/Mod.Creation.cs
Normal file
154
Penumbra/Mods/Mod.Creation.cs
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Filesystem;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
|
using Penumbra.Import;
|
||||||
|
|
||||||
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
|
public partial class Mod
|
||||||
|
{
|
||||||
|
// Create and return a new directory based on the given directory and name, that is
|
||||||
|
// - Not Empty
|
||||||
|
// - Unique, by appending (digit) for duplicates.
|
||||||
|
// - Containing no symbols invalid for FFXIV or windows paths.
|
||||||
|
internal static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName )
|
||||||
|
{
|
||||||
|
var name = Path.GetFileNameWithoutExtension( modListName );
|
||||||
|
if( name.Length == 0 )
|
||||||
|
{
|
||||||
|
name = "_";
|
||||||
|
}
|
||||||
|
|
||||||
|
var newModFolderBase = NewOptionDirectory( outDirectory, name );
|
||||||
|
var newModFolder = newModFolderBase.FullName.ObtainUniqueFile();
|
||||||
|
if( newModFolder.Length == 0 )
|
||||||
|
{
|
||||||
|
throw new IOException( "Could not create mod folder: too many folders of the same name exist." );
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory( newModFolder );
|
||||||
|
return new DirectoryInfo( newModFolder );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the name for a group or option subfolder based on its parent folder and given name.
|
||||||
|
// subFolderName should never be empty, and the result is unique and contains no invalid symbols.
|
||||||
|
internal static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName )
|
||||||
|
{
|
||||||
|
var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName );
|
||||||
|
var newModFolder = newModFolderBase.FullName.ObtainUniqueFile();
|
||||||
|
return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the file containing the meta information about a mod from scratch.
|
||||||
|
internal static void CreateMeta( DirectoryInfo directory, string? name, string? author, string? description, string? version,
|
||||||
|
string? website )
|
||||||
|
{
|
||||||
|
var mod = new Mod( directory );
|
||||||
|
mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString( name! );
|
||||||
|
mod.Author = author != null ? new LowerString( author ) : mod.Author;
|
||||||
|
mod.Description = description ?? mod.Description;
|
||||||
|
mod.Version = version ?? mod.Version;
|
||||||
|
mod.Website = website ?? mod.Website;
|
||||||
|
mod.SaveMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a file for an option group from given data.
|
||||||
|
internal static void CreateOptionGroup( DirectoryInfo baseFolder, ModGroup groupData,
|
||||||
|
int priority, string desc, IEnumerable< ISubMod > subMods )
|
||||||
|
{
|
||||||
|
switch( groupData.SelectionType )
|
||||||
|
{
|
||||||
|
case SelectType.Multi:
|
||||||
|
{
|
||||||
|
var group = new MultiModGroup()
|
||||||
|
{
|
||||||
|
Name = groupData.GroupName!,
|
||||||
|
Description = desc,
|
||||||
|
Priority = priority,
|
||||||
|
};
|
||||||
|
group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) );
|
||||||
|
IModGroup.SaveModGroup( group, baseFolder );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SelectType.Single:
|
||||||
|
{
|
||||||
|
var group = new SingleModGroup()
|
||||||
|
{
|
||||||
|
Name = groupData.GroupName!,
|
||||||
|
Description = desc,
|
||||||
|
Priority = priority,
|
||||||
|
};
|
||||||
|
group.OptionData.AddRange( subMods.OfType< SubMod >() );
|
||||||
|
IModGroup.SaveModGroup( group, baseFolder );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the data for a given sub mod from its data and the folder it is based on.
|
||||||
|
internal static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option )
|
||||||
|
{
|
||||||
|
var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories )
|
||||||
|
.Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) )
|
||||||
|
.Where( t => t.Item1 );
|
||||||
|
|
||||||
|
var mod = new SubMod()
|
||||||
|
{
|
||||||
|
Name = option.Name!,
|
||||||
|
};
|
||||||
|
foreach( var (_, gamePath, file) in list )
|
||||||
|
{
|
||||||
|
mod.FileData.TryAdd( gamePath, file );
|
||||||
|
}
|
||||||
|
|
||||||
|
mod.IncorporateMetaChanges( baseFolder, true );
|
||||||
|
return mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the default data file from all unused files that were not handled before
|
||||||
|
// and are used in sub mods.
|
||||||
|
internal static void CreateDefaultFiles( DirectoryInfo directory )
|
||||||
|
{
|
||||||
|
var mod = new Mod( directory );
|
||||||
|
foreach( var file in mod.FindUnusedFiles() )
|
||||||
|
{
|
||||||
|
if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) )
|
||||||
|
{
|
||||||
|
mod._default.FileData.TryAdd( gamePath, file );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod._default.IncorporateMetaChanges( directory, true );
|
||||||
|
mod.SaveDefaultMod();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the name of a new valid directory based on the base directory and the given name.
|
||||||
|
private static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName )
|
||||||
|
=> new(Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) ));
|
||||||
|
|
||||||
|
|
||||||
|
// XIV can not deal with non-ascii symbols in a path,
|
||||||
|
// and the path must obviously be valid itself.
|
||||||
|
private static string ReplaceBadXivSymbols( string s, string replacement = "_" )
|
||||||
|
{
|
||||||
|
StringBuilder sb = new(s.Length);
|
||||||
|
foreach( var c in s )
|
||||||
|
{
|
||||||
|
if( c.IsInvalidAscii() || c.IsInvalidInPath() )
|
||||||
|
{
|
||||||
|
sb.Append( replacement );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append( c );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ using Penumbra.Meta.Manipulations;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public partial class Mod2
|
public partial class Mod
|
||||||
{
|
{
|
||||||
public ISubMod Default
|
public ISubMod Default
|
||||||
=> _default;
|
=> _default;
|
||||||
|
|
@ -6,19 +6,18 @@ using Dalamud.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.Importer;
|
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public sealed partial class Mod2
|
public sealed partial class Mod
|
||||||
{
|
{
|
||||||
private static class Migration
|
private static class Migration
|
||||||
{
|
{
|
||||||
public static bool Migrate( Mod2 mod, JObject json )
|
public static bool Migrate( Mod mod, JObject json )
|
||||||
=> MigrateV0ToV1( mod, json );
|
=> MigrateV0ToV1( mod, json );
|
||||||
|
|
||||||
private static bool MigrateV0ToV1( Mod2 mod, JObject json )
|
private static bool MigrateV0ToV1( Mod mod, JObject json )
|
||||||
{
|
{
|
||||||
if( mod.FileVersion > 0 )
|
if( mod.FileVersion > 0 )
|
||||||
{
|
{
|
||||||
|
|
@ -27,14 +26,15 @@ public sealed partial class Mod2
|
||||||
|
|
||||||
var swaps = json[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >()
|
var swaps = json[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >()
|
||||||
?? new Dictionary< Utf8GamePath, FullPath >();
|
?? new Dictionary< Utf8GamePath, FullPath >();
|
||||||
var groups = json[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >();
|
var groups = json[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >();
|
||||||
var priority = 1;
|
var priority = 1;
|
||||||
|
var seenMetaFiles = new HashSet< FullPath >();
|
||||||
foreach( var group in groups.Values )
|
foreach( var group in groups.Values )
|
||||||
{
|
{
|
||||||
ConvertGroup( mod, group, ref priority );
|
ConvertGroup( mod, group, ref priority, seenMetaFiles );
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach( var unusedFile in mod.FindUnusedFiles() )
|
foreach( var unusedFile in mod.FindUnusedFiles().Where( f => !seenMetaFiles.Contains( f ) ) )
|
||||||
{
|
{
|
||||||
if( unusedFile.ToGamePath( mod.BasePath, out var gamePath )
|
if( unusedFile.ToGamePath( mod.BasePath, out var gamePath )
|
||||||
&& !mod._default.FileData.TryAdd( gamePath, unusedFile ) )
|
&& !mod._default.FileData.TryAdd( gamePath, unusedFile ) )
|
||||||
|
|
@ -61,7 +61,7 @@ public sealed partial class Mod2
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConvertGroup( Mod2 mod, OptionGroupV0 group, ref int priority )
|
private static void ConvertGroup( Mod mod, OptionGroupV0 group, ref int priority, HashSet< FullPath > seenMetaFiles )
|
||||||
{
|
{
|
||||||
if( group.Options.Count == 0 )
|
if( group.Options.Count == 0 )
|
||||||
{
|
{
|
||||||
|
|
@ -82,14 +82,14 @@ public sealed partial class Mod2
|
||||||
mod._groups.Add( newMultiGroup );
|
mod._groups.Add( newMultiGroup );
|
||||||
foreach( var option in group.Options )
|
foreach( var option in group.Options )
|
||||||
{
|
{
|
||||||
newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod.BasePath, option ), optionPriority++ ) );
|
newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod.BasePath, option, seenMetaFiles ), optionPriority++ ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case SelectType.Single:
|
case SelectType.Single:
|
||||||
if( group.Options.Count == 1 )
|
if( group.Options.Count == 1 )
|
||||||
{
|
{
|
||||||
AddFilesToSubMod( mod._default, mod.BasePath, group.Options[ 0 ] );
|
AddFilesToSubMod( mod._default, mod.BasePath, group.Options[ 0 ], seenMetaFiles );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,28 +102,34 @@ public sealed partial class Mod2
|
||||||
mod._groups.Add( newSingleGroup );
|
mod._groups.Add( newSingleGroup );
|
||||||
foreach( var option in group.Options )
|
foreach( var option in group.Options )
|
||||||
{
|
{
|
||||||
newSingleGroup.OptionData.Add( SubModFromOption( mod.BasePath, option ) );
|
newSingleGroup.OptionData.Add( SubModFromOption( mod.BasePath, option, seenMetaFiles ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddFilesToSubMod( SubMod mod, DirectoryInfo basePath, OptionV0 option )
|
private static void AddFilesToSubMod( SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet< FullPath > seenMetaFiles )
|
||||||
{
|
{
|
||||||
foreach( var (relPath, gamePaths) in option.OptionFiles )
|
foreach( var (relPath, gamePaths) in option.OptionFiles )
|
||||||
{
|
{
|
||||||
|
var fullPath = new FullPath( basePath, relPath );
|
||||||
foreach( var gamePath in gamePaths )
|
foreach( var gamePath in gamePaths )
|
||||||
{
|
{
|
||||||
mod.FileData.TryAdd( gamePath, new FullPath( basePath, relPath ) );
|
mod.FileData.TryAdd( gamePath, fullPath );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( fullPath.Extension is ".meta" or ".rgsp" )
|
||||||
|
{
|
||||||
|
seenMetaFiles.Add( fullPath );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SubMod SubModFromOption( DirectoryInfo basePath, OptionV0 option )
|
private static SubMod SubModFromOption( DirectoryInfo basePath, OptionV0 option, HashSet< FullPath > seenMetaFiles )
|
||||||
{
|
{
|
||||||
var subMod = new SubMod() { Name = option.OptionName };
|
var subMod = new SubMod { Name = option.OptionName };
|
||||||
AddFilesToSubMod( subMod, basePath, option );
|
AddFilesToSubMod( subMod, basePath, option, seenMetaFiles );
|
||||||
subMod.IncorporateMetaChanges( basePath, false );
|
subMod.IncorporateMetaChanges( basePath, false );
|
||||||
return subMod;
|
return subMod;
|
||||||
}
|
}
|
||||||
|
|
@ -152,5 +158,45 @@ public sealed partial class Mod2
|
||||||
public OptionGroupV0()
|
public OptionGroupV0()
|
||||||
{ }
|
{ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not used anymore, but required for migration.
|
||||||
|
private class SingleOrArrayConverter< T > : JsonConverter
|
||||||
|
{
|
||||||
|
public override bool CanConvert( Type objectType )
|
||||||
|
=> objectType == typeof( HashSet< T > );
|
||||||
|
|
||||||
|
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
||||||
|
{
|
||||||
|
var token = JToken.Load( reader );
|
||||||
|
|
||||||
|
if( token.Type == JTokenType.Array )
|
||||||
|
{
|
||||||
|
return token.ToObject< HashSet< T > >() ?? new HashSet< T >();
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmp = token.ToObject< T >();
|
||||||
|
return tmp != null
|
||||||
|
? new HashSet< T > { tmp }
|
||||||
|
: new HashSet< T >();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite
|
||||||
|
=> true;
|
||||||
|
|
||||||
|
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
|
||||||
|
{
|
||||||
|
writer.WriteStartArray();
|
||||||
|
if( value != null )
|
||||||
|
{
|
||||||
|
var v = ( HashSet< T > )value;
|
||||||
|
foreach( var val in v )
|
||||||
|
{
|
||||||
|
serializer.Serialize( writer, val?.ToString() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ using Dalamud.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
|
|
@ -20,7 +21,7 @@ public enum MetaChangeType : byte
|
||||||
Migration = 0x40,
|
Migration = 0x40,
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed partial class Mod2
|
public sealed partial class Mod
|
||||||
{
|
{
|
||||||
public const uint CurrentFileVersion = 1;
|
public const uint CurrentFileVersion = 1;
|
||||||
public uint FileVersion { get; private set; } = CurrentFileVersion;
|
public uint FileVersion { get; private set; } = CurrentFileVersion;
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using Penumbra.GameData.ByteString;
|
|
||||||
using Penumbra.Importer.Models;
|
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
|
||||||
|
|
||||||
public partial class Mod2
|
|
||||||
{
|
|
||||||
internal static void CreateMeta( DirectoryInfo directory, string? name, string? author, string? description, string? version,
|
|
||||||
string? website )
|
|
||||||
{
|
|
||||||
var mod = new Mod2( directory );
|
|
||||||
if( name is { Length: 0 } )
|
|
||||||
{
|
|
||||||
mod.Name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( author != null )
|
|
||||||
{
|
|
||||||
mod.Author = author;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( description != null )
|
|
||||||
{
|
|
||||||
mod.Description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( version != null )
|
|
||||||
{
|
|
||||||
mod.Version = version;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( website != null )
|
|
||||||
{
|
|
||||||
mod.Website = website;
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.SaveMeta();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void CreateOptionGroup( DirectoryInfo baseFolder, ModGroup groupData,
|
|
||||||
int priority, string desc, List< ISubMod > subMods )
|
|
||||||
{
|
|
||||||
switch( groupData.SelectionType )
|
|
||||||
{
|
|
||||||
case SelectType.Multi:
|
|
||||||
{
|
|
||||||
var group = new MultiModGroup()
|
|
||||||
{
|
|
||||||
Name = groupData.GroupName!,
|
|
||||||
Description = desc,
|
|
||||||
Priority = priority,
|
|
||||||
};
|
|
||||||
group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) );
|
|
||||||
IModGroup.SaveModGroup( group, baseFolder );
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SelectType.Single:
|
|
||||||
{
|
|
||||||
var group = new SingleModGroup()
|
|
||||||
{
|
|
||||||
Name = groupData.GroupName!,
|
|
||||||
Description = desc,
|
|
||||||
Priority = priority,
|
|
||||||
};
|
|
||||||
group.OptionData.AddRange( subMods.OfType< SubMod >() );
|
|
||||||
IModGroup.SaveModGroup( group, baseFolder );
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option )
|
|
||||||
{
|
|
||||||
var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories )
|
|
||||||
.Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) )
|
|
||||||
.Where( t => t.Item1 );
|
|
||||||
|
|
||||||
var mod = new SubMod()
|
|
||||||
{
|
|
||||||
Name = option.Name!,
|
|
||||||
};
|
|
||||||
foreach( var (_, gamePath, file) in list )
|
|
||||||
{
|
|
||||||
mod.FileData.TryAdd( gamePath, file );
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.IncorporateMetaChanges( baseFolder, true );
|
|
||||||
return mod;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void CreateDefaultFiles( DirectoryInfo directory )
|
|
||||||
{
|
|
||||||
var mod = new Mod2( directory );
|
|
||||||
foreach( var file in mod.FindUnusedFiles() )
|
|
||||||
{
|
|
||||||
if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) )
|
|
||||||
{
|
|
||||||
mod._default.FileData.TryAdd( gamePath, file );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod._default.IncorporateMetaChanges( directory, true );
|
|
||||||
mod.SaveDefaultMod();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,7 +7,7 @@ using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.Importer;
|
using Penumbra.Import;
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using OtterGui.Filesystem;
|
using OtterGui.Filesystem;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public sealed class ModFileSystem : FileSystem< Mod2 >, IDisposable
|
public sealed class ModFileSystem : FileSystem< Mod >, IDisposable
|
||||||
{
|
{
|
||||||
|
public static string ModFileSystemFile
|
||||||
|
=> Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "sort_order.json" );
|
||||||
|
|
||||||
// Save the current sort order.
|
// Save the current sort order.
|
||||||
// Does not save or copy the backup in the current mod directory,
|
// Does not save or copy the backup in the current mod directory,
|
||||||
// as this is done on mod directory changes only.
|
// as this is done on mod directory changes only.
|
||||||
public void Save()
|
public void Save()
|
||||||
=> SaveToFile( new FileInfo( Mod2.Manager.ModFileSystemFile ), SaveMod, true );
|
=> SaveToFile( new FileInfo( ModFileSystemFile ), SaveMod, true );
|
||||||
|
|
||||||
// Create a new ModFileSystem from the currently loaded mods and the current sort order file.
|
// Create a new ModFileSystem from the currently loaded mods and the current sort order file.
|
||||||
public static ModFileSystem Load()
|
public static ModFileSystem Load()
|
||||||
|
|
@ -20,18 +25,24 @@ public sealed class ModFileSystem : FileSystem< Mod2 >, IDisposable
|
||||||
|
|
||||||
ret.Changed += ret.OnChange;
|
ret.Changed += ret.OnChange;
|
||||||
Penumbra.ModManager.ModDiscoveryFinished += ret.Reload;
|
Penumbra.ModManager.ModDiscoveryFinished += ret.Reload;
|
||||||
|
Penumbra.ModManager.ModMetaChanged += ret.OnMetaChange;
|
||||||
|
Penumbra.ModManager.ModPathChanged += ret.OnModPathChange;
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
=> Penumbra.ModManager.ModDiscoveryFinished -= Reload;
|
{
|
||||||
|
Penumbra.ModManager.ModPathChanged -= OnModPathChange;
|
||||||
|
Penumbra.ModManager.ModDiscoveryFinished -= Reload;
|
||||||
|
Penumbra.ModManager.ModMetaChanged -= OnMetaChange;
|
||||||
|
}
|
||||||
|
|
||||||
// Reload the whole filesystem from currently loaded mods and the current sort order file.
|
// Reload the whole filesystem from currently loaded mods and the current sort order file.
|
||||||
// Used on construction and on mod rediscoveries.
|
// Used on construction and on mod rediscoveries.
|
||||||
private void Reload()
|
private void Reload()
|
||||||
{
|
{
|
||||||
if( Load( new FileInfo( Mod2.Manager.ModFileSystemFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) )
|
if( Load( new FileInfo( ModFileSystemFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) )
|
||||||
{
|
{
|
||||||
Save();
|
Save();
|
||||||
}
|
}
|
||||||
|
|
@ -46,17 +57,61 @@ public sealed class ModFileSystem : FileSystem< Mod2 >, IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update sort order when defaulted mod names change.
|
||||||
|
private void OnMetaChange( MetaChangeType type, Mod mod, string? oldName )
|
||||||
|
{
|
||||||
|
if( type.HasFlag( MetaChangeType.Name ) && oldName != null )
|
||||||
|
{
|
||||||
|
var old = oldName.FixName();
|
||||||
|
if( Find( old, out var child ) )
|
||||||
|
{
|
||||||
|
Rename( child, mod.Name.Text );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the filesystem if a mod has been added or removed.
|
||||||
|
// Save it, if the mod directory has been moved, since this will change the save format.
|
||||||
|
private void OnModPathChange( ModPathChangeType type, Mod mod, DirectoryInfo? oldPath, DirectoryInfo? newPath )
|
||||||
|
{
|
||||||
|
switch( type )
|
||||||
|
{
|
||||||
|
case ModPathChangeType.Added:
|
||||||
|
var originalName = mod.Name.Text.FixName();
|
||||||
|
var name = originalName;
|
||||||
|
var counter = 1;
|
||||||
|
while( Find( name, out _ ) )
|
||||||
|
{
|
||||||
|
name = $"{originalName} ({++counter})";
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateLeaf( Root, name, mod );
|
||||||
|
break;
|
||||||
|
case ModPathChangeType.Deleted:
|
||||||
|
var leaf = Root.GetAllDescendants( SortMode.Lexicographical ).OfType< Leaf >().FirstOrDefault( l => l.Value == mod );
|
||||||
|
if( leaf != null )
|
||||||
|
{
|
||||||
|
Delete( leaf );
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ModPathChangeType.Moved:
|
||||||
|
Save();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Used for saving and loading.
|
// Used for saving and loading.
|
||||||
private static string ModToIdentifier( Mod2 mod )
|
private static string ModToIdentifier( Mod mod )
|
||||||
=> mod.BasePath.Name;
|
=> mod.BasePath.Name;
|
||||||
|
|
||||||
private static string ModToName( Mod2 mod )
|
private static string ModToName( Mod mod )
|
||||||
=> mod.Name.Text;
|
=> mod.Name.Text.FixName();
|
||||||
|
|
||||||
private static (string, bool) SaveMod( Mod2 mod, string fullPath )
|
private static (string, bool) SaveMod( Mod mod, string fullPath )
|
||||||
{
|
{
|
||||||
|
var regex = new Regex( $@"^{Regex.Escape( ModToName( mod ) )}( \(\d+\))?" );
|
||||||
// Only save pairs with non-default paths.
|
// Only save pairs with non-default paths.
|
||||||
if( fullPath == ModToName( mod ) )
|
if( regex.IsMatch( fullPath ) )
|
||||||
{
|
{
|
||||||
return ( string.Empty, false );
|
return ( string.Empty, false );
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using OtterGui.Filesystem;
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
@ -76,4 +77,7 @@ public interface IModGroup : IEnumerable< ISubMod >
|
||||||
j.WriteEndArray();
|
j.WriteEndArray();
|
||||||
j.WriteEndObject();
|
j.WriteEndObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IModGroup Convert( SelectType type );
|
||||||
|
public bool MoveOption( int optionIdxFrom, int optionIdxTo );
|
||||||
}
|
}
|
||||||
|
|
@ -5,10 +5,11 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using OtterGui.Filesystem;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public partial class Mod2
|
public partial class Mod
|
||||||
{
|
{
|
||||||
private sealed class MultiModGroup : IModGroup
|
private sealed class MultiModGroup : IModGroup
|
||||||
{
|
{
|
||||||
|
|
@ -63,5 +64,26 @@ public partial class Mod2
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IModGroup Convert( SelectType type )
|
||||||
|
{
|
||||||
|
switch( type )
|
||||||
|
{
|
||||||
|
case SelectType.Multi: return this;
|
||||||
|
case SelectType.Single:
|
||||||
|
var multi = new SingleModGroup()
|
||||||
|
{
|
||||||
|
Name = Name,
|
||||||
|
Description = Description,
|
||||||
|
Priority = Priority,
|
||||||
|
};
|
||||||
|
multi.OptionData.AddRange( PrioritizedOptions.Select( p => p.Mod ) );
|
||||||
|
return multi;
|
||||||
|
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveOption( int optionIdxFrom, int optionIdxTo )
|
||||||
|
=> PrioritizedOptions.Move( optionIdxFrom, optionIdxTo );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,12 +2,14 @@ using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using OtterGui.Filesystem;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public partial class Mod2
|
public partial class Mod
|
||||||
{
|
{
|
||||||
private sealed class SingleModGroup : IModGroup
|
private sealed class SingleModGroup : IModGroup
|
||||||
{
|
{
|
||||||
|
|
@ -62,5 +64,26 @@ public partial class Mod2
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IModGroup Convert( SelectType type )
|
||||||
|
{
|
||||||
|
switch( type )
|
||||||
|
{
|
||||||
|
case SelectType.Single: return this;
|
||||||
|
case SelectType.Multi:
|
||||||
|
var multi = new MultiModGroup()
|
||||||
|
{
|
||||||
|
Name = Name,
|
||||||
|
Description = Description,
|
||||||
|
Priority = Priority,
|
||||||
|
};
|
||||||
|
multi.PrioritizedOptions.AddRange( OptionData.Select( ( o, i ) => ( o, i ) ) );
|
||||||
|
return multi;
|
||||||
|
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveOption( int optionIdxFrom, int optionIdxTo )
|
||||||
|
=> OptionData.Move( optionIdxFrom, optionIdxTo );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,12 +6,12 @@ using Dalamud.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.Importer;
|
using Penumbra.Import;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public partial class Mod2
|
public partial class Mod
|
||||||
{
|
{
|
||||||
private string DefaultFile
|
private string DefaultFile
|
||||||
=> Path.Combine( BasePath.FullName, "default_mod.json" );
|
=> Path.Combine( BasePath.FullName, "default_mod.json" );
|
||||||
|
|
@ -135,31 +135,7 @@ public partial class Mod2
|
||||||
{
|
{
|
||||||
File.Delete( file.FullName );
|
File.Delete( file.FullName );
|
||||||
}
|
}
|
||||||
|
ManipulationData.UnionWith( meta.MetaManipulations );
|
||||||
foreach( var manip in meta.EqpManipulations )
|
|
||||||
{
|
|
||||||
ManipulationData.Add( manip );
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach( var manip in meta.EqdpManipulations )
|
|
||||||
{
|
|
||||||
ManipulationData.Add( manip );
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach( var manip in meta.EstManipulations )
|
|
||||||
{
|
|
||||||
ManipulationData.Add( manip );
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach( var manip in meta.GmpManipulations )
|
|
||||||
{
|
|
||||||
ManipulationData.Add( manip );
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach( var manip in meta.ImcManipulations )
|
|
||||||
{
|
|
||||||
ManipulationData.Add( manip );
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ".rgsp":
|
case ".rgsp":
|
||||||
|
|
@ -174,11 +150,7 @@ public partial class Mod2
|
||||||
{
|
{
|
||||||
File.Delete( file.FullName );
|
File.Delete( file.FullName );
|
||||||
}
|
}
|
||||||
|
ManipulationData.UnionWith( rgsp.MetaManipulations );
|
||||||
foreach( var manip in rgsp.RspManipulations )
|
|
||||||
{
|
|
||||||
ManipulationData.Add( manip );
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default: continue;
|
default: continue;
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using OtterGui.Filesystem;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
|
|
||||||
// Contains the settings for a given mod.
|
// Contains the settings for a given mod.
|
||||||
public class ModSettings2
|
public class ModSettings
|
||||||
{
|
{
|
||||||
public static readonly ModSettings2 Empty = new();
|
public static readonly ModSettings Empty = new();
|
||||||
public List< uint > Settings { get; init; } = new();
|
public List< uint > Settings { get; init; } = new();
|
||||||
public int Priority { get; set; }
|
public int Priority { get; set; }
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
public ModSettings2 DeepCopy()
|
public ModSettings DeepCopy()
|
||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
Enabled = Enabled,
|
Enabled = Enabled,
|
||||||
|
|
@ -21,7 +22,7 @@ public class ModSettings2
|
||||||
Settings = Settings.ToList(),
|
Settings = Settings.ToList(),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static ModSettings2 DefaultSettings( Mod2 mod )
|
public static ModSettings DefaultSettings( Mod mod )
|
||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
Enabled = false,
|
Enabled = false,
|
||||||
|
|
@ -29,19 +30,31 @@ public class ModSettings2
|
||||||
Settings = Enumerable.Repeat( 0u, mod.Groups.Count ).ToList(),
|
Settings = Enumerable.Repeat( 0u, mod.Groups.Count ).ToList(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public bool HandleChanges( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx )
|
||||||
|
|
||||||
public void HandleChanges( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx )
|
|
||||||
{
|
{
|
||||||
switch( type )
|
switch( type )
|
||||||
{
|
{
|
||||||
|
case ModOptionChangeType.GroupRenamed: return true;
|
||||||
case ModOptionChangeType.GroupAdded:
|
case ModOptionChangeType.GroupAdded:
|
||||||
Settings.Insert( groupIdx, 0 );
|
Settings.Insert( groupIdx, 0 );
|
||||||
break;
|
return true;
|
||||||
case ModOptionChangeType.GroupDeleted:
|
case ModOptionChangeType.GroupDeleted:
|
||||||
Settings.RemoveAt( groupIdx );
|
Settings.RemoveAt( groupIdx );
|
||||||
break;
|
return true;
|
||||||
|
case ModOptionChangeType.GroupTypeChanged:
|
||||||
|
{
|
||||||
|
var group = mod.Groups[ groupIdx ];
|
||||||
|
var config = Settings[ groupIdx ];
|
||||||
|
Settings[ groupIdx ] = group.Type switch
|
||||||
|
{
|
||||||
|
SelectType.Single => ( uint )Math.Min( group.Count - 1, BitOperations.TrailingZeroCount( config ) ),
|
||||||
|
SelectType.Multi => 1u << ( int )config,
|
||||||
|
_ => config,
|
||||||
|
};
|
||||||
|
return config != Settings[ groupIdx ];
|
||||||
|
}
|
||||||
case ModOptionChangeType.OptionDeleted:
|
case ModOptionChangeType.OptionDeleted:
|
||||||
|
{
|
||||||
var group = mod.Groups[ groupIdx ];
|
var group = mod.Groups[ groupIdx ];
|
||||||
var config = Settings[ groupIdx ];
|
var config = Settings[ groupIdx ];
|
||||||
Settings[ groupIdx ] = group.Type switch
|
Settings[ groupIdx ] = group.Type switch
|
||||||
|
|
@ -50,20 +63,38 @@ public class ModSettings2
|
||||||
SelectType.Multi => RemoveBit( config, optionIdx ),
|
SelectType.Multi => RemoveBit( config, optionIdx ),
|
||||||
_ => config,
|
_ => config,
|
||||||
};
|
};
|
||||||
break;
|
return config != Settings[ groupIdx ];
|
||||||
|
}
|
||||||
|
case ModOptionChangeType.GroupMoved: return Settings.Move( groupIdx, movedToIdx );
|
||||||
|
case ModOptionChangeType.OptionMoved:
|
||||||
|
{
|
||||||
|
var group = mod.Groups[ groupIdx ];
|
||||||
|
var config = Settings[ groupIdx ];
|
||||||
|
Settings[ groupIdx ] = group.Type switch
|
||||||
|
{
|
||||||
|
SelectType.Single => config == optionIdx ? ( uint )movedToIdx : config,
|
||||||
|
SelectType.Multi => MoveBit( config, optionIdx, movedToIdx ),
|
||||||
|
_ => config,
|
||||||
|
};
|
||||||
|
return config != Settings[ groupIdx ];
|
||||||
|
}
|
||||||
|
default: return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetValue( Mod2 mod, int groupIdx, uint newValue )
|
private static uint FixSetting( IModGroup group, uint value )
|
||||||
|
=> group.Type switch
|
||||||
|
{
|
||||||
|
SelectType.Single => ( uint )Math.Min( value, group.Count - 1 ),
|
||||||
|
SelectType.Multi => ( uint )( value & ( ( 1 << group.Count ) - 1 ) ),
|
||||||
|
_ => value,
|
||||||
|
};
|
||||||
|
|
||||||
|
public void SetValue( Mod mod, int groupIdx, uint newValue )
|
||||||
{
|
{
|
||||||
AddMissingSettings( groupIdx + 1 );
|
AddMissingSettings( groupIdx + 1 );
|
||||||
var group = mod.Groups[ groupIdx ];
|
var group = mod.Groups[ groupIdx ];
|
||||||
Settings[ groupIdx ] = group.Type switch
|
Settings[ groupIdx ] = FixSetting( group, newValue );
|
||||||
{
|
|
||||||
SelectType.Single => ( uint )Math.Max( newValue, group.Count ),
|
|
||||||
SelectType.Multi => ( ( 1u << group.Count ) - 1 ) & newValue,
|
|
||||||
_ => newValue,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint RemoveBit( uint config, int bit )
|
private static uint RemoveBit( uint config, int bit )
|
||||||
|
|
@ -75,6 +106,16 @@ public class ModSettings2
|
||||||
return low | high;
|
return low | high;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static uint MoveBit( uint config, int bit1, int bit2 )
|
||||||
|
{
|
||||||
|
var enabled = ( config & ( 1 << bit1 ) ) != 0 ? 1u << bit2 : 0u;
|
||||||
|
config = RemoveBit( config, bit1 );
|
||||||
|
var lowMask = ( 1u << bit2 ) - 1u;
|
||||||
|
var low = config & lowMask;
|
||||||
|
var high = ( config & ~lowMask ) << 1;
|
||||||
|
return low | enabled | high;
|
||||||
|
}
|
||||||
|
|
||||||
internal bool AddMissingSettings( int totalCount )
|
internal bool AddMissingSettings( int totalCount )
|
||||||
{
|
{
|
||||||
if( totalCount <= Settings.Count )
|
if( totalCount <= Settings.Count )
|
||||||
|
|
@ -100,7 +141,7 @@ public class ModSettings2
|
||||||
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ),
|
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ),
|
||||||
};
|
};
|
||||||
|
|
||||||
public SavedSettings( ModSettings2 settings, Mod2 mod )
|
public SavedSettings( ModSettings settings, Mod mod )
|
||||||
{
|
{
|
||||||
Priority = settings.Priority;
|
Priority = settings.Priority;
|
||||||
Enabled = settings.Enabled;
|
Enabled = settings.Enabled;
|
||||||
|
|
@ -113,7 +154,7 @@ public class ModSettings2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ToSettings( Mod2 mod, out ModSettings2 settings )
|
public bool ToSettings( Mod mod, out ModSettings settings )
|
||||||
{
|
{
|
||||||
var list = new List< uint >( mod.Groups.Count );
|
var list = new List< uint >( mod.Groups.Count );
|
||||||
var changes = Settings.Count != mod.Groups.Count;
|
var changes = Settings.Count != mod.Groups.Count;
|
||||||
|
|
@ -121,7 +162,12 @@ public class ModSettings2
|
||||||
{
|
{
|
||||||
if( Settings.TryGetValue( group.Name, out var config ) )
|
if( Settings.TryGetValue( group.Name, out var config ) )
|
||||||
{
|
{
|
||||||
list.Add( config );
|
var actualConfig = FixSetting( group, config );
|
||||||
|
list.Add( actualConfig );
|
||||||
|
if( actualConfig != config )
|
||||||
|
{
|
||||||
|
changes = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -130,7 +176,7 @@ public class ModSettings2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
settings = new ModSettings2
|
settings = new ModSettings
|
||||||
{
|
{
|
||||||
Enabled = Enabled,
|
Enabled = Enabled,
|
||||||
Priority = Priority,
|
Priority = Priority,
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
public static ResidentResourceManager ResidentResources { get; private set; } = null!;
|
public static ResidentResourceManager ResidentResources { get; private set; } = null!;
|
||||||
public static CharacterUtility CharacterUtility { get; private set; } = null!;
|
public static CharacterUtility CharacterUtility { get; private set; } = null!;
|
||||||
public static MetaFileManager MetaFileManager { get; private set; } = null!;
|
public static MetaFileManager MetaFileManager { get; private set; } = null!;
|
||||||
public static Mod2.Manager ModManager { get; private set; } = null!;
|
public static Mod.Manager ModManager { get; private set; } = null!;
|
||||||
public static ModCollection.Manager CollectionManager { get; private set; } = null!;
|
public static ModCollection.Manager CollectionManager { get; private set; } = null!;
|
||||||
public static SimpleRedirectManager Redirects { get; private set; } = null!;
|
public static SimpleRedirectManager Redirects { get; private set; } = null!;
|
||||||
public static ResourceLoader ResourceLoader { get; private set; } = null!;
|
public static ResourceLoader ResourceLoader { get; private set; } = null!;
|
||||||
|
|
@ -78,7 +78,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
MetaFileManager = new MetaFileManager();
|
MetaFileManager = new MetaFileManager();
|
||||||
ResourceLoader = new ResourceLoader( this );
|
ResourceLoader = new ResourceLoader( this );
|
||||||
ResourceLogger = new ResourceLogger( ResourceLoader );
|
ResourceLogger = new ResourceLogger( ResourceLoader );
|
||||||
ModManager = new Mod2.Manager( Config.ModDirectory );
|
ModManager = new Mod.Manager( Config.ModDirectory );
|
||||||
ModManager.DiscoverMods();
|
ModManager.DiscoverMods();
|
||||||
CollectionManager = new ModCollection.Manager( ModManager );
|
CollectionManager = new ModCollection.Manager( ModManager );
|
||||||
ModFileSystem = ModFileSystem.Load();
|
ModFileSystem = ModFileSystem.Load();
|
||||||
|
|
@ -138,6 +138,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
btn = new LaunchButton( _configWindow );
|
btn = new LaunchButton( _configWindow );
|
||||||
system = new WindowSystem( Name );
|
system = new WindowSystem( Name );
|
||||||
system.AddWindow( _configWindow );
|
system.AddWindow( _configWindow );
|
||||||
|
system.AddWindow( cfg.SubModPopup );
|
||||||
Dalamud.PluginInterface.UiBuilder.Draw += system.Draw;
|
Dalamud.PluginInterface.UiBuilder.Draw += system.Draw;
|
||||||
Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle;
|
Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle;
|
||||||
}
|
}
|
||||||
|
|
@ -294,8 +295,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
case "reload":
|
case "reload":
|
||||||
{
|
{
|
||||||
ModManager.DiscoverMods();
|
ModManager.DiscoverMods();
|
||||||
Dalamud.Chat.Print(
|
Dalamud.Chat.Print( $"Reloaded Penumbra mods. You have {ModManager.Mods.Count} mods."
|
||||||
$"Reloaded Penumbra mods. You have {ModManager.Mods.Count} mods."
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -314,7 +314,8 @@ public class Penumbra : IDalamudPlugin
|
||||||
}
|
}
|
||||||
case "debug":
|
case "debug":
|
||||||
{
|
{
|
||||||
// TODO
|
Config.DebugMode = true;
|
||||||
|
Config.Save();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "enable":
|
case "enable":
|
||||||
|
|
@ -370,7 +371,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
{
|
{
|
||||||
var list = new DirectoryInfo( ModCollection.CollectionDirectory ).EnumerateFiles( "*.json" ).ToList();
|
var list = new DirectoryInfo( ModCollection.CollectionDirectory ).EnumerateFiles( "*.json" ).ToList();
|
||||||
list.Add( Dalamud.PluginInterface.ConfigFile );
|
list.Add( Dalamud.PluginInterface.ConfigFile );
|
||||||
list.Add( new FileInfo( Mod2.Manager.ModFileSystemFile ) );
|
list.Add( new FileInfo( ModFileSystem.ModFileSystemFile ) );
|
||||||
list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) );
|
list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) );
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System.Numerics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
|
using OtterGui.Classes;
|
||||||
using OtterGui.Filesystem;
|
using OtterGui.Filesystem;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
|
|
@ -21,7 +22,7 @@ public partial class ModFileSystemSelector
|
||||||
}
|
}
|
||||||
|
|
||||||
private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase;
|
private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase;
|
||||||
private readonly IReadOnlySet< Mod2 > _newMods = new HashSet< Mod2 >();
|
private readonly IReadOnlySet< Mod > _newMods = new HashSet< Mod >();
|
||||||
private LowerString _modFilter = LowerString.Empty;
|
private LowerString _modFilter = LowerString.Empty;
|
||||||
private int _filterType = -1;
|
private int _filterType = -1;
|
||||||
private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods;
|
private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods;
|
||||||
|
|
@ -75,7 +76,7 @@ public partial class ModFileSystemSelector
|
||||||
// Folders have default state and are filtered out on the direct string instead of the other options.
|
// Folders have default state and are filtered out on the direct string instead of the other options.
|
||||||
// If any filter is set, they should be hidden by default unless their children are visible,
|
// If any filter is set, they should be hidden by default unless their children are visible,
|
||||||
// or they contain the path search string.
|
// or they contain the path search string.
|
||||||
protected override bool ApplyFiltersAndState( FileSystem< Mod2 >.IPath path, out ModState state )
|
protected override bool ApplyFiltersAndState( FileSystem< Mod >.IPath path, out ModState state )
|
||||||
{
|
{
|
||||||
if( path is ModFileSystem.Folder f )
|
if( path is ModFileSystem.Folder f )
|
||||||
{
|
{
|
||||||
|
|
@ -88,7 +89,7 @@ public partial class ModFileSystemSelector
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the string filters.
|
// Apply the string filters.
|
||||||
private bool ApplyStringFilters( ModFileSystem.Leaf leaf, Mod2 mod )
|
private bool ApplyStringFilters( ModFileSystem.Leaf leaf, Mod mod )
|
||||||
{
|
{
|
||||||
return _filterType switch
|
return _filterType switch
|
||||||
{
|
{
|
||||||
|
|
@ -102,7 +103,7 @@ public partial class ModFileSystemSelector
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only get the text color for a mod if no filters are set.
|
// Only get the text color for a mod if no filters are set.
|
||||||
private uint GetTextColor( Mod2 mod, ModSettings2? settings, ModCollection collection )
|
private uint GetTextColor( Mod mod, ModSettings? settings, ModCollection collection )
|
||||||
{
|
{
|
||||||
if( _newMods.Contains( mod ) )
|
if( _newMods.Contains( mod ) )
|
||||||
{
|
{
|
||||||
|
|
@ -119,7 +120,7 @@ public partial class ModFileSystemSelector
|
||||||
return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedDisabledMod.Value() : ColorId.DisabledMod.Value();
|
return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedDisabledMod.Value() : ColorId.DisabledMod.Value();
|
||||||
}
|
}
|
||||||
|
|
||||||
var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ).ToList();
|
var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index );
|
||||||
if( conflicts.Count == 0 )
|
if( conflicts.Count == 0 )
|
||||||
{
|
{
|
||||||
return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedMod.Value() : ColorId.EnabledMod.Value();
|
return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedMod.Value() : ColorId.EnabledMod.Value();
|
||||||
|
|
@ -130,7 +131,7 @@ public partial class ModFileSystemSelector
|
||||||
: ColorId.HandledConflictMod.Value();
|
: ColorId.HandledConflictMod.Value();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CheckStateFilters( Mod2 mod, ModSettings2? settings, ModCollection collection, ref ModState state )
|
private bool CheckStateFilters( Mod mod, ModSettings? settings, ModCollection collection, ref ModState state )
|
||||||
{
|
{
|
||||||
var isNew = _newMods.Contains( mod );
|
var isNew = _newMods.Contains( mod );
|
||||||
// Handle mod details.
|
// Handle mod details.
|
||||||
|
|
@ -188,7 +189,7 @@ public partial class ModFileSystemSelector
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conflicts can only be relevant if the mod is enabled.
|
// Conflicts can only be relevant if the mod is enabled.
|
||||||
var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ).ToList();
|
var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index );
|
||||||
if( conflicts.Count > 0 )
|
if( conflicts.Count > 0 )
|
||||||
{
|
{
|
||||||
if( conflicts.Any( c => !c.Solved ) )
|
if( conflicts.Any( c => !c.Solved ) )
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,30 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
|
using Dalamud.Logging;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Filesystem;
|
using OtterGui.Filesystem;
|
||||||
using OtterGui.FileSystem.Selector;
|
using OtterGui.FileSystem.Selector;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.Import;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
|
|
||||||
namespace Penumbra.UI.Classes;
|
namespace Penumbra.UI.Classes;
|
||||||
|
|
||||||
public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, ModFileSystemSelector.ModState >
|
public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, ModFileSystemSelector.ModState >
|
||||||
{
|
{
|
||||||
public ModSettings2 SelectedSettings { get; private set; } = ModSettings2.Empty;
|
private readonly FileDialogManager _fileManager = new();
|
||||||
|
private TexToolsImporter? _import;
|
||||||
|
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
|
||||||
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
|
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
|
||||||
|
|
||||||
public ModFileSystemSelector( ModFileSystem fileSystem, IReadOnlySet< Mod2 > newMods )
|
public ModFileSystemSelector( ModFileSystem fileSystem, IReadOnlySet< Mod > newMods )
|
||||||
: base( fileSystem )
|
: base( fileSystem )
|
||||||
{
|
{
|
||||||
_newMods = newMods;
|
_newMods = newMods;
|
||||||
|
|
@ -26,6 +33,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
|
||||||
SubscribeRightClickFolder( InheritDescendants, 15 );
|
SubscribeRightClickFolder( InheritDescendants, 15 );
|
||||||
SubscribeRightClickFolder( OwnDescendants, 15 );
|
SubscribeRightClickFolder( OwnDescendants, 15 );
|
||||||
AddButton( AddNewModButton, 0 );
|
AddButton( AddNewModButton, 0 );
|
||||||
|
AddButton( AddImportModButton, 1 );
|
||||||
AddButton( DeleteModButton, 1000 );
|
AddButton( DeleteModButton, 1000 );
|
||||||
SetFilterTooltip();
|
SetFilterTooltip();
|
||||||
|
|
||||||
|
|
@ -33,6 +41,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
|
||||||
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange;
|
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange;
|
||||||
Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange;
|
Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange;
|
||||||
Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange;
|
Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange;
|
||||||
|
Penumbra.ModManager.ModMetaChanged += OnModMetaChange;
|
||||||
Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection;
|
Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection;
|
||||||
Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection;
|
Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection;
|
||||||
OnCollectionChange( ModCollection.Type.Current, null, Penumbra.CollectionManager.Current, null );
|
OnCollectionChange( ModCollection.Type.Current, null, Penumbra.CollectionManager.Current, null );
|
||||||
|
|
@ -43,6 +52,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
|
||||||
base.Dispose();
|
base.Dispose();
|
||||||
Penumbra.ModManager.ModDiscoveryStarted -= StoreCurrentSelection;
|
Penumbra.ModManager.ModDiscoveryStarted -= StoreCurrentSelection;
|
||||||
Penumbra.ModManager.ModDiscoveryFinished -= RestoreLastSelection;
|
Penumbra.ModManager.ModDiscoveryFinished -= RestoreLastSelection;
|
||||||
|
Penumbra.ModManager.ModMetaChanged -= OnModMetaChange;
|
||||||
Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange;
|
Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange;
|
||||||
Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange;
|
Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange;
|
||||||
Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange;
|
Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange;
|
||||||
|
|
@ -64,10 +74,11 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
|
||||||
protected override uint FolderLineColor
|
protected override uint FolderLineColor
|
||||||
=> ColorId.FolderLine.Value();
|
=> ColorId.FolderLine.Value();
|
||||||
|
|
||||||
protected override void DrawLeafName( FileSystem< Mod2 >.Leaf leaf, in ModState state, bool selected )
|
protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected )
|
||||||
{
|
{
|
||||||
var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags;
|
var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags;
|
||||||
using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color );
|
using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color );
|
||||||
|
using var id = ImRaii.PushId( leaf.Value.Index );
|
||||||
using var _ = ImRaii.TreeNode( leaf.Value.Name, flags );
|
using var _ = ImRaii.TreeNode( leaf.Value.Name, flags );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,17 +118,90 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
|
||||||
|
|
||||||
|
|
||||||
// Add custom buttons.
|
// Add custom buttons.
|
||||||
private static void AddNewModButton( Vector2 size )
|
private string _newModName = string.Empty;
|
||||||
|
|
||||||
|
private void AddNewModButton( Vector2 size )
|
||||||
{
|
{
|
||||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", false, true ) )
|
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", !Penumbra.ModManager.Valid, true ) )
|
||||||
{ }
|
{
|
||||||
|
ImGui.OpenPopup( "Create New Mod" );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( ImGuiUtil.OpenNameField( "Create New Mod", ref _newModName ) )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName );
|
||||||
|
Mod.CreateMeta( newDir, _newModName, string.Empty, string.Empty, "1.0", string.Empty );
|
||||||
|
Penumbra.ModManager.AddMod( newDir );
|
||||||
|
_newModName = string.Empty;
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
PluginLog.Error( $"Could not create directory for new Mod {_newModName}:\n{e}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add an import mods button that opens a file selector.
|
||||||
|
private void AddImportModButton( Vector2 size )
|
||||||
|
{
|
||||||
|
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size,
|
||||||
|
"Import one or multiple mods from Tex Tools Mod Pack Files.", !Penumbra.ModManager.Valid, true ) )
|
||||||
|
{
|
||||||
|
_fileManager.OpenFileDialog( "Import Mod Pack", "TexTools Mod Packs{.ttmp,.ttmp2}", ( s, f ) =>
|
||||||
|
{
|
||||||
|
if( s )
|
||||||
|
{
|
||||||
|
_import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ) );
|
||||||
|
ImGui.OpenPopup( "Import Status" );
|
||||||
|
}
|
||||||
|
}, 0, Penumbra.Config.ModDirectory );
|
||||||
|
}
|
||||||
|
|
||||||
|
_fileManager.Draw();
|
||||||
|
DrawInfoPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the progress information for import.
|
||||||
|
private void DrawInfoPopup()
|
||||||
|
{
|
||||||
|
var display = ImGui.GetIO().DisplaySize;
|
||||||
|
ImGui.SetNextWindowSize( display / 4 );
|
||||||
|
ImGui.SetNextWindowPos( 3 * display / 8 );
|
||||||
|
using var popup = ImRaii.Popup( "Import Status", ImGuiWindowFlags.Modal );
|
||||||
|
if( _import != null && popup.Success )
|
||||||
|
{
|
||||||
|
_import.DrawProgressInfo( ImGuiHelpers.ScaledVector2( -1, ImGui.GetFrameHeight() ) );
|
||||||
|
if( _import.State == ImporterState.Done )
|
||||||
|
{
|
||||||
|
ImGui.SetCursorPosY( ImGui.GetWindowHeight() - ImGui.GetFrameHeight() * 2 );
|
||||||
|
if( ImGui.Button( "Close", -Vector2.UnitX ) )
|
||||||
|
{
|
||||||
|
_import = null;
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeleteModButton( Vector2 size )
|
private void DeleteModButton( Vector2 size )
|
||||||
{
|
{
|
||||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size,
|
var keys = ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyShift;
|
||||||
"Delete the currently selected mod entirely from your drive.", SelectedLeaf == null, true ) )
|
var tt = SelectedLeaf == null
|
||||||
{ }
|
? "No mod selected."
|
||||||
|
: "Delete the currently selected mod entirely from your drive.\n"
|
||||||
|
+ "This can not be undone.";
|
||||||
|
if( !keys )
|
||||||
|
{
|
||||||
|
tt += "\nHold Control and Shift while clicking to delete the mod.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true )
|
||||||
|
&& Selected != null )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.DeleteMod( Selected.Index );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -146,6 +230,17 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnModMetaChange( MetaChangeType type, Mod mod, string? oldName )
|
||||||
|
{
|
||||||
|
switch( type )
|
||||||
|
{
|
||||||
|
case MetaChangeType.Name:
|
||||||
|
case MetaChangeType.Author:
|
||||||
|
SetFilterDirty();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnInheritanceChange( bool _ )
|
private void OnInheritanceChange( bool _ )
|
||||||
{
|
{
|
||||||
SetFilterDirty();
|
SetFilterDirty();
|
||||||
|
|
@ -175,17 +270,17 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
|
||||||
OnSelectionChange( Selected, Selected, default );
|
OnSelectionChange( Selected, Selected, default );
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSelectionChange( Mod2? _1, Mod2? newSelection, in ModState _2 )
|
private void OnSelectionChange( Mod? _1, Mod? newSelection, in ModState _2 )
|
||||||
{
|
{
|
||||||
if( newSelection == null )
|
if( newSelection == null )
|
||||||
{
|
{
|
||||||
SelectedSettings = ModSettings2.Empty;
|
SelectedSettings = ModSettings.Empty;
|
||||||
SelectedSettingCollection = ModCollection.Empty;
|
SelectedSettingCollection = ModCollection.Empty;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
( var settings, SelectedSettingCollection ) = Penumbra.CollectionManager.Current[ newSelection.Index ];
|
( var settings, SelectedSettingCollection ) = Penumbra.CollectionManager.Current[ newSelection.Index ];
|
||||||
SelectedSettings = settings ?? ModSettings2.Empty;
|
SelectedSettings = settings ?? ModSettings.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
225
Penumbra/UI/Classes/SubModEditWindow.cs
Normal file
225
Penumbra/UI/Classes/SubModEditWindow.cs
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Interface.Windowing;
|
||||||
|
using ImGuiNET;
|
||||||
|
using OtterGui.Raii;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
|
using Penumbra.Meta.Manipulations;
|
||||||
|
using Penumbra.Mods;
|
||||||
|
using Penumbra.Util;
|
||||||
|
|
||||||
|
namespace Penumbra.UI.Classes;
|
||||||
|
|
||||||
|
public class SubModEditWindow : Window
|
||||||
|
{
|
||||||
|
private const string WindowBaseLabel = "###SubModEdit";
|
||||||
|
private Mod? _mod;
|
||||||
|
private int _groupIdx = -1;
|
||||||
|
private int _optionIdx = -1;
|
||||||
|
private IModGroup? _group;
|
||||||
|
private ISubMod? _subMod;
|
||||||
|
private readonly List< FilePathInfo > _availableFiles = new();
|
||||||
|
|
||||||
|
private readonly struct FilePathInfo
|
||||||
|
{
|
||||||
|
public readonly FullPath File;
|
||||||
|
public readonly Utf8RelPath RelFile;
|
||||||
|
public readonly long Size;
|
||||||
|
public readonly List< (int, int, Utf8GamePath) > SubMods;
|
||||||
|
|
||||||
|
public FilePathInfo( FileInfo file, Mod mod )
|
||||||
|
{
|
||||||
|
File = new FullPath( file );
|
||||||
|
RelFile = Utf8RelPath.FromFile( File, mod.BasePath, out var f ) ? f : Utf8RelPath.Empty;
|
||||||
|
Size = file.Length;
|
||||||
|
SubMods = new List< (int, int, Utf8GamePath) >();
|
||||||
|
var path = File;
|
||||||
|
foreach( var (group, groupIdx) in mod.Groups.WithIndex() )
|
||||||
|
{
|
||||||
|
foreach( var (subMod, optionIdx) in group.WithIndex() )
|
||||||
|
{
|
||||||
|
SubMods.AddRange( subMod.Files.Where( kvp => kvp.Value.Equals( path ) ).Select( kvp => ( groupIdx, optionIdx, kvp.Key ) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SubMods.AddRange( mod.Default.Files.Where( kvp => kvp.Value.Equals( path ) ).Select( kvp => (-1, 0, kvp.Key) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly HashSet< MetaManipulation > _manipulations = new();
|
||||||
|
private readonly Dictionary< Utf8GamePath, FullPath > _files = new();
|
||||||
|
private readonly Dictionary< Utf8GamePath, FullPath > _fileSwaps = new();
|
||||||
|
|
||||||
|
public void Activate( Mod mod, int groupIdx, int optionIdx )
|
||||||
|
{
|
||||||
|
IsOpen = true;
|
||||||
|
_mod = mod;
|
||||||
|
_groupIdx = groupIdx;
|
||||||
|
_group = groupIdx >= 0 ? mod.Groups[ groupIdx ] : null;
|
||||||
|
_optionIdx = optionIdx;
|
||||||
|
_subMod = groupIdx >= 0 ? _group![ optionIdx ] : _mod.Default;
|
||||||
|
_availableFiles.Clear();
|
||||||
|
_availableFiles.AddRange( mod.BasePath.EnumerateDirectories()
|
||||||
|
.SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||||
|
.Select( f => new FilePathInfo( f, _mod ) ) );
|
||||||
|
|
||||||
|
_manipulations.Clear();
|
||||||
|
_manipulations.UnionWith( _subMod.Manipulations );
|
||||||
|
_files.SetTo( _subMod.Files );
|
||||||
|
_fileSwaps.SetTo( _subMod.FileSwaps );
|
||||||
|
|
||||||
|
WindowName = $"{_mod.Name}: {(_group != null ? $"{_group.Name} - " : string.Empty)}{_subMod.Name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool DrawConditions()
|
||||||
|
=> _subMod != null;
|
||||||
|
|
||||||
|
public override void Draw()
|
||||||
|
{
|
||||||
|
using var tabBar = ImRaii.TabBar( "##tabs" );
|
||||||
|
if( !tabBar )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawFileTab();
|
||||||
|
DrawMetaTab();
|
||||||
|
DrawSwapTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
if( _mod != null )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.OptionUpdate( _mod, _groupIdx, _optionIdx, _files, _manipulations, _fileSwaps );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnClose()
|
||||||
|
{
|
||||||
|
_subMod = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawFileTab()
|
||||||
|
{
|
||||||
|
using var tab = ImRaii.TabItem( "File Redirections" );
|
||||||
|
if( !tab )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var list = ImRaii.Table( "##files", 3 );
|
||||||
|
if( !list )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach( var file in _availableFiles )
|
||||||
|
{
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ConfigWindow.Text( file.RelFile.Path );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text( file.Size.ToString() );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
if( file.SubMods.Count == 0 )
|
||||||
|
{
|
||||||
|
ImGui.Text( "Unused" );
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach( var (groupIdx, optionIdx, gamePath) in file.SubMods )
|
||||||
|
{
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
var group = groupIdx >= 0 ? _mod!.Groups[ groupIdx ] : null;
|
||||||
|
var option = groupIdx >= 0 ? group![ optionIdx ] : _mod!.Default;
|
||||||
|
var text = groupIdx >= 0
|
||||||
|
? $"{group!.Name} - {option.Name}"
|
||||||
|
: option.Name;
|
||||||
|
ImGui.Text( text );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ConfigWindow.Text( gamePath.Path );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
foreach( var (gamePath, fullPath) in _files )
|
||||||
|
{
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ConfigWindow.Text( gamePath.Path );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text( fullPath.FullName );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMetaTab()
|
||||||
|
{
|
||||||
|
using var tab = ImRaii.TabItem( "Meta Manipulations" );
|
||||||
|
if( !tab )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var list = ImRaii.Table( "##meta", 3 );
|
||||||
|
if( !list )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach( var manip in _manipulations )
|
||||||
|
{
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text( manip.ManipulationType.ToString() );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text( manip.ManipulationType switch
|
||||||
|
{
|
||||||
|
MetaManipulation.Type.Imc => manip.Imc.ToString(),
|
||||||
|
MetaManipulation.Type.Eqdp => manip.Eqdp.ToString(),
|
||||||
|
MetaManipulation.Type.Eqp => manip.Eqp.ToString(),
|
||||||
|
MetaManipulation.Type.Est => manip.Est.ToString(),
|
||||||
|
MetaManipulation.Type.Gmp => manip.Gmp.ToString(),
|
||||||
|
MetaManipulation.Type.Rsp => manip.Rsp.ToString(),
|
||||||
|
_ => string.Empty,
|
||||||
|
} );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text( manip.ManipulationType switch
|
||||||
|
{
|
||||||
|
MetaManipulation.Type.Imc => manip.Imc.Entry.ToString(),
|
||||||
|
MetaManipulation.Type.Eqdp => manip.Eqdp.Entry.ToString(),
|
||||||
|
MetaManipulation.Type.Eqp => manip.Eqp.Entry.ToString(),
|
||||||
|
MetaManipulation.Type.Est => manip.Est.Entry.ToString(),
|
||||||
|
MetaManipulation.Type.Gmp => manip.Gmp.Entry.ToString(),
|
||||||
|
MetaManipulation.Type.Rsp => manip.Rsp.Entry.ToString(),
|
||||||
|
_ => string.Empty,
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawSwapTab()
|
||||||
|
{
|
||||||
|
using var tab = ImRaii.TabItem( "File Swaps" );
|
||||||
|
if( !tab )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var list = ImRaii.Table( "##swaps", 3 );
|
||||||
|
if( !list )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach( var (from, to) in _fileSwaps )
|
||||||
|
{
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ConfigWindow.Text( from.Path );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text( to.FullName );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SubModEditWindow()
|
||||||
|
: base( WindowBaseLabel )
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
|
using OtterGui.Classes;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
|
|
||||||
namespace Penumbra.UI;
|
namespace Penumbra.UI;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using System.Numerics;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
|
using OtterGui.Classes;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
|
|
|
||||||
444
Penumbra/UI/ConfigWindow.ModPanel.Edit.cs
Normal file
444
Penumbra/UI/ConfigWindow.ModPanel.Edit.cs
Normal file
|
|
@ -0,0 +1,444 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using ImGuiNET;
|
||||||
|
using OtterGui;
|
||||||
|
using OtterGui.Raii;
|
||||||
|
using Penumbra.Mods;
|
||||||
|
|
||||||
|
namespace Penumbra.UI;
|
||||||
|
|
||||||
|
public partial class ConfigWindow
|
||||||
|
{
|
||||||
|
private partial class ModPanel
|
||||||
|
{
|
||||||
|
public readonly Queue< Action > _delayedActions = new();
|
||||||
|
|
||||||
|
private void DrawAddOptionGroupInput()
|
||||||
|
{
|
||||||
|
ImGui.SetNextItemWidth( _window._inputTextWidth.X );
|
||||||
|
ImGui.InputTextWithHint( "##newGroup", "Add new option group...", ref _newGroupName, 256 );
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
var nameValid = Mod.Manager.VerifyFileName( _mod, null, _newGroupName, false );
|
||||||
|
var tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name.";
|
||||||
|
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
|
||||||
|
tt, !nameValid, true ) )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.AddModGroup( _mod, SelectType.Single, _newGroupName );
|
||||||
|
_newGroupName = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 _cellPadding = Vector2.Zero;
|
||||||
|
private Vector2 _itemSpacing = Vector2.Zero;
|
||||||
|
|
||||||
|
private void DrawEditModTab()
|
||||||
|
{
|
||||||
|
using var tab = DrawTab( EditModTabHeader, Tabs.Edit );
|
||||||
|
if( !tab )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var child = ImRaii.Child( "##editChild", -Vector2.One );
|
||||||
|
if( !child )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cellPadding = ImGui.GetStyle().CellPadding with { X = 2 * ImGuiHelpers.GlobalScale };
|
||||||
|
_itemSpacing = ImGui.GetStyle().CellPadding with { X = 4 * ImGuiHelpers.GlobalScale };
|
||||||
|
|
||||||
|
EditRegularMeta();
|
||||||
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
|
|
||||||
|
if( TextInput( "Mod Path", PathFieldIdx, NoFieldIdx, _leaf.FullName(), out var newPath, 256, _window._inputTextWidth.X ) )
|
||||||
|
{
|
||||||
|
_window._penumbra.ModFileSystem.RenameAndMove( _leaf, newPath );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
|
DrawAddOptionGroupInput();
|
||||||
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
|
|
||||||
|
for( var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx )
|
||||||
|
{
|
||||||
|
EditGroup( groupIdx );
|
||||||
|
}
|
||||||
|
|
||||||
|
EndActions();
|
||||||
|
EditDescriptionPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Special field indices to reuse the same string buffer.
|
||||||
|
private const int NoFieldIdx = -1;
|
||||||
|
private const int NameFieldIdx = -2;
|
||||||
|
private const int AuthorFieldIdx = -3;
|
||||||
|
private const int VersionFieldIdx = -4;
|
||||||
|
private const int WebsiteFieldIdx = -5;
|
||||||
|
private const int PathFieldIdx = -6;
|
||||||
|
private const int DescriptionFieldIdx = -7;
|
||||||
|
|
||||||
|
private void EditRegularMeta()
|
||||||
|
{
|
||||||
|
if( TextInput( "Name", NameFieldIdx, NoFieldIdx, _mod.Name, out var newName, 256, _window._inputTextWidth.X ) )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.ChangeModName( _mod.Index, newName );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( TextInput( "Author", AuthorFieldIdx, NoFieldIdx, _mod.Author, out var newAuthor, 256, _window._inputTextWidth.X ) )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.ChangeModAuthor( _mod.Index, newAuthor );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( TextInput( "Version", VersionFieldIdx, NoFieldIdx, _mod.Version, out var newVersion, 32, _window._inputTextWidth.X ) )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.ChangeModVersion( _mod.Index, newVersion );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( TextInput( "Website", WebsiteFieldIdx, NoFieldIdx, _mod.Website, out var newWebsite, 256, _window._inputTextWidth.X ) )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.ChangeModWebsite( _mod.Index, newWebsite );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( ImGui.Button( "Edit Description", _window._inputTextWidth ) )
|
||||||
|
{
|
||||||
|
_delayedActions.Enqueue( () => OpenEditDescriptionPopup( DescriptionFieldIdx ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( ImGui.Button( "Edit Default Mod", _window._inputTextWidth ) )
|
||||||
|
{
|
||||||
|
_window.SubModPopup.Activate( _mod, -1, 0 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Temporary strings
|
||||||
|
private string? _currentEdit;
|
||||||
|
private int? _currentGroupPriority;
|
||||||
|
private int _currentField = -1;
|
||||||
|
private int _optionIndex = -1;
|
||||||
|
|
||||||
|
private string _newGroupName = string.Empty;
|
||||||
|
private string _newOptionName = string.Empty;
|
||||||
|
private string _newDescription = string.Empty;
|
||||||
|
private int _newDescriptionIdx = -1;
|
||||||
|
|
||||||
|
private void EditGroup( int groupIdx )
|
||||||
|
{
|
||||||
|
var group = _mod.Groups[ groupIdx ];
|
||||||
|
using var id = ImRaii.PushId( groupIdx );
|
||||||
|
using var frame = ImRaii.FramedGroup( $"Group #{groupIdx + 1}" );
|
||||||
|
|
||||||
|
using var style = ImRaii.PushStyle( ImGuiStyleVar.CellPadding, _cellPadding )
|
||||||
|
.Push( ImGuiStyleVar.ItemSpacing, _itemSpacing );
|
||||||
|
|
||||||
|
if( TextInput( "##Name", groupIdx, NoFieldIdx, group.Name, out var newGroupName, 256, _window._inputTextWidth.X ) )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.RenameModGroup( _mod, groupIdx, newGroupName );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiUtil.HoverTooltip( "Group Name" );
|
||||||
|
ImGui.SameLine();
|
||||||
|
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
|
||||||
|
"Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) )
|
||||||
|
{
|
||||||
|
_delayedActions.Enqueue( () => Penumbra.ModManager.DeleteModGroup( _mod, groupIdx ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
|
||||||
|
"Edit group description.", false, true ) )
|
||||||
|
{
|
||||||
|
_delayedActions.Enqueue( () => OpenEditDescriptionPopup( groupIdx ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
if( PriorityInput( "##Priority", groupIdx, NoFieldIdx, group.Priority, out var priority, 50 * ImGuiHelpers.GlobalScale ) )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.ChangeGroupPriority( _mod, groupIdx, priority );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiUtil.HoverTooltip( "Group Priority" );
|
||||||
|
|
||||||
|
ImGui.SetNextItemWidth( _window._inputTextWidth.X - 2 * ImGui.GetFrameHeight() - 8 * ImGuiHelpers.GlobalScale );
|
||||||
|
using( var combo = ImRaii.Combo( "##GroupType", GroupTypeName( group.Type ) ) )
|
||||||
|
{
|
||||||
|
if( combo )
|
||||||
|
{
|
||||||
|
foreach( var type in new[] { SelectType.Single, SelectType.Multi } )
|
||||||
|
{
|
||||||
|
if( ImGui.Selectable( GroupTypeName( type ), group.Type == type ) )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, type );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}.";
|
||||||
|
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.ArrowUp.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
|
||||||
|
tt, groupIdx == 0, true ) )
|
||||||
|
{
|
||||||
|
_delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx - 1 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
tt = groupIdx == _mod.Groups.Count - 1
|
||||||
|
? "Can not move this group further downwards."
|
||||||
|
: $"Move this group down to group {groupIdx + 2}.";
|
||||||
|
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.ArrowDown.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
|
||||||
|
tt, groupIdx == _mod.Groups.Count - 1, true ) )
|
||||||
|
{
|
||||||
|
_delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx + 1 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
|
|
||||||
|
using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.SizingFixedFit );
|
||||||
|
ImGui.TableSetupColumn( "idx", ImGuiTableColumnFlags.WidthFixed, 60 * ImGuiHelpers.GlobalScale );
|
||||||
|
ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed, _window._inputTextWidth.X - 62 * ImGuiHelpers.GlobalScale );
|
||||||
|
ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight() );
|
||||||
|
ImGui.TableSetupColumn( "edit", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight() );
|
||||||
|
ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale );
|
||||||
|
if( table )
|
||||||
|
{
|
||||||
|
for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx )
|
||||||
|
{
|
||||||
|
EditOption( group, groupIdx, optionIdx );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.SetNextItemWidth( -1 );
|
||||||
|
ImGui.InputTextWithHint( "##newOption", "Add new option...", ref _newOptionName, 256 );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
|
||||||
|
"Add a new option to this group.", _newOptionName.Length == 0, true ) )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.AddOption( _mod, groupIdx, _newOptionName );
|
||||||
|
_newOptionName = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GroupTypeName( SelectType type )
|
||||||
|
=> type switch
|
||||||
|
{
|
||||||
|
SelectType.Single => "Single Group",
|
||||||
|
SelectType.Multi => "Multi Group",
|
||||||
|
_ => "Unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
private int _dragDropGroupIdx = -1;
|
||||||
|
private int _dragDropOptionIdx = -1;
|
||||||
|
|
||||||
|
private void OptionDragDrop( IModGroup group, int groupIdx, int optionIdx )
|
||||||
|
{
|
||||||
|
const string label = "##DragOption";
|
||||||
|
using( var source = ImRaii.DragDropSource() )
|
||||||
|
{
|
||||||
|
if( source )
|
||||||
|
{
|
||||||
|
if( ImGui.SetDragDropPayload( label, IntPtr.Zero, 0 ) )
|
||||||
|
{
|
||||||
|
_dragDropGroupIdx = groupIdx;
|
||||||
|
_dragDropOptionIdx = optionIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Text( $"Dragging option {group[ optionIdx ].Name} from group {group.Name}..." );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using( var target = ImRaii.DragDropTarget() )
|
||||||
|
{
|
||||||
|
if( target.Success && ImGuiUtil.IsDropping( label ) )
|
||||||
|
{
|
||||||
|
if( _dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0 )
|
||||||
|
{
|
||||||
|
if( _dragDropGroupIdx == groupIdx )
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
Dalamud.Chat.Print(
|
||||||
|
$"Dropped {_mod.Groups[ _dragDropGroupIdx ][ _dragDropOptionIdx ].Name} onto {_mod.Groups[ groupIdx ][ optionIdx ].Name}" );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Dalamud.Chat.Print(
|
||||||
|
$"Dropped {_mod.Groups[ _dragDropGroupIdx ][ _dragDropOptionIdx ].Name} onto {_mod.Groups[ groupIdx ][ optionIdx ].Name}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_dragDropGroupIdx = -1;
|
||||||
|
_dragDropOptionIdx = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EditOption( IModGroup group, int groupIdx, int optionIdx )
|
||||||
|
{
|
||||||
|
var option = group[ optionIdx ];
|
||||||
|
using var id = ImRaii.PushId( optionIdx );
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.Selectable( $"Option #{optionIdx + 1}" );
|
||||||
|
OptionDragDrop( group, groupIdx, optionIdx );
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
if( TextInput( "##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1 ) )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.RenameOption( _mod, groupIdx, optionIdx, newOptionName );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
|
||||||
|
"Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) )
|
||||||
|
{
|
||||||
|
_delayedActions.Enqueue( () => Penumbra.ModManager.DeleteOption( _mod, groupIdx, optionIdx ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
|
||||||
|
"Edit this option.", false, true ) )
|
||||||
|
{
|
||||||
|
_window.SubModPopup.Activate( _mod, groupIdx, optionIdx );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
if( group.Type == SelectType.Multi )
|
||||||
|
{
|
||||||
|
if( PriorityInput( "##Priority", groupIdx, optionIdx, group.OptionPriority( optionIdx ), out var priority,
|
||||||
|
50 * ImGuiHelpers.GlobalScale ) )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.ChangeOptionPriority( _mod, groupIdx, optionIdx, priority );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiUtil.HoverTooltip( "Option priority." );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TextInput( string label, int field, int option, string oldValue, out string value, uint maxLength, float width )
|
||||||
|
{
|
||||||
|
var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue;
|
||||||
|
ImGui.SetNextItemWidth( width );
|
||||||
|
if( ImGui.InputText( label, ref tmp, maxLength ) )
|
||||||
|
{
|
||||||
|
_currentEdit = tmp;
|
||||||
|
_optionIndex = option;
|
||||||
|
_currentField = field;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null )
|
||||||
|
{
|
||||||
|
var ret = _currentEdit != oldValue;
|
||||||
|
value = _currentEdit;
|
||||||
|
_currentEdit = null;
|
||||||
|
_currentField = NoFieldIdx;
|
||||||
|
_optionIndex = NoFieldIdx;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool PriorityInput( string label, int field, int option, int oldValue, out int value, float width )
|
||||||
|
{
|
||||||
|
var tmp = field == _currentField && option == _optionIndex ? _currentGroupPriority ?? oldValue : oldValue;
|
||||||
|
ImGui.SetNextItemWidth( width );
|
||||||
|
if( ImGui.InputInt( label, ref tmp, 0, 0 ) )
|
||||||
|
{
|
||||||
|
_currentGroupPriority = tmp;
|
||||||
|
_optionIndex = option;
|
||||||
|
_currentField = field;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( ImGui.IsItemDeactivatedAfterEdit() && _currentGroupPriority != null )
|
||||||
|
{
|
||||||
|
var ret = _currentGroupPriority != oldValue;
|
||||||
|
value = _currentGroupPriority.Value;
|
||||||
|
_currentGroupPriority = null;
|
||||||
|
_currentField = NoFieldIdx;
|
||||||
|
_optionIndex = NoFieldIdx;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a marked group or option outside of iteration.
|
||||||
|
private void EndActions()
|
||||||
|
{
|
||||||
|
while( _delayedActions.TryDequeue( out var action ) )
|
||||||
|
{
|
||||||
|
action.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenEditDescriptionPopup( int groupIdx )
|
||||||
|
{
|
||||||
|
_newDescriptionIdx = groupIdx;
|
||||||
|
_newDescription = groupIdx < 0 ? _mod.Description : _mod.Groups[ groupIdx ].Description;
|
||||||
|
ImGui.OpenPopup( "Edit Description" );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EditDescriptionPopup()
|
||||||
|
{
|
||||||
|
using var popup = ImRaii.Popup( "Edit Description" );
|
||||||
|
if( popup )
|
||||||
|
{
|
||||||
|
if( ImGui.IsWindowAppearing() )
|
||||||
|
{
|
||||||
|
ImGui.SetKeyboardFocusHere();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.InputTextMultiline( "##editDescription", ref _newDescription, 4096, ImGuiHelpers.ScaledVector2( 800, 800 ) );
|
||||||
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
|
|
||||||
|
var buttonSize = ImGuiHelpers.ScaledVector2( 100, 0 );
|
||||||
|
var width = 2 * buttonSize.X
|
||||||
|
+ 4 * ImGui.GetStyle().FramePadding.X
|
||||||
|
+ ImGui.GetStyle().ItemSpacing.X;
|
||||||
|
ImGui.SetCursorPosX( ( 800 * ImGuiHelpers.GlobalScale - width ) / 2 );
|
||||||
|
|
||||||
|
var oldDescription = _newDescriptionIdx == DescriptionFieldIdx
|
||||||
|
? _mod.Description
|
||||||
|
: _mod.Groups[ _newDescriptionIdx ].Description;
|
||||||
|
|
||||||
|
var tooltip = _newDescription != oldDescription ? string.Empty : "No changes made yet.";
|
||||||
|
|
||||||
|
if( ImGuiUtil.DrawDisabledButton( "Save", buttonSize, tooltip, tooltip.Length > 0 ) )
|
||||||
|
{
|
||||||
|
if( _newDescriptionIdx == DescriptionFieldIdx )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.ChangeModDescription( _mod.Index, _newDescription );
|
||||||
|
}
|
||||||
|
else if( _newDescriptionIdx >= 0 )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.ChangeGroupDescription( _mod, _newDescriptionIdx, _newDescription );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
if( ImGui.Button( "Cancel", buttonSize )
|
||||||
|
|| ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) )
|
||||||
|
{
|
||||||
|
_newDescriptionIdx = NoFieldIdx;
|
||||||
|
_newDescription = string.Empty;
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
214
Penumbra/UI/ConfigWindow.ModPanel.Header.cs
Normal file
214
Penumbra/UI/ConfigWindow.ModPanel.Header.cs
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.GameFonts;
|
||||||
|
using ImGuiNET;
|
||||||
|
using OtterGui;
|
||||||
|
using OtterGui.Raii;
|
||||||
|
using Penumbra.UI.Classes;
|
||||||
|
|
||||||
|
namespace Penumbra.UI;
|
||||||
|
|
||||||
|
public partial class ConfigWindow
|
||||||
|
{
|
||||||
|
private partial class ModPanel : IDisposable
|
||||||
|
{
|
||||||
|
// We use a big, nice game font for the title.
|
||||||
|
private readonly GameFontHandle _nameFont =
|
||||||
|
Dalamud.PluginInterface.UiBuilder.GetGameFontHandle( new GameFontStyle( GameFontFamilyAndSize.Jupiter23 ) );
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_nameFont.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header data.
|
||||||
|
private string _modName = string.Empty;
|
||||||
|
private string _modAuthor = string.Empty;
|
||||||
|
private string _modVersion = string.Empty;
|
||||||
|
private string _modWebsite = string.Empty;
|
||||||
|
private string _modWebsiteButton = string.Empty;
|
||||||
|
private bool _websiteValid;
|
||||||
|
|
||||||
|
private float _modNameWidth;
|
||||||
|
private float _modAuthorWidth;
|
||||||
|
private float _modVersionWidth;
|
||||||
|
private float _modWebsiteButtonWidth;
|
||||||
|
private float _secondRowWidth;
|
||||||
|
|
||||||
|
// Draw the header for the current mod,
|
||||||
|
// consisting of its name, version, author and website, if they exist.
|
||||||
|
private void DrawModHeader()
|
||||||
|
{
|
||||||
|
var offset = DrawModName();
|
||||||
|
DrawVersion( offset );
|
||||||
|
DrawSecondRow( offset );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the mod name in the game font with a 2px border, centered,
|
||||||
|
// with at least the width of the version space to each side.
|
||||||
|
private float DrawModName()
|
||||||
|
{
|
||||||
|
var decidingWidth = Math.Max( _secondRowWidth, ImGui.GetWindowWidth() );
|
||||||
|
var offsetWidth = ( decidingWidth - _modNameWidth ) / 2;
|
||||||
|
var offsetVersion = _modVersion.Length > 0
|
||||||
|
? _modVersionWidth + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X
|
||||||
|
: 0;
|
||||||
|
var offset = Math.Max( offsetWidth, offsetVersion );
|
||||||
|
if( offset > 0 )
|
||||||
|
{
|
||||||
|
ImGui.SetCursorPosX( offset );
|
||||||
|
}
|
||||||
|
|
||||||
|
using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.MetaInfoText );
|
||||||
|
using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale );
|
||||||
|
using var font = ImRaii.PushFont( _nameFont.ImFont, _nameFont.Available );
|
||||||
|
ImGuiUtil.DrawTextButton( _modName, Vector2.Zero, 0 );
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the version in the top-right corner.
|
||||||
|
private void DrawVersion( float offset )
|
||||||
|
{
|
||||||
|
var oldPos = ImGui.GetCursorPos();
|
||||||
|
ImGui.SetCursorPos( new Vector2( 2 * offset + _modNameWidth - _modVersionWidth - ImGui.GetStyle().WindowPadding.X,
|
||||||
|
ImGui.GetStyle().FramePadding.Y ) );
|
||||||
|
ImGuiUtil.TextColored( Colors.MetaInfoText, _modVersion );
|
||||||
|
ImGui.SetCursorPos( oldPos );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw author and website if they exist. The website is a button if it is valid.
|
||||||
|
// Usually, author begins at the left boundary of the name,
|
||||||
|
// and website ends at the right boundary of the name.
|
||||||
|
// If their combined width is larger than the name, they are combined-centered.
|
||||||
|
private void DrawSecondRow( float offset )
|
||||||
|
{
|
||||||
|
if( _modAuthor.Length == 0 )
|
||||||
|
{
|
||||||
|
if( _modWebsiteButton.Length == 0 )
|
||||||
|
{
|
||||||
|
ImGui.NewLine();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += ( _modNameWidth - _modWebsiteButtonWidth ) / 2;
|
||||||
|
ImGui.SetCursorPosX( offset );
|
||||||
|
DrawWebsite();
|
||||||
|
}
|
||||||
|
else if( _modWebsiteButton.Length == 0 )
|
||||||
|
{
|
||||||
|
offset += ( _modNameWidth - _modAuthorWidth ) / 2;
|
||||||
|
ImGui.SetCursorPosX( offset );
|
||||||
|
DrawAuthor();
|
||||||
|
}
|
||||||
|
else if( _secondRowWidth < _modNameWidth )
|
||||||
|
{
|
||||||
|
ImGui.SetCursorPosX( offset );
|
||||||
|
DrawAuthor();
|
||||||
|
ImGui.SameLine( offset + _modNameWidth - _modWebsiteButtonWidth );
|
||||||
|
DrawWebsite();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
offset -= ( _secondRowWidth - _modNameWidth ) / 2;
|
||||||
|
if( offset > 0 )
|
||||||
|
{
|
||||||
|
ImGui.SetCursorPosX( offset );
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawAuthor();
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawWebsite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the author text.
|
||||||
|
private void DrawAuthor()
|
||||||
|
{
|
||||||
|
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero );
|
||||||
|
ImGuiUtil.TextColored( Colors.MetaInfoText, "by " );
|
||||||
|
ImGui.SameLine();
|
||||||
|
style.Pop();
|
||||||
|
ImGui.Text( _mod.Author );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw either a website button if the source is a valid website address,
|
||||||
|
// or a source text if it is not.
|
||||||
|
private void DrawWebsite()
|
||||||
|
{
|
||||||
|
if( _websiteValid )
|
||||||
|
{
|
||||||
|
if( ImGui.SmallButton( _modWebsiteButton ) )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var process = new ProcessStartInfo( _modWebsite )
|
||||||
|
{
|
||||||
|
UseShellExecute = true,
|
||||||
|
};
|
||||||
|
Process.Start( process );
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiUtil.HoverTooltip( _modWebsite );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero );
|
||||||
|
ImGuiUtil.TextColored( Colors.MetaInfoText, "from " );
|
||||||
|
ImGui.SameLine();
|
||||||
|
style.Pop();
|
||||||
|
ImGui.Text( _mod.Website );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all mod header data. Should someone change frame padding or item spacing,
|
||||||
|
// or his default font, this will break, but he will just have to select a different mod to restore.
|
||||||
|
private void UpdateModData()
|
||||||
|
{
|
||||||
|
// Name
|
||||||
|
var name = $" {_mod.Name} ";
|
||||||
|
if( name != _modName )
|
||||||
|
{
|
||||||
|
using var font = ImRaii.PushFont( _nameFont.ImFont, _nameFont.Available );
|
||||||
|
_modName = name;
|
||||||
|
_modNameWidth = ImGui.CalcTextSize( name ).X + 2 * ( ImGui.GetStyle().FramePadding.X + 2 * ImGuiHelpers.GlobalScale );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Author
|
||||||
|
var author = _mod.Author.IsEmpty ? string.Empty : $"by {_mod.Author}";
|
||||||
|
if( author != _modAuthor )
|
||||||
|
{
|
||||||
|
_modAuthor = author;
|
||||||
|
_modAuthorWidth = ImGui.CalcTextSize( author ).X;
|
||||||
|
_secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version
|
||||||
|
var version = _mod.Version.Length > 0 ? $"({_mod.Version})" : string.Empty;
|
||||||
|
if( version != _modVersion )
|
||||||
|
{
|
||||||
|
_modVersion = version;
|
||||||
|
_modVersionWidth = ImGui.CalcTextSize( version ).X;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Website
|
||||||
|
if( _modWebsite != _mod.Website )
|
||||||
|
{
|
||||||
|
_modWebsite = _mod.Website;
|
||||||
|
_websiteValid = Uri.TryCreate( _modWebsite, UriKind.Absolute, out var uriResult )
|
||||||
|
&& ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp );
|
||||||
|
_modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}";
|
||||||
|
_modWebsiteButtonWidth = _websiteValid
|
||||||
|
? ImGui.CalcTextSize( _modWebsiteButton ).X + 2 * ImGui.GetStyle().FramePadding.X
|
||||||
|
: ImGui.CalcTextSize( _modWebsiteButton ).X;
|
||||||
|
_secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
207
Penumbra/UI/ConfigWindow.ModPanel.Settings.cs
Normal file
207
Penumbra/UI/ConfigWindow.ModPanel.Settings.cs
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using ImGuiNET;
|
||||||
|
using OtterGui;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Raii;
|
||||||
|
using OtterGui.Widgets;
|
||||||
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.Mods;
|
||||||
|
using Penumbra.UI.Classes;
|
||||||
|
|
||||||
|
namespace Penumbra.UI;
|
||||||
|
|
||||||
|
public partial class ConfigWindow
|
||||||
|
{
|
||||||
|
private partial class ModPanel
|
||||||
|
{
|
||||||
|
private ModSettings _settings = null!;
|
||||||
|
private ModCollection _collection = null!;
|
||||||
|
private bool _emptySetting;
|
||||||
|
private bool _inherited;
|
||||||
|
private SubList< ConflictCache.Conflict > _conflicts = SubList< ConflictCache.Conflict >.Empty;
|
||||||
|
|
||||||
|
private int? _currentPriority;
|
||||||
|
|
||||||
|
private void UpdateSettingsData( ModFileSystemSelector selector )
|
||||||
|
{
|
||||||
|
_settings = selector.SelectedSettings;
|
||||||
|
_collection = selector.SelectedSettingCollection;
|
||||||
|
_emptySetting = _settings == ModSettings.Empty;
|
||||||
|
_inherited = _collection != Penumbra.CollectionManager.Current;
|
||||||
|
_conflicts = Penumbra.CollectionManager.Current.ModConflicts( _mod.Index );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the whole settings tab as well as its contents.
|
||||||
|
private void DrawSettingsTab()
|
||||||
|
{
|
||||||
|
using var tab = DrawTab( SettingsTabHeader, Tabs.Settings );
|
||||||
|
if( !tab )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var child = ImRaii.Child( "##settings" );
|
||||||
|
if( !child )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawInheritedWarning();
|
||||||
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
|
DrawEnabledInput();
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawPriorityInput();
|
||||||
|
DrawRemoveSettings();
|
||||||
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
|
for( var idx = 0; idx < _mod.Groups.Count; ++idx )
|
||||||
|
{
|
||||||
|
DrawSingleGroup( _mod.Groups[ idx ], idx );
|
||||||
|
}
|
||||||
|
|
||||||
|
for( var idx = 0; idx < _mod.Groups.Count; ++idx )
|
||||||
|
{
|
||||||
|
DrawMultiGroup( _mod.Groups[ idx ], idx );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Draw a big red bar if the current setting is inherited.
|
||||||
|
private void DrawInheritedWarning()
|
||||||
|
{
|
||||||
|
if( !_inherited )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg );
|
||||||
|
var width = new Vector2( ImGui.GetContentRegionAvail().X, 0 );
|
||||||
|
if( ImGui.Button( $"These settings are inherited from {_collection.Name}.", width ) )
|
||||||
|
{
|
||||||
|
Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, false );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiUtil.HoverTooltip( "You can click this button to copy the current settings to the current selection.\n"
|
||||||
|
+ "You can also just change any setting, which will copy the settings with the single setting changed to the current selection." );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a checkbox for the enabled status of the mod.
|
||||||
|
private void DrawEnabledInput()
|
||||||
|
{
|
||||||
|
var enabled = _settings.Enabled;
|
||||||
|
if( ImGui.Checkbox( "Enabled", ref enabled ) )
|
||||||
|
{
|
||||||
|
Penumbra.CollectionManager.Current.SetModState( _mod.Index, enabled );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a priority input.
|
||||||
|
// Priority is changed on deactivation of the input box.
|
||||||
|
private void DrawPriorityInput()
|
||||||
|
{
|
||||||
|
var priority = _currentPriority ?? _settings.Priority;
|
||||||
|
ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale );
|
||||||
|
if( ImGui.InputInt( "##Priority", ref priority, 0, 0 ) )
|
||||||
|
{
|
||||||
|
_currentPriority = priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue )
|
||||||
|
{
|
||||||
|
if( _currentPriority != _settings.Priority )
|
||||||
|
{
|
||||||
|
Penumbra.CollectionManager.Current.SetModPriority( _mod.Index, _currentPriority.Value );
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentPriority = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiUtil.LabeledHelpMarker( "Priority", "Mods with higher priority take precedence before Mods with lower priority.\n"
|
||||||
|
+ "That means, if Mod A should overwrite changes from Mod B, Mod A should have higher priority than Mod B." );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a button to remove the current settings and inherit them instead
|
||||||
|
// on the top-right corner of the window/tab.
|
||||||
|
private void DrawRemoveSettings()
|
||||||
|
{
|
||||||
|
const string text = "Remove Settings";
|
||||||
|
if( _inherited || _emptySetting )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0;
|
||||||
|
ImGui.SameLine( ImGui.GetWindowWidth() - ImGui.CalcTextSize( text ).X - ImGui.GetStyle().FramePadding.X * 2 - scroll);
|
||||||
|
if( ImGui.Button( text ) )
|
||||||
|
{
|
||||||
|
Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, true );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiUtil.HoverTooltip( "Remove current settings from this collection so that it can inherit them.\n"
|
||||||
|
+ "If no inherited collection has settings for this mod, it will be disabled." );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a single group selector as a combo box.
|
||||||
|
// If a description is provided, add a help marker besides it.
|
||||||
|
private void DrawSingleGroup( IModGroup group, int groupIdx )
|
||||||
|
{
|
||||||
|
if( group.Type != SelectType.Single || !group.IsOption )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var id = ImRaii.PushId( groupIdx );
|
||||||
|
var selectedOption = _emptySetting ? 0 : ( int )_settings.Settings[ groupIdx ];
|
||||||
|
ImGui.SetNextItemWidth( _window._inputTextWidth.X * 3 / 4 );
|
||||||
|
using var combo = ImRaii.Combo( string.Empty, group[ selectedOption ].Name );
|
||||||
|
if( combo )
|
||||||
|
{
|
||||||
|
for( var idx2 = 0; idx2 < group.Count; ++idx2 )
|
||||||
|
{
|
||||||
|
if( ImGui.Selectable( group[ idx2 ].Name, idx2 == selectedOption ) )
|
||||||
|
{
|
||||||
|
Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx2 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
combo.Dispose();
|
||||||
|
ImGui.SameLine();
|
||||||
|
if( group.Description.Length > 0 )
|
||||||
|
{
|
||||||
|
ImGuiUtil.LabeledHelpMarker( group.Name, group.Description );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.Text( group.Name );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a multi group selector as a bordered set of checkboxes.
|
||||||
|
// If a description is provided, add a help marker in the title.
|
||||||
|
private void DrawMultiGroup( IModGroup group, int groupIdx )
|
||||||
|
{
|
||||||
|
if( group.Type != SelectType.Multi || !group.IsOption )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var id = ImRaii.PushId( groupIdx );
|
||||||
|
var flags = _emptySetting ? 0u : _settings.Settings[ groupIdx ];
|
||||||
|
Widget.BeginFramedGroup( group.Name, group.Description );
|
||||||
|
for( var idx2 = 0; idx2 < group.Count; ++idx2 )
|
||||||
|
{
|
||||||
|
var flag = 1u << idx2;
|
||||||
|
var setting = ( flags & flag ) != 0;
|
||||||
|
if( ImGui.Checkbox( group[ idx2 ].Name, ref setting ) )
|
||||||
|
{
|
||||||
|
flags = setting ? flags | flag : flags & ~flag;
|
||||||
|
Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, flags );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget.EndFramedGroup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
178
Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs
Normal file
178
Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel.Design;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using ImGuiNET;
|
||||||
|
using OtterGui;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Raii;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
|
using Penumbra.Meta.Manipulations;
|
||||||
|
using Penumbra.Mods;
|
||||||
|
using Penumbra.UI.Classes;
|
||||||
|
|
||||||
|
namespace Penumbra.UI;
|
||||||
|
|
||||||
|
public partial class ConfigWindow
|
||||||
|
{
|
||||||
|
private partial class ModPanel
|
||||||
|
{
|
||||||
|
[Flags]
|
||||||
|
private enum Tabs
|
||||||
|
{
|
||||||
|
Description = 0x01,
|
||||||
|
Settings = 0x02,
|
||||||
|
ChangedItems = 0x04,
|
||||||
|
Conflicts = 0x08,
|
||||||
|
Edit = 0x10,
|
||||||
|
};
|
||||||
|
|
||||||
|
// We want to keep the preferred tab selected even if switching through mods.
|
||||||
|
private Tabs _preferredTab = Tabs.Settings;
|
||||||
|
private Tabs _availableTabs = 0;
|
||||||
|
|
||||||
|
// Required to use tabs that can not be closed but have a flag to set them open.
|
||||||
|
private static readonly Utf8String ConflictTabHeader = Utf8String.FromStringUnsafe( "Conflicts", false );
|
||||||
|
private static readonly Utf8String DescriptionTabHeader = Utf8String.FromStringUnsafe( "Description", false );
|
||||||
|
private static readonly Utf8String SettingsTabHeader = Utf8String.FromStringUnsafe( "Settings", false );
|
||||||
|
private static readonly Utf8String ChangedItemsTabHeader = Utf8String.FromStringUnsafe( "Changed Items", false );
|
||||||
|
private static readonly Utf8String EditModTabHeader = Utf8String.FromStringUnsafe( "Edit Mod", false );
|
||||||
|
|
||||||
|
private void DrawTabBar()
|
||||||
|
{
|
||||||
|
ImGui.Dummy( _window._defaultSpace );
|
||||||
|
using var tabBar = ImRaii.TabBar( "##ModTabs" );
|
||||||
|
if( !tabBar )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_availableTabs = Tabs.Settings
|
||||||
|
| ( _mod.ChangedItems.Count > 0 ? Tabs.ChangedItems : 0 )
|
||||||
|
| ( _mod.Description.Length > 0 ? Tabs.Description : 0 )
|
||||||
|
| ( _conflicts.Count > 0 ? Tabs.Conflicts : 0 )
|
||||||
|
| ( Penumbra.Config.ShowAdvanced ? Tabs.Edit : 0 );
|
||||||
|
|
||||||
|
DrawSettingsTab();
|
||||||
|
DrawDescriptionTab();
|
||||||
|
DrawChangedItemsTab();
|
||||||
|
DrawConflictsTab();
|
||||||
|
DrawEditModTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just a simple text box with the wrapped description, if it exists.
|
||||||
|
private void DrawDescriptionTab()
|
||||||
|
{
|
||||||
|
using var tab = DrawTab( DescriptionTabHeader, Tabs.Description );
|
||||||
|
if( !tab )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var child = ImRaii.Child( "##description" );
|
||||||
|
if( !child )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TextWrapped( _mod.Description );
|
||||||
|
}
|
||||||
|
|
||||||
|
// A simple clipped list of changed items.
|
||||||
|
private void DrawChangedItemsTab()
|
||||||
|
{
|
||||||
|
using var tab = DrawTab( ChangedItemsTabHeader, Tabs.ChangedItems );
|
||||||
|
if( !tab )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var list = ImRaii.ListBox( "##changedItems", -Vector2.One );
|
||||||
|
if( !list )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var zipList = ZipList.FromSortedList( _mod.ChangedItems );
|
||||||
|
var height = ImGui.GetTextLineHeight();
|
||||||
|
ImGuiClip.ClippedDraw( zipList, kvp => _window.DrawChangedItem( kvp.Item1, kvp.Item2 ), height );
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any conflicts exist, show them in this tab.
|
||||||
|
private void DrawConflictsTab()
|
||||||
|
{
|
||||||
|
using var tab = DrawTab( ConflictTabHeader, Tabs.Conflicts );
|
||||||
|
if( !tab )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var box = ImRaii.ListBox( "##conflicts" );
|
||||||
|
if( !box )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var conflicts = Penumbra.CollectionManager.Current.ModConflicts( _mod.Index );
|
||||||
|
Mod? oldBadMod = null;
|
||||||
|
using var indent = ImRaii.PushIndent( 0f );
|
||||||
|
foreach( var conflict in conflicts )
|
||||||
|
{
|
||||||
|
var badMod = Penumbra.ModManager[ conflict.Mod2 ];
|
||||||
|
if( badMod != oldBadMod )
|
||||||
|
{
|
||||||
|
if( oldBadMod != null )
|
||||||
|
{
|
||||||
|
indent.Pop( 30f );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( ImGui.Selectable( badMod.Name ) )
|
||||||
|
{
|
||||||
|
_window._selector.SelectByValue( badMod );
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
using var color = ImRaii.PushColor( ImGuiCol.Text, conflict.Mod1Priority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() );
|
||||||
|
ImGui.Text( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2 ].Settings!.Priority})" );
|
||||||
|
|
||||||
|
indent.Push( 30f );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( conflict.Data is Utf8GamePath p )
|
||||||
|
{
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if( conflict.Data is MetaManipulation m )
|
||||||
|
{
|
||||||
|
ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty );
|
||||||
|
}
|
||||||
|
|
||||||
|
oldBadMod = badMod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Draw a tab by given name if it is available, and deal with changing the preferred tab.
|
||||||
|
private ImRaii.IEndObject DrawTab( Utf8String name, Tabs flag )
|
||||||
|
{
|
||||||
|
if( !_availableTabs.HasFlag( flag ) )
|
||||||
|
{
|
||||||
|
return ImRaii.IEndObject.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var flags = _preferredTab == flag ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None;
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
var tab = ImRaii.TabItem( name.Path, flags );
|
||||||
|
if( ImGui.IsItemClicked() )
|
||||||
|
{
|
||||||
|
_preferredTab = flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,386 +1,15 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Widgets;
|
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.UI.Classes;
|
using Penumbra.UI.Classes;
|
||||||
|
|
||||||
namespace Penumbra.UI;
|
namespace Penumbra.UI;
|
||||||
|
|
||||||
public partial class ConfigWindow
|
|
||||||
{
|
|
||||||
private class ModPanel
|
|
||||||
{
|
|
||||||
private readonly ConfigWindow _window;
|
|
||||||
private bool _valid;
|
|
||||||
private bool _emptySetting;
|
|
||||||
private bool _inherited;
|
|
||||||
private ModFileSystem.Leaf _leaf = null!;
|
|
||||||
private Mod2 _mod = null!;
|
|
||||||
private ModSettings2 _settings = null!;
|
|
||||||
private ModCollection _collection = null!;
|
|
||||||
private string _lastWebsite = string.Empty;
|
|
||||||
private bool _websiteValid;
|
|
||||||
|
|
||||||
private string? _currentSortOrderPath;
|
|
||||||
private int? _currentPriority;
|
|
||||||
|
|
||||||
public ModPanel( ConfigWindow window )
|
|
||||||
=> _window = window;
|
|
||||||
|
|
||||||
private void Init( ModFileSystemSelector selector )
|
|
||||||
{
|
|
||||||
_valid = selector.Selected != null;
|
|
||||||
if( !_valid )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_leaf = selector.SelectedLeaf!;
|
|
||||||
_mod = selector.Selected!;
|
|
||||||
_settings = selector.SelectedSettings;
|
|
||||||
_collection = selector.SelectedSettingCollection;
|
|
||||||
_emptySetting = _settings == ModSettings2.Empty;
|
|
||||||
_inherited = _collection != Penumbra.CollectionManager.Current;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Draw( ModFileSystemSelector selector )
|
|
||||||
{
|
|
||||||
Init( selector );
|
|
||||||
if( !_valid )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DrawInheritedWarning();
|
|
||||||
DrawHeaderLine();
|
|
||||||
DrawFilesystemPath();
|
|
||||||
DrawEnabledInput();
|
|
||||||
ImGui.SameLine();
|
|
||||||
DrawPriorityInput();
|
|
||||||
DrawRemoveSettings();
|
|
||||||
DrawTabBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawDescriptionTab()
|
|
||||||
{
|
|
||||||
if( _mod.Description.Length == 0 )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var tab = ImRaii.TabItem( "Description" );
|
|
||||||
if( !tab )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var child = ImRaii.Child( "##tab" );
|
|
||||||
if( !child )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.TextWrapped( _mod.Description );
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSettingsTab()
|
|
||||||
{
|
|
||||||
if( !_mod.HasOptions )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var tab = ImRaii.TabItem( "Settings" );
|
|
||||||
if( !tab )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var child = ImRaii.Child( "##tab" );
|
|
||||||
if( !child )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for( var idx = 0; idx < _mod.Groups.Count; ++idx )
|
|
||||||
{
|
|
||||||
var group = _mod.Groups[ idx ];
|
|
||||||
if( group.Type == SelectType.Single && group.IsOption )
|
|
||||||
{
|
|
||||||
using var id = ImRaii.PushId( idx );
|
|
||||||
var selectedOption = _emptySetting ? 0 : ( int )_settings.Settings[ idx ];
|
|
||||||
ImGui.SetNextItemWidth( _window._inputTextWidth.X );
|
|
||||||
using var combo = ImRaii.Combo( string.Empty, group[ selectedOption ].Name );
|
|
||||||
if( combo )
|
|
||||||
{
|
|
||||||
for( var idx2 = 0; idx2 < group.Count; ++idx2 )
|
|
||||||
{
|
|
||||||
if( ImGui.Selectable( group[ idx2 ].Name, idx2 == selectedOption ) )
|
|
||||||
{
|
|
||||||
Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, idx, ( uint )idx2 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
combo.Dispose();
|
|
||||||
ImGui.SameLine();
|
|
||||||
if( group.Description.Length > 0 )
|
|
||||||
{
|
|
||||||
ImGuiUtil.LabeledHelpMarker( group.Name, group.Description );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.Text( group.Name );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO add description
|
|
||||||
for( var idx = 0; idx < _mod.Groups.Count; ++idx )
|
|
||||||
{
|
|
||||||
var group = _mod.Groups[ idx ];
|
|
||||||
if( group.Type == SelectType.Multi && group.IsOption )
|
|
||||||
{
|
|
||||||
using var id = ImRaii.PushId( idx );
|
|
||||||
var flags = _emptySetting ? 0u : _settings.Settings[ idx ];
|
|
||||||
Widget.BeginFramedGroup( group.Name );
|
|
||||||
for( var idx2 = 0; idx2 < group.Count; ++idx2 )
|
|
||||||
{
|
|
||||||
var flag = 1u << idx2;
|
|
||||||
var setting = ( flags & flag ) != 0;
|
|
||||||
if( ImGui.Checkbox( group[ idx2 ].Name, ref setting ) )
|
|
||||||
{
|
|
||||||
flags = setting ? flags | flag : flags & ~flag;
|
|
||||||
Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, idx, flags );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget.EndFramedGroup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawChangedItemsTab()
|
|
||||||
{
|
|
||||||
if( _mod.ChangedItems.Count == 0 )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var tab = ImRaii.TabItem( "Changed Items" );
|
|
||||||
if( !tab )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var list = ImRaii.ListBox( "##changedItems", -Vector2.One );
|
|
||||||
if( !list )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach( var (name, data) in _mod.ChangedItems )
|
|
||||||
{
|
|
||||||
_window.DrawChangedItem( name, data );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawTabBar()
|
|
||||||
{
|
|
||||||
using var tabBar = ImRaii.TabBar( "##ModTabs" );
|
|
||||||
if( !tabBar )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DrawDescriptionTab();
|
|
||||||
DrawSettingsTab();
|
|
||||||
DrawChangedItemsTab();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawInheritedWarning()
|
|
||||||
{
|
|
||||||
if( _inherited )
|
|
||||||
{
|
|
||||||
using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg );
|
|
||||||
var w = new Vector2( ImGui.GetContentRegionAvail().X, 0 );
|
|
||||||
if( ImGui.Button( $"These settings are inherited from {_collection.Name}.", w ) )
|
|
||||||
{
|
|
||||||
Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, false );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawPriorityInput()
|
|
||||||
{
|
|
||||||
var priority = _currentPriority ?? _settings.Priority;
|
|
||||||
ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale );
|
|
||||||
if( ImGui.InputInt( "Priority", ref priority, 0, 0 ) )
|
|
||||||
{
|
|
||||||
_currentPriority = priority;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue )
|
|
||||||
{
|
|
||||||
if( _currentPriority != _settings.Priority )
|
|
||||||
{
|
|
||||||
Penumbra.CollectionManager.Current.SetModPriority( _mod.Index, _currentPriority.Value );
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentPriority = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawRemoveSettings()
|
|
||||||
{
|
|
||||||
if( _inherited )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
if( ImGui.Button( "Remove Settings" ) )
|
|
||||||
{
|
|
||||||
Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, true );
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGuiUtil.HoverTooltip( "Remove current settings from this collection so that it can inherit them.\n"
|
|
||||||
+ "If no inherited collection has settings for this mod, it will be disabled." );
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawEnabledInput()
|
|
||||||
{
|
|
||||||
var enabled = _settings.Enabled;
|
|
||||||
if( ImGui.Checkbox( "Enabled", ref enabled ) )
|
|
||||||
{
|
|
||||||
Penumbra.CollectionManager.Current.SetModState( _mod.Index, enabled );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawFilesystemPath()
|
|
||||||
{
|
|
||||||
var fullName = _leaf.FullName();
|
|
||||||
var path = _currentSortOrderPath ?? fullName;
|
|
||||||
ImGui.SetNextItemWidth( 300 * ImGuiHelpers.GlobalScale );
|
|
||||||
if( ImGui.InputText( "Sort Order", ref path, 256 ) )
|
|
||||||
{
|
|
||||||
_currentSortOrderPath = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( ImGui.IsItemDeactivatedAfterEdit() && _currentSortOrderPath != null )
|
|
||||||
{
|
|
||||||
if( _currentSortOrderPath != fullName )
|
|
||||||
{
|
|
||||||
_window._penumbra.ModFileSystem.RenameAndMove( _leaf, _currentSortOrderPath );
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentSortOrderPath = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Draw the first info line for the mod panel,
|
|
||||||
// containing all basic meta information.
|
|
||||||
private void DrawHeaderLine()
|
|
||||||
{
|
|
||||||
DrawName();
|
|
||||||
ImGui.SameLine();
|
|
||||||
DrawVersion();
|
|
||||||
ImGui.SameLine();
|
|
||||||
DrawAuthor();
|
|
||||||
ImGui.SameLine();
|
|
||||||
DrawWebsite();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the mod name.
|
|
||||||
private void DrawName()
|
|
||||||
{
|
|
||||||
ImGui.Text( _mod.Name.Text );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the author of the mod, if any.
|
|
||||||
private void DrawAuthor()
|
|
||||||
{
|
|
||||||
using var group = ImRaii.Group();
|
|
||||||
ImGuiUtil.TextColored( Colors.MetaInfoText, "by" );
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.Text( _mod.Author.IsEmpty ? "Unknown" : _mod.Author.Text );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the mod version, if any.
|
|
||||||
private void DrawVersion()
|
|
||||||
{
|
|
||||||
if( _mod.Version.Length > 0 )
|
|
||||||
{
|
|
||||||
ImGui.Text( $"(Version {_mod.Version})" );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.Dummy( Vector2.Zero );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the last seen website and check for validity.
|
|
||||||
private void UpdateWebsite( string newWebsite )
|
|
||||||
{
|
|
||||||
if( _lastWebsite == newWebsite )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastWebsite = newWebsite;
|
|
||||||
_websiteValid = Uri.TryCreate( _lastWebsite, UriKind.Absolute, out var uriResult )
|
|
||||||
&& ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the website source either as a button to open the site,
|
|
||||||
// if it is a valid http website, or as pure text.
|
|
||||||
private void DrawWebsite()
|
|
||||||
{
|
|
||||||
UpdateWebsite( _mod.Website );
|
|
||||||
if( _lastWebsite.Length == 0 )
|
|
||||||
{
|
|
||||||
ImGui.Dummy( Vector2.Zero );
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var group = ImRaii.Group();
|
|
||||||
if( _websiteValid )
|
|
||||||
{
|
|
||||||
if( ImGui.Button( "Open Website" ) )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var process = new ProcessStartInfo( _lastWebsite )
|
|
||||||
{
|
|
||||||
UseShellExecute = true,
|
|
||||||
};
|
|
||||||
Process.Start( process );
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGuiUtil.HoverTooltip( _lastWebsite );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGuiUtil.TextColored( Colors.MetaInfoText, "from" );
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.Text( _lastWebsite );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class ConfigWindow
|
public partial class ConfigWindow
|
||||||
{
|
{
|
||||||
public void DrawModsTab()
|
public void DrawModsTab()
|
||||||
|
|
@ -401,14 +30,13 @@ public partial class ConfigWindow
|
||||||
using var group = ImRaii.Group();
|
using var group = ImRaii.Group();
|
||||||
DrawHeaderLine();
|
DrawHeaderLine();
|
||||||
|
|
||||||
using var child = ImRaii.Child( "##ModsTabMod", -Vector2.One, true );
|
using var child = ImRaii.Child( "##ModsTabMod", -Vector2.One, true, ImGuiWindowFlags.HorizontalScrollbar );
|
||||||
if( child )
|
if( child )
|
||||||
{
|
{
|
||||||
_modPanel.Draw( _selector );
|
_modPanel.Draw( _selector );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Draw the header line that can quick switch between collections.
|
// Draw the header line that can quick switch between collections.
|
||||||
private void DrawHeaderLine()
|
private void DrawHeaderLine()
|
||||||
{
|
{
|
||||||
|
|
@ -466,4 +94,46 @@ public partial class ConfigWindow
|
||||||
? absoluteSize
|
? absoluteSize
|
||||||
: Math.Max( absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100 );
|
: Math.Max( absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The basic setup for the mod panel.
|
||||||
|
// Details are in other files.
|
||||||
|
private partial class ModPanel
|
||||||
|
{
|
||||||
|
private readonly ConfigWindow _window;
|
||||||
|
|
||||||
|
private bool _valid;
|
||||||
|
private ModFileSystem.Leaf _leaf = null!;
|
||||||
|
private Mod _mod = null!;
|
||||||
|
|
||||||
|
public ModPanel( ConfigWindow window )
|
||||||
|
{
|
||||||
|
_window = window;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Draw( ModFileSystemSelector selector )
|
||||||
|
{
|
||||||
|
Init( selector );
|
||||||
|
if( !_valid )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawModHeader();
|
||||||
|
DrawTabBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Init( ModFileSystemSelector selector )
|
||||||
|
{
|
||||||
|
_valid = selector.Selected != null;
|
||||||
|
if( !_valid )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_leaf = selector.SelectedLeaf!;
|
||||||
|
_mod = selector.Selected!;
|
||||||
|
UpdateSettingsData( selector );
|
||||||
|
UpdateModData();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +47,7 @@ public partial class ConfigWindow
|
||||||
DrawAdvancedSettings();
|
DrawAdvancedSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string? _settingsNewModDirectory;
|
private string? _newModDirectory;
|
||||||
private readonly FileDialogManager _dialogManager = new();
|
private readonly FileDialogManager _dialogManager = new();
|
||||||
private bool _dialogOpen;
|
private bool _dialogOpen;
|
||||||
|
|
||||||
|
|
@ -70,12 +70,18 @@ public partial class ConfigWindow
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO
|
_newModDirectory ??= Penumbra.Config.ModDirectory;
|
||||||
//_dialogManager.OpenFolderDialog( "Choose Mod Directory", ( b, s ) =>
|
var startDir = Directory.Exists( _newModDirectory )
|
||||||
//{
|
? _newModDirectory
|
||||||
// _newModDirectory = b ? s : _newModDirectory;
|
: Directory.Exists( Penumbra.Config.ModDirectory )
|
||||||
// _dialogOpen = false;
|
? Penumbra.Config.ModDirectory
|
||||||
//}, _newModDirectory, false);
|
: ".";
|
||||||
|
|
||||||
|
_dialogManager.OpenFolderDialog( "Choose Mod Directory", ( b, s ) =>
|
||||||
|
{
|
||||||
|
_newModDirectory = b ? s : _newModDirectory;
|
||||||
|
_dialogOpen = false;
|
||||||
|
}, startDir );
|
||||||
_dialogOpen = true;
|
_dialogOpen = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,12 +105,12 @@ public partial class ConfigWindow
|
||||||
|
|
||||||
private void DrawRootFolder()
|
private void DrawRootFolder()
|
||||||
{
|
{
|
||||||
_settingsNewModDirectory ??= Penumbra.Config.ModDirectory;
|
_newModDirectory ??= Penumbra.Config.ModDirectory;
|
||||||
|
|
||||||
var spacing = 3 * ImGuiHelpers.GlobalScale;
|
var spacing = 3 * ImGuiHelpers.GlobalScale;
|
||||||
using var group = ImRaii.Group();
|
using var group = ImRaii.Group();
|
||||||
ImGui.SetNextItemWidth( _window._inputTextWidth.X - spacing - ImGui.GetFrameHeight() );
|
ImGui.SetNextItemWidth( _window._inputTextWidth.X - spacing - ImGui.GetFrameHeight() );
|
||||||
var save = ImGui.InputText( "##rootDirectory", ref _settingsNewModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue );
|
var save = ImGui.InputText( "##rootDirectory", ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue );
|
||||||
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( spacing, 0 ) );
|
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( spacing, 0 ) );
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
DrawDirectoryPickerButton();
|
DrawDirectoryPickerButton();
|
||||||
|
|
@ -121,14 +127,14 @@ public partial class ConfigWindow
|
||||||
var pos = ImGui.GetCursorPosX();
|
var pos = ImGui.GetCursorPosX();
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
|
|
||||||
if( Penumbra.Config.ModDirectory == _settingsNewModDirectory || _settingsNewModDirectory.Length == 0 )
|
if( Penumbra.Config.ModDirectory == _newModDirectory || _newModDirectory.Length == 0 )
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory, pos ) )
|
if( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory, pos ) )
|
||||||
{
|
{
|
||||||
Penumbra.ModManager.DiscoverMods( _settingsNewModDirectory );
|
Penumbra.ModManager.DiscoverMods( _newModDirectory );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,12 +143,13 @@ public partial class ConfigWindow
|
||||||
{
|
{
|
||||||
DrawOpenDirectoryButton( 0, Penumbra.ModManager.BasePath, Penumbra.ModManager.Valid );
|
DrawOpenDirectoryButton( 0, Penumbra.ModManager.BasePath, Penumbra.ModManager.Valid );
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if( ImGui.Button( "Rediscover Mods" ) )
|
var tt = Penumbra.ModManager.Valid
|
||||||
|
? "Force Penumbra to completely re-scan your root directory as if it was restarted."
|
||||||
|
: "The currently selected folder is not valid. Please select a different folder.";
|
||||||
|
if( ImGuiUtil.DrawDisabledButton( "Rediscover Mods", Vector2.Zero, tt, !Penumbra.ModManager.Valid ) )
|
||||||
{
|
{
|
||||||
Penumbra.ModManager.DiscoverMods();
|
Penumbra.ModManager.DiscoverMods();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGuiUtil.HoverTooltip( "Force Penumbra to completely re-scan your root directory as if it was restarted." );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawEnabledBox()
|
private void DrawEnabledBox()
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,14 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
||||||
private readonly EffectiveTab _effectiveTab;
|
private readonly EffectiveTab _effectiveTab;
|
||||||
private readonly DebugTab _debugTab;
|
private readonly DebugTab _debugTab;
|
||||||
private readonly ResourceTab _resourceTab;
|
private readonly ResourceTab _resourceTab;
|
||||||
|
public readonly SubModEditWindow SubModPopup = new();
|
||||||
|
|
||||||
public ConfigWindow( Penumbra penumbra )
|
public ConfigWindow( Penumbra penumbra )
|
||||||
: base( GetLabel() )
|
: base( GetLabel() )
|
||||||
{
|
{
|
||||||
_penumbra = penumbra;
|
_penumbra = penumbra;
|
||||||
_settingsTab = new SettingsTab( this );
|
_settingsTab = new SettingsTab( this );
|
||||||
_selector = new ModFileSystemSelector( _penumbra.ModFileSystem, new HashSet< Mod2 >() ); // TODO
|
_selector = new ModFileSystemSelector( _penumbra.ModFileSystem, new HashSet< Mod >() ); // TODO
|
||||||
_modPanel = new ModPanel( this );
|
_modPanel = new ModPanel( this );
|
||||||
_collectionsTab = new CollectionsTab( this );
|
_collectionsTab = new CollectionsTab( this );
|
||||||
_effectiveTab = new EffectiveTab();
|
_effectiveTab = new EffectiveTab();
|
||||||
|
|
@ -61,6 +62,7 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_selector.Dispose();
|
_selector.Dispose();
|
||||||
|
_modPanel.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetLabel()
|
private static string GetLabel()
|
||||||
|
|
@ -70,10 +72,12 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
||||||
|
|
||||||
private Vector2 _defaultSpace;
|
private Vector2 _defaultSpace;
|
||||||
private Vector2 _inputTextWidth;
|
private Vector2 _inputTextWidth;
|
||||||
|
private Vector2 _iconButtonSize;
|
||||||
|
|
||||||
private void SetupSizes()
|
private void SetupSizes()
|
||||||
{
|
{
|
||||||
_defaultSpace = new Vector2( 0, 10 * ImGuiHelpers.GlobalScale );
|
_defaultSpace = new Vector2( 0, 10 * ImGuiHelpers.GlobalScale );
|
||||||
_inputTextWidth = new Vector2( 350f * ImGuiHelpers.GlobalScale, 0 );
|
_inputTextWidth = new Vector2( 350f * ImGuiHelpers.GlobalScale, 0 );
|
||||||
|
_iconButtonSize = new Vector2( ImGui.GetFrameHeight() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ namespace Penumbra.UI;
|
||||||
// using the Dalamud-provided collapsible submenu.
|
// using the Dalamud-provided collapsible submenu.
|
||||||
public class LaunchButton : IDisposable
|
public class LaunchButton : IDisposable
|
||||||
{
|
{
|
||||||
private readonly ConfigWindow _configWindow;
|
private readonly ConfigWindow _configWindow;
|
||||||
private readonly TextureWrap? _icon;
|
private readonly TextureWrap? _icon;
|
||||||
private readonly TitleScreenMenu.TitleScreenMenuEntry? _entry;
|
private readonly TitleScreenMenu.TitleScreenMenuEntry? _entry;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Penumbra.Util;
|
namespace Penumbra.Util;
|
||||||
|
|
||||||
public static class ArrayExtensions
|
public static class ArrayExtensions
|
||||||
{
|
{
|
||||||
|
public static IEnumerable< (T, int) > WithIndex< T >( this IEnumerable< T > list )
|
||||||
|
=> list.Select( ( x, i ) => ( x, i ) );
|
||||||
|
|
||||||
public static int IndexOf< T >( this IReadOnlyList< T > array, Predicate< T > predicate )
|
public static int IndexOf< T >( this IReadOnlyList< T > array, Predicate< T > predicate )
|
||||||
{
|
{
|
||||||
for( var i = 0; i < array.Count; ++i )
|
for( var i = 0; i < array.Count; ++i )
|
||||||
|
|
@ -61,35 +65,4 @@ public static class ArrayExtensions
|
||||||
result = default;
|
result = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool Move< T >( this IList< T > list, int idx1, int idx2 )
|
|
||||||
{
|
|
||||||
idx1 = Math.Clamp( idx1, 0, list.Count - 1 );
|
|
||||||
idx2 = Math.Clamp( idx2, 0, list.Count - 1 );
|
|
||||||
if( idx1 == idx2 )
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tmp = list[ idx1 ];
|
|
||||||
// move element down and shift other elements up
|
|
||||||
if( idx1 < idx2 )
|
|
||||||
{
|
|
||||||
for( var i = idx1; i < idx2; i++ )
|
|
||||||
{
|
|
||||||
list[ i ] = list[ i + 1 ];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// move element up and shift other elements down
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for( var i = idx1; i > idx2; i-- )
|
|
||||||
{
|
|
||||||
list[ i ] = list[ i - 1 ];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
list[ idx2 ] = tmp;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -66,12 +66,12 @@ public static class Backup
|
||||||
{
|
{
|
||||||
++count;
|
++count;
|
||||||
var time = file.CreationTimeUtc;
|
var time = file.CreationTimeUtc;
|
||||||
if( ( oldest?.CreationTimeUtc ?? DateTime.MinValue ) < time )
|
if( ( oldest?.CreationTimeUtc ?? DateTime.MaxValue ) > time )
|
||||||
{
|
{
|
||||||
oldest = file;
|
oldest = file;
|
||||||
}
|
}
|
||||||
|
|
||||||
if( ( newest?.CreationTimeUtc ?? DateTime.MaxValue ) > time )
|
if( ( newest?.CreationTimeUtc ?? DateTime.MinValue ) < time )
|
||||||
{
|
{
|
||||||
newest = file;
|
newest = file;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
|
|
||||||
namespace Penumbra.Util;
|
|
||||||
|
|
||||||
public static class DialogExtensions
|
|
||||||
{
|
|
||||||
public static Task< DialogResult > ShowDialogAsync( this CommonDialog form )
|
|
||||||
{
|
|
||||||
using var process = Process.GetCurrentProcess();
|
|
||||||
return form.ShowDialogAsync( new DialogHandle( process.MainWindowHandle ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task< DialogResult > ShowDialogAsync( this CommonDialog form, IWin32Window owner )
|
|
||||||
{
|
|
||||||
var taskSource = new TaskCompletionSource< DialogResult >();
|
|
||||||
var th = new Thread( () => DialogThread( form, owner, taskSource ) );
|
|
||||||
th.Start();
|
|
||||||
return taskSource.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
[STAThread]
|
|
||||||
private static void DialogThread( CommonDialog form, IWin32Window owner,
|
|
||||||
TaskCompletionSource< DialogResult > taskSource )
|
|
||||||
{
|
|
||||||
Application.SetCompatibleTextRenderingDefault( false );
|
|
||||||
Application.EnableVisualStyles();
|
|
||||||
using var hiddenForm = new HiddenForm( form, owner, taskSource );
|
|
||||||
Application.Run( hiddenForm );
|
|
||||||
Application.ExitThread();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DialogHandle : IWin32Window
|
|
||||||
{
|
|
||||||
public IntPtr Handle { get; set; }
|
|
||||||
|
|
||||||
public DialogHandle( IntPtr handle )
|
|
||||||
=> Handle = handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HiddenForm : Form
|
|
||||||
{
|
|
||||||
private readonly CommonDialog _form;
|
|
||||||
private readonly IWin32Window _owner;
|
|
||||||
private readonly TaskCompletionSource< DialogResult > _taskSource;
|
|
||||||
|
|
||||||
public HiddenForm( CommonDialog form, IWin32Window owner, TaskCompletionSource< DialogResult > taskSource )
|
|
||||||
{
|
|
||||||
_form = form;
|
|
||||||
_owner = owner;
|
|
||||||
_taskSource = taskSource;
|
|
||||||
|
|
||||||
Opacity = 0;
|
|
||||||
FormBorderStyle = FormBorderStyle.None;
|
|
||||||
ShowInTaskbar = false;
|
|
||||||
Size = new Size( 0, 0 );
|
|
||||||
|
|
||||||
Shown += HiddenForm_Shown;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HiddenForm_Shown( object? sender, EventArgs _ )
|
|
||||||
{
|
|
||||||
Hide();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _form.ShowDialog( _owner );
|
|
||||||
_taskSource.SetResult( result );
|
|
||||||
}
|
|
||||||
catch( Exception e )
|
|
||||||
{
|
|
||||||
_taskSource.SetException( e );
|
|
||||||
}
|
|
||||||
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
52
Penumbra/Util/DictionaryExtensions.cs
Normal file
52
Penumbra/Util/DictionaryExtensions.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Penumbra.Util;
|
||||||
|
|
||||||
|
public static class DictionaryExtensions
|
||||||
|
{
|
||||||
|
// Returns whether two dictionaries contain equal keys and values.
|
||||||
|
public static bool SetEquals< TKey, TValue >( this IReadOnlyDictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs )
|
||||||
|
{
|
||||||
|
if( lhs.Count != rhs.Count )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach( var (key, value) in lhs )
|
||||||
|
{
|
||||||
|
if( !rhs.TryGetValue( key, out var rhsValue ) )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( value == null )
|
||||||
|
{
|
||||||
|
if( rhsValue != null )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !value.Equals( rhsValue ) )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set one dictionary to the other, deleting previous entries and ensuring capacity beforehand.
|
||||||
|
public static void SetTo< TKey, TValue >( this Dictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs )
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
lhs.Clear();
|
||||||
|
lhs.EnsureCapacity( rhs.Count );
|
||||||
|
foreach( var (key, value) in rhs )
|
||||||
|
{
|
||||||
|
lhs.Add( key, value );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace Penumbra.Util;
|
|
||||||
|
|
||||||
public static class Functions
|
|
||||||
{
|
|
||||||
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
|
|
||||||
public static bool SetDifferent< T >( T oldValue, T newValue, Action< T > set ) where T : IEquatable< T >
|
|
||||||
{
|
|
||||||
if( oldValue.Equals( newValue ) )
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
set( newValue );
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -74,7 +74,7 @@ public static class ModelChanger
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool ChangeModMaterials( Mod2 mod, string from, string to )
|
public static bool ChangeModMaterials( Mod mod, string from, string to )
|
||||||
{
|
{
|
||||||
if( ValidStrings( from, to ) )
|
if( ValidStrings( from, to ) )
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace Penumbra.Util;
|
|
||||||
|
|
||||||
public class SingleOrArrayConverter< T > : JsonConverter
|
|
||||||
{
|
|
||||||
public override bool CanConvert( Type objectType )
|
|
||||||
=> objectType == typeof( HashSet< T > );
|
|
||||||
|
|
||||||
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
|
||||||
{
|
|
||||||
var token = JToken.Load( reader );
|
|
||||||
|
|
||||||
if( token.Type == JTokenType.Array )
|
|
||||||
{
|
|
||||||
return token.ToObject< HashSet< T > >() ?? new HashSet< T >();
|
|
||||||
}
|
|
||||||
|
|
||||||
var tmp = token.ToObject< T >();
|
|
||||||
return tmp != null
|
|
||||||
? new HashSet< T > { tmp }
|
|
||||||
: new HashSet< T >();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanWrite
|
|
||||||
=> true;
|
|
||||||
|
|
||||||
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
|
|
||||||
{
|
|
||||||
writer.WriteStartArray();
|
|
||||||
if( value != null )
|
|
||||||
{
|
|
||||||
var v = ( HashSet< T > )value;
|
|
||||||
foreach( var val in v )
|
|
||||||
{
|
|
||||||
serializer.Serialize( writer, val?.ToString() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Penumbra.Util;
|
|
||||||
|
|
||||||
public static class StringPathExtensions
|
|
||||||
{
|
|
||||||
private static readonly HashSet< char > Invalid = new(Path.GetInvalidFileNameChars());
|
|
||||||
|
|
||||||
public static string ReplaceInvalidPathSymbols( this string s, string replacement = "_" )
|
|
||||||
{
|
|
||||||
StringBuilder sb = new(s.Length);
|
|
||||||
foreach( var c in s )
|
|
||||||
{
|
|
||||||
if( Invalid.Contains( c ) )
|
|
||||||
{
|
|
||||||
sb.Append( replacement );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sb.Append( c );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string RemoveInvalidPathSymbols( this string s )
|
|
||||||
=> string.Concat( s.Split( Path.GetInvalidFileNameChars() ) );
|
|
||||||
|
|
||||||
public static string ReplaceNonAsciiSymbols( this string s, string replacement = "_" )
|
|
||||||
{
|
|
||||||
StringBuilder sb = new(s.Length);
|
|
||||||
foreach( var c in s )
|
|
||||||
{
|
|
||||||
if( c >= 128 )
|
|
||||||
{
|
|
||||||
sb.Append( replacement );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sb.Append( c );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ReplaceBadXivSymbols( this string s, string replacement = "_" )
|
|
||||||
{
|
|
||||||
StringBuilder sb = new(s.Length);
|
|
||||||
foreach( var c in s )
|
|
||||||
{
|
|
||||||
if( c >= 128 || Invalid.Contains( c ) )
|
|
||||||
{
|
|
||||||
sb.Append( replacement );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sb.Append( c );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Penumbra.Util;
|
|
||||||
|
|
||||||
public static class TempFile
|
|
||||||
{
|
|
||||||
public static FileInfo TempFileName( DirectoryInfo baseDir, string suffix = "" )
|
|
||||||
{
|
|
||||||
const uint maxTries = 15;
|
|
||||||
for( var i = 0; i < maxTries; ++i )
|
|
||||||
{
|
|
||||||
var name = Path.GetRandomFileName();
|
|
||||||
var path = new FileInfo( Path.Combine( baseDir.FullName,
|
|
||||||
suffix.Length > 0 ? name[ ..name.LastIndexOf( '.' ) ] + suffix : name ) );
|
|
||||||
if( !path.Exists )
|
|
||||||
{
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new IOException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue