mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 12:14:17 +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 );
|
||||
}
|
||||
|
||||
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 )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public class SimpleRedirectManager
|
|||
return RedirectResult.NoPermission;
|
||||
}
|
||||
|
||||
if( Mod2.FilterFile( path ) )
|
||||
if( Mod.FilterFile( path ) )
|
||||
{
|
||||
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 ) )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using OtterGui.Filesystem;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Util;
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ public partial class ModCollection
|
|||
public delegate void CollectionChangeDelegate( Type type, ModCollection? oldCollection, ModCollection? newCollection,
|
||||
string? characterName = null );
|
||||
|
||||
private readonly Mod2.Manager _modManager;
|
||||
private readonly Mod.Manager _modManager;
|
||||
|
||||
// The empty collection is always available and always has index 0.
|
||||
// It can not be deleted or moved.
|
||||
|
|
@ -59,7 +60,7 @@ public partial class ModCollection
|
|||
public IEnumerable< ModCollection > GetEnumeratorWithEmpty()
|
||||
=> _collections;
|
||||
|
||||
public Manager( Mod2.Manager manager )
|
||||
public Manager( Mod.Manager manager )
|
||||
{
|
||||
_modManager = manager;
|
||||
|
||||
|
|
@ -207,7 +208,7 @@ public partial class ModCollection
|
|||
|
||||
|
||||
// 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 )
|
||||
{
|
||||
switch( type )
|
||||
|
|
@ -221,10 +222,10 @@ public partial class ModCollection
|
|||
OnModAddedActive( mod.TotalManipulations > 0 );
|
||||
break;
|
||||
case ModPathChangeType.Deleted:
|
||||
var settings = new List< ModSettings2? >( _collections.Count );
|
||||
var settings = new List< ModSettings? >( _collections.Count );
|
||||
foreach( var collection in this )
|
||||
{
|
||||
settings.Add( collection[ mod.Index ].Settings );
|
||||
settings.Add( collection._settings[ mod.Index ] );
|
||||
collection.RemoveMod( mod, mod.Index );
|
||||
}
|
||||
|
||||
|
|
@ -242,26 +243,50 @@ public partial class ModCollection
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnModOptionsChanged( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx )
|
||||
// Automatically update all relevant collections when a mod is changed.
|
||||
// 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
|
||||
switch( type )
|
||||
if( recomputeList )
|
||||
{
|
||||
case ModOptionChangeType.GroupRenamed:
|
||||
case ModOptionChangeType.GroupAdded:
|
||||
case ModOptionChangeType.GroupDeleted:
|
||||
case ModOptionChangeType.PriorityChanged:
|
||||
case ModOptionChangeType.OptionAdded:
|
||||
case ModOptionChangeType.OptionDeleted:
|
||||
case ModOptionChangeType.OptionChanged:
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException( nameof( type ), type, null );
|
||||
foreach( var collection in this.Where( c => c.HasCache ) )
|
||||
{
|
||||
if( collection[ mod.Index ].Settings is { Enabled: true } )
|
||||
{
|
||||
collection.CalculateEffectiveFileList( withMeta, collection == Penumbra.CollectionManager.Default );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
|
|
@ -73,9 +73,16 @@ public struct ConflictCache
|
|||
}
|
||||
|
||||
// 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()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Dalamud.Logging;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Meta.Manager;
|
||||
|
||||
|
|
@ -64,8 +65,8 @@ public partial class ModCollection
|
|||
internal IReadOnlyList< ConflictCache.Conflict > Conflicts
|
||||
=> _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >();
|
||||
|
||||
internal IEnumerable< ConflictCache.Conflict > ModConflicts( int modIdx )
|
||||
=> _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.Conflict >();
|
||||
internal SubList< ConflictCache.Conflict > ModConflicts( int modIdx )
|
||||
=> _cache?.Conflicts.ModConflicts( modIdx ) ?? SubList< ConflictCache.Conflict >.Empty;
|
||||
|
||||
// Update the effective file list for the given cache.
|
||||
// Creates a cache if necessary.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public partial class ModCollection
|
|||
// Shared caches to avoid allocations.
|
||||
private static readonly Dictionary< Utf8GamePath, FileRegister > RegisteredFiles = 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 SortedList< string, object? > _changedItems = new();
|
||||
|
|
@ -225,7 +225,7 @@ public partial class ModCollection
|
|||
foreach( var (path, file) in mod.Files.Concat( mod.FileSwaps ) )
|
||||
{
|
||||
// Skip all filtered files
|
||||
if( Mod2.FilterFile( path ) )
|
||||
if( Mod.FilterFile( path ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -257,6 +257,11 @@ public partial class ModCollection
|
|||
{
|
||||
var config = settings.Settings[ idx ];
|
||||
var group = mod.Groups[ idx ];
|
||||
if( group.Count == 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch( group.Type )
|
||||
{
|
||||
case SelectType.Single:
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ public partial class ModCollection
|
|||
}
|
||||
|
||||
// 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 ) ) )
|
||||
{
|
||||
|
|
@ -56,7 +56,7 @@ public partial class ModCollection
|
|||
|
||||
// Set the enabled state of every mod in mods to the new value.
|
||||
// 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;
|
||||
foreach( var mod in mods )
|
||||
|
|
@ -137,7 +137,7 @@ public partial class ModCollection
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ using System.Text;
|
|||
using Dalamud.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Filesystem;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ public partial class ModCollection
|
|||
if( settings != null )
|
||||
{
|
||||
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 version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0;
|
||||
// Custom deserialization that is converted with the constructor.
|
||||
var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings2.SavedSettings > >()
|
||||
?? new Dictionary< string, ModSettings2.SavedSettings >();
|
||||
var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings.SavedSettings > >()
|
||||
?? new Dictionary< string, ModSettings.SavedSettings >();
|
||||
inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >();
|
||||
|
||||
return new ModCollection( name, version, settings );
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OtterGui.Filesystem;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Util;
|
||||
|
||||
|
|
@ -119,7 +120,7 @@ public partial class ModCollection
|
|||
// Obtain the actual settings for a given mod via index.
|
||||
// Also returns the collection the settings are taken from.
|
||||
// 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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -45,13 +45,13 @@ public sealed partial class ModCollection
|
|||
}
|
||||
|
||||
// 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 );
|
||||
|
||||
private static bool SettingIsDefaultV0( ModSettings2? setting )
|
||||
private static bool SettingIsDefaultV0( ModSettings? setting )
|
||||
=> 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);
|
||||
}
|
||||
|
|
@ -27,17 +27,17 @@ public partial class ModCollection
|
|||
|
||||
// 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.
|
||||
private readonly List< ModSettings2? > _settings;
|
||||
private readonly List< ModSettings? > _settings;
|
||||
|
||||
public IReadOnlyList< ModSettings2? > Settings
|
||||
public IReadOnlyList< ModSettings? > Settings
|
||||
=> _settings;
|
||||
|
||||
// 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 );
|
||||
|
||||
// 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.
|
||||
private ModCollection( string name, ModCollection duplicate )
|
||||
|
|
@ -52,13 +52,13 @@ public partial class ModCollection
|
|||
}
|
||||
|
||||
// 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;
|
||||
Version = version;
|
||||
_unusedSettings = allSettings;
|
||||
|
||||
_settings = new List< ModSettings2? >();
|
||||
_settings = new List< ModSettings? >();
|
||||
ApplyModSettings();
|
||||
|
||||
Migration.Migrate( this );
|
||||
|
|
@ -68,7 +68,7 @@ public partial class ModCollection
|
|||
|
||||
// Create a new, unique empty collection of a given 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.
|
||||
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.
|
||||
private bool AddMod( Mod2 mod )
|
||||
private bool AddMod( Mod mod )
|
||||
{
|
||||
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.
|
||||
private void RemoveMod( Mod2 mod, int idx )
|
||||
private void RemoveMod( Mod mod, int idx )
|
||||
{
|
||||
var settings = _settings[ idx ];
|
||||
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 );
|
||||
|
|
@ -127,7 +127,7 @@ public partial class ModCollection
|
|||
{
|
||||
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();
|
||||
|
|
|
|||
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.Enums;
|
||||
using Penumbra.Interop.Structs;
|
||||
using Penumbra.Mods;
|
||||
using FileMode = Penumbra.Interop.Structs.FileMode;
|
||||
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ public partial class MetaManager
|
|||
private readonly ModCollection _collection;
|
||||
private static int _imcManagerCount;
|
||||
|
||||
|
||||
public MetaManagerImc( ModCollection collection )
|
||||
{
|
||||
_collection = collection;
|
||||
|
|
|
|||
|
|
@ -12,91 +12,10 @@ public interface IMetaManipulation
|
|||
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 )]
|
||||
public readonly struct MetaManipulation : IEquatable< MetaManipulation >, IComparable< MetaManipulation >
|
||||
{
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ public partial class Configuration
|
|||
private void ResettleSortOrder()
|
||||
{
|
||||
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 writer = new StreamWriter( stream );
|
||||
using var j = new JsonTextWriter( writer );
|
||||
|
|
@ -169,7 +169,7 @@ public partial class Configuration
|
|||
var data = JArray.Parse( text );
|
||||
|
||||
var maxPriority = 0;
|
||||
var dict = new Dictionary< string, ModSettings2.SavedSettings >();
|
||||
var dict = new Dictionary< string, ModSettings.SavedSettings >();
|
||||
foreach( var setting in data.Cast< JObject >() )
|
||||
{
|
||||
var modName = ( string )setting[ "FolderName" ]!;
|
||||
|
|
@ -178,7 +178,7 @@ public partial class Configuration
|
|||
var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, uint > >()
|
||||
?? setting[ "Conf" ]!.ToObject< Dictionary< string, uint > >();
|
||||
|
||||
dict[ modName ] = new ModSettings2.SavedSettings()
|
||||
dict[ modName ] = new ModSettings.SavedSettings()
|
||||
{
|
||||
Enabled = enabled,
|
||||
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;
|
||||
|
||||
public sealed partial class Mod2
|
||||
public sealed partial class Mod
|
||||
{
|
||||
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 void ChangeModName( Index idx, string newName )
|
||||
|
|
@ -14,9 +14,10 @@ public sealed partial class Mod2
|
|||
var mod = this[ idx ];
|
||||
if( mod.Name != newName )
|
||||
{
|
||||
var oldName = mod.Name;
|
||||
mod.Name = newName;
|
||||
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.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.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.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.SaveMeta();
|
||||
ModMetaChanged?.Invoke( MetaChangeType.Website, mod );
|
||||
ModMetaChanged?.Invoke( MetaChangeType.Website, mod, null );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using OtterGui.Filesystem;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Util;
|
||||
|
|
@ -13,25 +14,43 @@ public enum ModOptionChangeType
|
|||
GroupRenamed,
|
||||
GroupAdded,
|
||||
GroupDeleted,
|
||||
GroupMoved,
|
||||
GroupTypeChanged,
|
||||
PriorityChanged,
|
||||
OptionAdded,
|
||||
OptionDeleted,
|
||||
OptionChanged,
|
||||
OptionMoved,
|
||||
OptionFilesChanged,
|
||||
OptionSwapsChanged,
|
||||
OptionMetaChanged,
|
||||
OptionUpdated,
|
||||
DisplayChange,
|
||||
}
|
||||
|
||||
public sealed partial class Mod2
|
||||
public sealed partial class Mod
|
||||
{
|
||||
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 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 oldName = group.Name;
|
||||
if( oldName == newName || !VerifyFileName( mod, group, newName ) )
|
||||
if( oldName == newName || !VerifyFileName( mod, group, newName, true ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -43,33 +62,41 @@ public sealed partial class Mod2
|
|||
_ => 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;
|
||||
}
|
||||
|
||||
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
|
||||
? new MultiModGroup { 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 ];
|
||||
mod._groups.RemoveAt( groupIdx );
|
||||
group.DeleteFile( BasePath );
|
||||
ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, 0 );
|
||||
group.DeleteFile( mod.BasePath );
|
||||
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 ];
|
||||
if( group.Description == newDescription )
|
||||
|
|
@ -83,10 +110,10 @@ public sealed partial class Mod2
|
|||
MultiModGroup m => m.Description = 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 ];
|
||||
if( group.Priority == newPriority )
|
||||
|
|
@ -100,14 +127,14 @@ public sealed partial class Mod2
|
|||
MultiModGroup m => m.Priority = 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 ] )
|
||||
{
|
||||
case SingleModGroup s:
|
||||
case SingleModGroup:
|
||||
ChangeGroupPriority( mod, groupIdx, newPriority );
|
||||
break;
|
||||
case MultiModGroup m:
|
||||
|
|
@ -117,12 +144,12 @@ public sealed partial class Mod2
|
|||
}
|
||||
|
||||
m.PrioritizedOptions[ optionIdx ] = ( m.PrioritizedOptions[ optionIdx ].Mod, newPriority );
|
||||
ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx );
|
||||
ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1 );
|
||||
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 ] )
|
||||
{
|
||||
|
|
@ -145,10 +172,10 @@ public sealed partial class Mod2
|
|||
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 ] )
|
||||
{
|
||||
|
|
@ -160,10 +187,30 @@ public sealed partial class Mod2
|
|||
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 ] )
|
||||
{
|
||||
|
|
@ -175,10 +222,19 @@ public sealed partial class Mod2
|
|||
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 );
|
||||
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 );
|
||||
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 );
|
||||
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();
|
||||
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 ) ) )
|
||||
{
|
||||
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 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
|
||||
{
|
||||
|
|
@ -278,7 +387,7 @@ public sealed partial class Mod2
|
|||
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.
|
||||
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.
|
||||
mod.HasOptions = type switch
|
||||
{
|
||||
ModOptionChangeType.GroupDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ),
|
||||
ModOptionChangeType.OptionAdded => mod.HasOptions |= mod._groups[ groupIdx ].IsOption,
|
||||
ModOptionChangeType.OptionDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ),
|
||||
_ => mod.HasOptions,
|
||||
ModOptionChangeType.GroupDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ),
|
||||
ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any( o => o.IsOption ),
|
||||
ModOptionChangeType.OptionAdded => mod.HasOptions |= mod._groups[ groupIdx ].IsOption,
|
||||
ModOptionChangeType.OptionDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ),
|
||||
_ => mod.HasOptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ using Dalamud.Logging;
|
|||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public sealed partial class Mod2
|
||||
public sealed partial class Mod
|
||||
{
|
||||
public sealed partial class Manager
|
||||
{
|
||||
|
|
@ -50,7 +50,7 @@ public sealed partial class Mod2
|
|||
}
|
||||
|
||||
BasePath = newDir;
|
||||
Valid = true;
|
||||
Valid = Directory.Exists( newDir.FullName );
|
||||
if( Penumbra.Config.ModDirectory != BasePath.FullName )
|
||||
{
|
||||
Penumbra.Config.ModDirectory = BasePath.FullName;
|
||||
|
|
@ -74,7 +74,7 @@ public sealed partial class Mod2
|
|||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
mod.Index = _mods.Count;
|
||||
_mods.Add( mod );
|
||||
}
|
||||
|
|
@ -4,22 +4,22 @@ using System.Collections.Generic;
|
|||
|
||||
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 ];
|
||||
|
||||
public IReadOnlyList< Mod2 > Mods
|
||||
public IReadOnlyList< Mod > Mods
|
||||
=> _mods;
|
||||
|
||||
public int Count
|
||||
=> _mods.Count;
|
||||
|
||||
public IEnumerator< Mod2 > GetEnumerator()
|
||||
public IEnumerator< Mod > GetEnumerator()
|
||||
=> _mods.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,
|
||||
}
|
||||
|
||||
public partial class Mod2
|
||||
public partial class Mod
|
||||
{
|
||||
public DirectoryInfo BasePath { get; private set; }
|
||||
public int Index { get; private set; } = -1;
|
||||
|
||||
private Mod2( DirectoryInfo basePath )
|
||||
private Mod( DirectoryInfo basePath )
|
||||
=> BasePath = basePath;
|
||||
|
||||
public static Mod2? LoadMod( DirectoryInfo basePath )
|
||||
public static Mod? LoadMod( DirectoryInfo basePath )
|
||||
{
|
||||
basePath.Refresh();
|
||||
if( !basePath.Exists )
|
||||
|
|
@ -27,7 +27,7 @@ public partial class Mod2
|
|||
return null;
|
||||
}
|
||||
|
||||
var mod = new Mod2( basePath );
|
||||
var mod = new Mod( basePath );
|
||||
mod.LoadMeta();
|
||||
if( mod.Name.Length == 0 )
|
||||
{
|
||||
|
|
@ -3,7 +3,7 @@ using System.Linq;
|
|||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public sealed partial class Mod2
|
||||
public sealed partial class Mod
|
||||
{
|
||||
public SortedList< string, object? > ChangedItems { get; } = new();
|
||||
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;
|
||||
|
||||
public partial class Mod2
|
||||
public partial class Mod
|
||||
{
|
||||
public ISubMod Default
|
||||
=> _default;
|
||||
|
|
@ -6,19 +6,18 @@ using Dalamud.Logging;
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Importer;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public sealed partial class Mod2
|
||||
public sealed partial class Mod
|
||||
{
|
||||
private static class Migration
|
||||
{
|
||||
public static bool Migrate( Mod2 mod, JObject json )
|
||||
public static bool Migrate( Mod mod, JObject json )
|
||||
=> MigrateV0ToV1( mod, json );
|
||||
|
||||
private static bool MigrateV0ToV1( Mod2 mod, JObject json )
|
||||
private static bool MigrateV0ToV1( Mod mod, JObject json )
|
||||
{
|
||||
if( mod.FileVersion > 0 )
|
||||
{
|
||||
|
|
@ -27,14 +26,15 @@ public sealed partial class Mod2
|
|||
|
||||
var swaps = json[ "FileSwaps" ]?.ToObject< 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 seenMetaFiles = new HashSet< FullPath >();
|
||||
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 )
|
||||
&& !mod._default.FileData.TryAdd( gamePath, unusedFile ) )
|
||||
|
|
@ -61,7 +61,7 @@ public sealed partial class Mod2
|
|||
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 )
|
||||
{
|
||||
|
|
@ -82,14 +82,14 @@ public sealed partial class Mod2
|
|||
mod._groups.Add( newMultiGroup );
|
||||
foreach( var option in group.Options )
|
||||
{
|
||||
newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod.BasePath, option ), optionPriority++ ) );
|
||||
newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod.BasePath, option, seenMetaFiles ), optionPriority++ ) );
|
||||
}
|
||||
|
||||
break;
|
||||
case SelectType.Single:
|
||||
if( group.Options.Count == 1 )
|
||||
{
|
||||
AddFilesToSubMod( mod._default, mod.BasePath, group.Options[ 0 ] );
|
||||
AddFilesToSubMod( mod._default, mod.BasePath, group.Options[ 0 ], seenMetaFiles );
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -102,28 +102,34 @@ public sealed partial class Mod2
|
|||
mod._groups.Add( newSingleGroup );
|
||||
foreach( var option in group.Options )
|
||||
{
|
||||
newSingleGroup.OptionData.Add( SubModFromOption( mod.BasePath, option ) );
|
||||
newSingleGroup.OptionData.Add( SubModFromOption( mod.BasePath, option, seenMetaFiles ) );
|
||||
}
|
||||
|
||||
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 )
|
||||
{
|
||||
var fullPath = new FullPath( basePath, relPath );
|
||||
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 };
|
||||
AddFilesToSubMod( subMod, basePath, option );
|
||||
var subMod = new SubMod { Name = option.OptionName };
|
||||
AddFilesToSubMod( subMod, basePath, option, seenMetaFiles );
|
||||
subMod.IncorporateMetaChanges( basePath, false );
|
||||
return subMod;
|
||||
}
|
||||
|
|
@ -152,5 +158,45 @@ public sealed partial class Mod2
|
|||
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.Linq;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ public enum MetaChangeType : byte
|
|||
Migration = 0x40,
|
||||
}
|
||||
|
||||
public sealed partial class Mod2
|
||||
public sealed partial class Mod
|
||||
{
|
||||
public const uint CurrentFileVersion = 1;
|
||||
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 Dalamud.Logging;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Importer;
|
||||
using Penumbra.Import;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using OtterGui.Filesystem;
|
||||
|
||||
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.
|
||||
// Does not save or copy the backup in the current mod directory,
|
||||
// as this is done on mod directory changes only.
|
||||
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.
|
||||
public static ModFileSystem Load()
|
||||
|
|
@ -20,18 +25,24 @@ public sealed class ModFileSystem : FileSystem< Mod2 >, IDisposable
|
|||
|
||||
ret.Changed += ret.OnChange;
|
||||
Penumbra.ModManager.ModDiscoveryFinished += ret.Reload;
|
||||
Penumbra.ModManager.ModMetaChanged += ret.OnMetaChange;
|
||||
Penumbra.ModManager.ModPathChanged += ret.OnModPathChange;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
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.
|
||||
// Used on construction and on mod rediscoveries.
|
||||
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();
|
||||
}
|
||||
|
|
@ -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.
|
||||
private static string ModToIdentifier( Mod2 mod )
|
||||
private static string ModToIdentifier( Mod mod )
|
||||
=> mod.BasePath.Name;
|
||||
|
||||
private static string ModToName( Mod2 mod )
|
||||
=> mod.Name.Text;
|
||||
private static string ModToName( Mod mod )
|
||||
=> 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.
|
||||
if( fullPath == ModToName( mod ) )
|
||||
if( regex.IsMatch( fullPath ) )
|
||||
{
|
||||
return ( string.Empty, false );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using Dalamud.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using OtterGui.Filesystem;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
|
@ -76,4 +77,7 @@ public interface IModGroup : IEnumerable< ISubMod >
|
|||
j.WriteEndArray();
|
||||
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 Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Filesystem;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class Mod2
|
||||
public partial class Mod
|
||||
{
|
||||
private sealed class MultiModGroup : IModGroup
|
||||
{
|
||||
|
|
@ -63,5 +64,26 @@ public partial class Mod2
|
|||
|
||||
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.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Filesystem;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class Mod2
|
||||
public partial class Mod
|
||||
{
|
||||
private sealed class SingleModGroup : IModGroup
|
||||
{
|
||||
|
|
@ -62,5 +64,26 @@ public partial class Mod2
|
|||
|
||||
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.Linq;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Importer;
|
||||
using Penumbra.Import;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class Mod2
|
||||
public partial class Mod
|
||||
{
|
||||
private string DefaultFile
|
||||
=> Path.Combine( BasePath.FullName, "default_mod.json" );
|
||||
|
|
@ -135,31 +135,7 @@ public partial class Mod2
|
|||
{
|
||||
File.Delete( file.FullName );
|
||||
}
|
||||
|
||||
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 );
|
||||
}
|
||||
ManipulationData.UnionWith( meta.MetaManipulations );
|
||||
|
||||
break;
|
||||
case ".rgsp":
|
||||
|
|
@ -174,11 +150,7 @@ public partial class Mod2
|
|||
{
|
||||
File.Delete( file.FullName );
|
||||
}
|
||||
|
||||
foreach( var manip in rgsp.RspManipulations )
|
||||
{
|
||||
ManipulationData.Add( manip );
|
||||
}
|
||||
ManipulationData.UnionWith( rgsp.MetaManipulations );
|
||||
|
||||
break;
|
||||
default: continue;
|
||||
|
|
@ -1,19 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using OtterGui.Filesystem;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
|
||||
// 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 int Priority { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public ModSettings2 DeepCopy()
|
||||
public ModSettings DeepCopy()
|
||||
=> new()
|
||||
{
|
||||
Enabled = Enabled,
|
||||
|
|
@ -21,7 +22,7 @@ public class ModSettings2
|
|||
Settings = Settings.ToList(),
|
||||
};
|
||||
|
||||
public static ModSettings2 DefaultSettings( Mod2 mod )
|
||||
public static ModSettings DefaultSettings( Mod mod )
|
||||
=> new()
|
||||
{
|
||||
Enabled = false,
|
||||
|
|
@ -29,19 +30,31 @@ public class ModSettings2
|
|||
Settings = Enumerable.Repeat( 0u, mod.Groups.Count ).ToList(),
|
||||
};
|
||||
|
||||
|
||||
|
||||
public void HandleChanges( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx )
|
||||
public bool HandleChanges( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx )
|
||||
{
|
||||
switch( type )
|
||||
{
|
||||
case ModOptionChangeType.GroupRenamed: return true;
|
||||
case ModOptionChangeType.GroupAdded:
|
||||
Settings.Insert( groupIdx, 0 );
|
||||
break;
|
||||
return true;
|
||||
case ModOptionChangeType.GroupDeleted:
|
||||
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:
|
||||
{
|
||||
var group = mod.Groups[ groupIdx ];
|
||||
var config = Settings[ groupIdx ];
|
||||
Settings[ groupIdx ] = group.Type switch
|
||||
|
|
@ -50,20 +63,38 @@ public class ModSettings2
|
|||
SelectType.Multi => RemoveBit( config, optionIdx ),
|
||||
_ => 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 );
|
||||
var group = mod.Groups[ groupIdx ];
|
||||
Settings[ groupIdx ] = group.Type switch
|
||||
{
|
||||
SelectType.Single => ( uint )Math.Max( newValue, group.Count ),
|
||||
SelectType.Multi => ( ( 1u << group.Count ) - 1 ) & newValue,
|
||||
_ => newValue,
|
||||
};
|
||||
Settings[ groupIdx ] = FixSetting( group, newValue );
|
||||
}
|
||||
|
||||
private static uint RemoveBit( uint config, int bit )
|
||||
|
|
@ -75,6 +106,16 @@ public class ModSettings2
|
|||
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 )
|
||||
{
|
||||
if( totalCount <= Settings.Count )
|
||||
|
|
@ -100,7 +141,7 @@ public class ModSettings2
|
|||
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ),
|
||||
};
|
||||
|
||||
public SavedSettings( ModSettings2 settings, Mod2 mod )
|
||||
public SavedSettings( ModSettings settings, Mod mod )
|
||||
{
|
||||
Priority = settings.Priority;
|
||||
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 changes = Settings.Count != mod.Groups.Count;
|
||||
|
|
@ -121,7 +162,12 @@ public class ModSettings2
|
|||
{
|
||||
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
|
||||
{
|
||||
|
|
@ -130,7 +176,7 @@ public class ModSettings2
|
|||
}
|
||||
}
|
||||
|
||||
settings = new ModSettings2
|
||||
settings = new ModSettings
|
||||
{
|
||||
Enabled = Enabled,
|
||||
Priority = Priority,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ public class Penumbra : IDalamudPlugin
|
|||
public static ResidentResourceManager ResidentResources { get; private set; } = null!;
|
||||
public static CharacterUtility CharacterUtility { 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 SimpleRedirectManager Redirects { get; private set; } = null!;
|
||||
public static ResourceLoader ResourceLoader { get; private set; } = null!;
|
||||
|
|
@ -78,7 +78,7 @@ public class Penumbra : IDalamudPlugin
|
|||
MetaFileManager = new MetaFileManager();
|
||||
ResourceLoader = new ResourceLoader( this );
|
||||
ResourceLogger = new ResourceLogger( ResourceLoader );
|
||||
ModManager = new Mod2.Manager( Config.ModDirectory );
|
||||
ModManager = new Mod.Manager( Config.ModDirectory );
|
||||
ModManager.DiscoverMods();
|
||||
CollectionManager = new ModCollection.Manager( ModManager );
|
||||
ModFileSystem = ModFileSystem.Load();
|
||||
|
|
@ -138,6 +138,7 @@ public class Penumbra : IDalamudPlugin
|
|||
btn = new LaunchButton( _configWindow );
|
||||
system = new WindowSystem( Name );
|
||||
system.AddWindow( _configWindow );
|
||||
system.AddWindow( cfg.SubModPopup );
|
||||
Dalamud.PluginInterface.UiBuilder.Draw += system.Draw;
|
||||
Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle;
|
||||
}
|
||||
|
|
@ -294,8 +295,7 @@ public class Penumbra : IDalamudPlugin
|
|||
case "reload":
|
||||
{
|
||||
ModManager.DiscoverMods();
|
||||
Dalamud.Chat.Print(
|
||||
$"Reloaded Penumbra mods. You have {ModManager.Mods.Count} mods."
|
||||
Dalamud.Chat.Print( $"Reloaded Penumbra mods. You have {ModManager.Mods.Count} mods."
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
|
@ -314,7 +314,8 @@ public class Penumbra : IDalamudPlugin
|
|||
}
|
||||
case "debug":
|
||||
{
|
||||
// TODO
|
||||
Config.DebugMode = true;
|
||||
Config.Save();
|
||||
break;
|
||||
}
|
||||
case "enable":
|
||||
|
|
@ -370,7 +371,7 @@ public class Penumbra : IDalamudPlugin
|
|||
{
|
||||
var list = new DirectoryInfo( ModCollection.CollectionDirectory ).EnumerateFiles( "*.json" ).ToList();
|
||||
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 ) );
|
||||
return list;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Numerics;
|
|||
using System.Runtime.InteropServices;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Filesystem;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.Collections;
|
||||
|
|
@ -21,7 +22,7 @@ public partial class ModFileSystemSelector
|
|||
}
|
||||
|
||||
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 int _filterType = -1;
|
||||
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.
|
||||
// If any filter is set, they should be hidden by default unless their children are visible,
|
||||
// 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 )
|
||||
{
|
||||
|
|
@ -88,7 +89,7 @@ public partial class ModFileSystemSelector
|
|||
}
|
||||
|
||||
// Apply the string filters.
|
||||
private bool ApplyStringFilters( ModFileSystem.Leaf leaf, Mod2 mod )
|
||||
private bool ApplyStringFilters( ModFileSystem.Leaf leaf, Mod mod )
|
||||
{
|
||||
return _filterType switch
|
||||
{
|
||||
|
|
@ -102,7 +103,7 @@ public partial class ModFileSystemSelector
|
|||
}
|
||||
|
||||
// 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 ) )
|
||||
{
|
||||
|
|
@ -119,7 +120,7 @@ public partial class ModFileSystemSelector
|
|||
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 )
|
||||
{
|
||||
return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedMod.Value() : ColorId.EnabledMod.Value();
|
||||
|
|
@ -130,7 +131,7 @@ public partial class ModFileSystemSelector
|
|||
: 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 );
|
||||
// Handle mod details.
|
||||
|
|
@ -188,7 +189,7 @@ public partial class ModFileSystemSelector
|
|||
}
|
||||
|
||||
// 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.Any( c => !c.Solved ) )
|
||||
|
|
|
|||
|
|
@ -1,23 +1,30 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Logging;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Filesystem;
|
||||
using OtterGui.FileSystem.Selector;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Import;
|
||||
using Penumbra.Mods;
|
||||
|
||||
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 ModFileSystemSelector( ModFileSystem fileSystem, IReadOnlySet< Mod2 > newMods )
|
||||
public ModFileSystemSelector( ModFileSystem fileSystem, IReadOnlySet< Mod > newMods )
|
||||
: base( fileSystem )
|
||||
{
|
||||
_newMods = newMods;
|
||||
|
|
@ -26,6 +33,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
|
|||
SubscribeRightClickFolder( InheritDescendants, 15 );
|
||||
SubscribeRightClickFolder( OwnDescendants, 15 );
|
||||
AddButton( AddNewModButton, 0 );
|
||||
AddButton( AddImportModButton, 1 );
|
||||
AddButton( DeleteModButton, 1000 );
|
||||
SetFilterTooltip();
|
||||
|
||||
|
|
@ -33,6 +41,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
|
|||
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange;
|
||||
Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange;
|
||||
Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange;
|
||||
Penumbra.ModManager.ModMetaChanged += OnModMetaChange;
|
||||
Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection;
|
||||
Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection;
|
||||
OnCollectionChange( ModCollection.Type.Current, null, Penumbra.CollectionManager.Current, null );
|
||||
|
|
@ -43,6 +52,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
|
|||
base.Dispose();
|
||||
Penumbra.ModManager.ModDiscoveryStarted -= StoreCurrentSelection;
|
||||
Penumbra.ModManager.ModDiscoveryFinished -= RestoreLastSelection;
|
||||
Penumbra.ModManager.ModMetaChanged -= OnModMetaChange;
|
||||
Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange;
|
||||
Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange;
|
||||
Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange;
|
||||
|
|
@ -64,10 +74,11 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
|
|||
protected override uint FolderLineColor
|
||||
=> 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;
|
||||
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 );
|
||||
}
|
||||
|
||||
|
|
@ -107,17 +118,90 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
|
|||
|
||||
|
||||
// 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 )
|
||||
{
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size,
|
||||
"Delete the currently selected mod entirely from your drive.", SelectedLeaf == null, true ) )
|
||||
{ }
|
||||
var keys = ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyShift;
|
||||
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 _ )
|
||||
{
|
||||
SetFilterDirty();
|
||||
|
|
@ -175,17 +270,17 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
|
|||
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 )
|
||||
{
|
||||
SelectedSettings = ModSettings2.Empty;
|
||||
SelectedSettings = ModSettings.Empty;
|
||||
SelectedSettingCollection = ModCollection.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
( 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 ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
|
||||
namespace Penumbra.UI;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Numerics;
|
|||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.Collections;
|
||||
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.Diagnostics;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.UI.Classes;
|
||||
|
||||
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 void DrawModsTab()
|
||||
|
|
@ -401,14 +30,13 @@ public partial class ConfigWindow
|
|||
using var group = ImRaii.Group();
|
||||
DrawHeaderLine();
|
||||
|
||||
using var child = ImRaii.Child( "##ModsTabMod", -Vector2.One, true );
|
||||
using var child = ImRaii.Child( "##ModsTabMod", -Vector2.One, true, ImGuiWindowFlags.HorizontalScrollbar );
|
||||
if( child )
|
||||
{
|
||||
_modPanel.Draw( _selector );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Draw the header line that can quick switch between collections.
|
||||
private void DrawHeaderLine()
|
||||
{
|
||||
|
|
@ -466,4 +94,46 @@ public partial class ConfigWindow
|
|||
? absoluteSize
|
||||
: 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();
|
||||
}
|
||||
|
||||
private string? _settingsNewModDirectory;
|
||||
private string? _newModDirectory;
|
||||
private readonly FileDialogManager _dialogManager = new();
|
||||
private bool _dialogOpen;
|
||||
|
||||
|
|
@ -70,12 +70,18 @@ public partial class ConfigWindow
|
|||
}
|
||||
else
|
||||
{
|
||||
// TODO
|
||||
//_dialogManager.OpenFolderDialog( "Choose Mod Directory", ( b, s ) =>
|
||||
//{
|
||||
// _newModDirectory = b ? s : _newModDirectory;
|
||||
// _dialogOpen = false;
|
||||
//}, _newModDirectory, false);
|
||||
_newModDirectory ??= Penumbra.Config.ModDirectory;
|
||||
var startDir = Directory.Exists( _newModDirectory )
|
||||
? _newModDirectory
|
||||
: Directory.Exists( Penumbra.Config.ModDirectory )
|
||||
? Penumbra.Config.ModDirectory
|
||||
: ".";
|
||||
|
||||
_dialogManager.OpenFolderDialog( "Choose Mod Directory", ( b, s ) =>
|
||||
{
|
||||
_newModDirectory = b ? s : _newModDirectory;
|
||||
_dialogOpen = false;
|
||||
}, startDir );
|
||||
_dialogOpen = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -99,12 +105,12 @@ public partial class ConfigWindow
|
|||
|
||||
private void DrawRootFolder()
|
||||
{
|
||||
_settingsNewModDirectory ??= Penumbra.Config.ModDirectory;
|
||||
_newModDirectory ??= Penumbra.Config.ModDirectory;
|
||||
|
||||
var spacing = 3 * ImGuiHelpers.GlobalScale;
|
||||
using var group = ImRaii.Group();
|
||||
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 ) );
|
||||
ImGui.SameLine();
|
||||
DrawDirectoryPickerButton();
|
||||
|
|
@ -121,14 +127,14 @@ public partial class ConfigWindow
|
|||
var pos = ImGui.GetCursorPosX();
|
||||
ImGui.NewLine();
|
||||
|
||||
if( Penumbra.Config.ModDirectory == _settingsNewModDirectory || _settingsNewModDirectory.Length == 0 )
|
||||
if( Penumbra.Config.ModDirectory == _newModDirectory || _newModDirectory.Length == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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 );
|
||||
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();
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( "Force Penumbra to completely re-scan your root directory as if it was restarted." );
|
||||
}
|
||||
|
||||
private void DrawEnabledBox()
|
||||
|
|
|
|||
|
|
@ -21,13 +21,14 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
private readonly EffectiveTab _effectiveTab;
|
||||
private readonly DebugTab _debugTab;
|
||||
private readonly ResourceTab _resourceTab;
|
||||
public readonly SubModEditWindow SubModPopup = new();
|
||||
|
||||
public ConfigWindow( Penumbra penumbra )
|
||||
: base( GetLabel() )
|
||||
{
|
||||
_penumbra = penumbra;
|
||||
_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 );
|
||||
_collectionsTab = new CollectionsTab( this );
|
||||
_effectiveTab = new EffectiveTab();
|
||||
|
|
@ -61,6 +62,7 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
public void Dispose()
|
||||
{
|
||||
_selector.Dispose();
|
||||
_modPanel.Dispose();
|
||||
}
|
||||
|
||||
private static string GetLabel()
|
||||
|
|
@ -70,10 +72,12 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
|
||||
private Vector2 _defaultSpace;
|
||||
private Vector2 _inputTextWidth;
|
||||
private Vector2 _iconButtonSize;
|
||||
|
||||
private void SetupSizes()
|
||||
{
|
||||
_defaultSpace = new Vector2( 0, 10 * ImGuiHelpers.GlobalScale );
|
||||
_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.
|
||||
public class LaunchButton : IDisposable
|
||||
{
|
||||
private readonly ConfigWindow _configWindow;
|
||||
private readonly ConfigWindow _configWindow;
|
||||
private readonly TextureWrap? _icon;
|
||||
private readonly TitleScreenMenu.TitleScreenMenuEntry? _entry;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace Penumbra.Util;
|
||||
|
||||
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 )
|
||||
{
|
||||
for( var i = 0; i < array.Count; ++i )
|
||||
|
|
@ -61,35 +65,4 @@ public static class ArrayExtensions
|
|||
result = default;
|
||||
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;
|
||||
var time = file.CreationTimeUtc;
|
||||
if( ( oldest?.CreationTimeUtc ?? DateTime.MinValue ) < time )
|
||||
if( ( oldest?.CreationTimeUtc ?? DateTime.MaxValue ) > time )
|
||||
{
|
||||
oldest = file;
|
||||
}
|
||||
|
||||
if( ( newest?.CreationTimeUtc ?? DateTime.MaxValue ) > time )
|
||||
if( ( newest?.CreationTimeUtc ?? DateTime.MinValue ) < time )
|
||||
{
|
||||
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 ) )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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