This commit is contained in:
Ottermandias 2022-04-14 15:23:58 +02:00
parent 48e442a9fd
commit da73feacf4
59 changed files with 2115 additions and 3428 deletions

View file

@ -1,31 +0,0 @@
using System.Collections.Generic;
using System.IO;
using Penumbra.GameData.ByteString;
namespace Penumbra.Mods;
// A complete Mod containing settings (i.e. dependent on a collection)
// and the resulting cache.
public class FullMod
{
public ModSettings Settings { get; }
public Mod Data { get; }
public FullMod( ModSettings settings, Mod data )
{
Settings = settings;
Data = data;
}
public bool FixSettings()
=> Settings.FixInvalidSettings( Data.Meta );
public HashSet< Utf8GamePath > GetFiles( FileInfo file )
{
var relPath = Utf8RelPath.FromFile( file, Data.BasePath, out var p ) ? p : Utf8RelPath.Empty;
return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta );
}
public override string ToString()
=> Data.Meta.Name;
}

View file

@ -1,102 +0,0 @@
using System.Collections.Generic;
using System.ComponentModel;
using Newtonsoft.Json;
using Penumbra.GameData.ByteString;
using Penumbra.Util;
namespace Penumbra.Mods;
public enum SelectType
{
Single,
Multi,
}
public struct Option
{
public string OptionName;
public string OptionDesc;
[JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )]
public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles;
public bool AddFile( Utf8RelPath filePath, Utf8GamePath gamePath )
{
if( OptionFiles.TryGetValue( filePath, out var set ) )
{
return set.Add( gamePath );
}
OptionFiles[ filePath ] = new HashSet< Utf8GamePath > { gamePath };
return true;
}
}
public struct OptionGroup
{
public string GroupName;
[JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )]
public SelectType SelectionType;
public List< Option > Options;
private bool ApplySingleGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths )
{
// Selection contains the path, merge all GamePaths for this config.
if( Options[ selection ].OptionFiles.TryGetValue( relPath, out var groupPaths ) )
{
paths.UnionWith( groupPaths );
return true;
}
// If the group contains the file in another selection, return true to skip it for default files.
for( var i = 0; i < Options.Count; ++i )
{
if( i == selection )
{
continue;
}
if( Options[ i ].OptionFiles.ContainsKey( relPath ) )
{
return true;
}
}
return false;
}
private bool ApplyMultiGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths )
{
var doNotAdd = false;
for( var i = 0; i < Options.Count; ++i )
{
if( ( selection & ( 1 << i ) ) != 0 )
{
if( Options[ i ].OptionFiles.TryGetValue( relPath, out var groupPaths ) )
{
paths.UnionWith( groupPaths );
doNotAdd = true;
}
}
else if( Options[ i ].OptionFiles.ContainsKey( relPath ) )
{
doNotAdd = true;
}
}
return doNotAdd;
}
// Adds all game paths from the given option that correspond to the given RelPath to paths, if any exist.
internal bool ApplyGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths )
{
return SelectionType switch
{
SelectType.Single => ApplySingleGroupFiles( relPath, selection, paths ),
SelectType.Multi => ApplyMultiGroupFiles( relPath, selection, paths ),
_ => throw new InvalidEnumArgumentException( "Invalid option group type." ),
};
}
}

View file

@ -25,7 +25,6 @@ public sealed partial class Mod2
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public Manager( string modDirectory )
{
SetBaseDirectory( modDirectory, true );

View file

@ -1,43 +0,0 @@
using System;
namespace Penumbra.Mods;
public partial class Mod
{
public struct SortOrder : IComparable< SortOrder >
{
public ModFolder ParentFolder { get; set; }
private string _sortOrderName;
public string SortOrderName
{
get => _sortOrderName;
set => _sortOrderName = value.Replace( '/', '\\' );
}
public string SortOrderPath
=> ParentFolder.FullName;
public string FullName
{
get
{
var path = SortOrderPath;
return path.Length > 0 ? $"{path}/{SortOrderName}" : SortOrderName;
}
}
public SortOrder( ModFolder parentFolder, string name )
{
ParentFolder = parentFolder;
_sortOrderName = name.Replace( '/', '\\' );
}
public string FullPath
=> SortOrderPath.Length > 0 ? $"{SortOrderPath}/{SortOrderName}" : SortOrderName;
public int CompareTo( SortOrder other )
=> string.Compare( FullPath, other.FullPath, StringComparison.InvariantCultureIgnoreCase );
}
}

View file

@ -1,95 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Logging;
using Penumbra.GameData.ByteString;
namespace Penumbra.Mods;
// Mod contains all permanent information about a mod,
// and is independent of collections or settings.
// It only changes when the user actively changes the mod or their filesystem.
public sealed partial class Mod
{
public DirectoryInfo BasePath;
public ModMeta Meta;
public ModResources Resources;
public SortOrder Order;
public SortedList< string, object? > ChangedItems { get; } = new();
public string LowerChangedItemsString { get; private set; } = string.Empty;
public FileInfo MetaFile { get; set; }
public int Index { get; private set; } = -1;
private Mod( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources)
{
BasePath = basePath;
Meta = meta;
Resources = resources;
MetaFile = MetaFileInfo( basePath );
Order = new SortOrder( parentFolder, Meta.Name );
Order.ParentFolder.AddMod( this );
ComputeChangedItems();
}
public void ComputeChangedItems()
{
var identifier = GameData.GameData.GetIdentifier();
ChangedItems.Clear();
foreach( var file in Resources.ModFiles.Select( f => f.ToRelPath( BasePath, out var p ) ? p : Utf8RelPath.Empty ) )
{
foreach( var path in ModFunctions.GetAllFiles( file, Meta ) )
{
identifier.Identify( ChangedItems, path.ToGamePath() );
}
}
foreach( var path in Meta.FileSwaps.Keys )
{
identifier.Identify( ChangedItems, path.ToGamePath() );
}
LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) );
}
public static FileInfo MetaFileInfo( DirectoryInfo basePath )
=> new(Path.Combine( basePath.FullName, "meta.json" ));
public static Mod? LoadMod( ModFolder parentFolder, DirectoryInfo basePath )
{
basePath.Refresh();
if( !basePath.Exists )
{
PluginLog.Error( $"Supplied mod directory {basePath} does not exist." );
return null;
}
var metaFile = MetaFileInfo( basePath );
if( !metaFile.Exists )
{
PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name );
return null;
}
var meta = ModMeta.LoadFromFile( metaFile );
if( meta == null )
{
return null;
}
var data = new ModResources();
if( data.RefreshModFiles( basePath ).HasFlag( ResourceChange.Meta ) )
{
data.SetManipulations( meta, basePath );
}
return new Mod( parentFolder, basePath, meta, data );
}
public void SaveMeta()
=> Meta.SaveToFile( MetaFile );
public override string ToString()
=> Order.FullPath;
}

View file

@ -37,7 +37,7 @@ public partial class Mod2
mod.LoadDefaultOption();
mod.LoadAllGroups();
mod.ComputeChangedItems();
mod.SetHasOptions();
mod.SetCounts();
return mod;
}

View file

@ -0,0 +1,108 @@
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();
}
}

View file

@ -17,19 +17,31 @@ public partial class Mod2
public IReadOnlyList< IModGroup > Groups
=> _groups;
private readonly SubMod _default = new();
private readonly List< IModGroup > _groups = new();
public int TotalFileCount { get; private set; }
public int TotalSwapCount { get; private set; }
public int TotalManipulations { get; private set; }
public bool HasOptions { get; private set; }
private void SetHasOptions()
private void SetCounts()
{
TotalFileCount = 0;
TotalSwapCount = 0;
TotalManipulations = 0;
foreach( var s in AllSubMods )
{
TotalFileCount += s.Files.Count;
TotalSwapCount += s.FileSwaps.Count;
TotalManipulations += s.Manipulations.Count;
}
HasOptions = _groups.Any( o
=> o is MultiModGroup m && m.PrioritizedOptions.Count > 0
|| o is SingleModGroup s && s.OptionData.Count > 1 );
}
private readonly SubMod _default = new();
private readonly List< IModGroup > _groups = new();
public IEnumerable< ISubMod > AllSubMods
=> _groups.SelectMany( o => o ).Prepend( _default );
@ -56,6 +68,13 @@ public partial class Mod2
.ToList();
}
// Filter invalid files.
// If audio streaming is not disabled, replacing .scd files crashes the game,
// so only add those files if it is disabled.
public static bool FilterFile( Utf8GamePath gamePath )
=> !Penumbra.Config.DisableSoundStreaming
&& gamePath.Path.EndsWith( '.', 's', 'c', 'd' );
public List< FullPath > FindMissingFiles()
=> AllFiles.Where( f => !f.Exists ).ToList();

View file

@ -49,7 +49,7 @@ public sealed partial class Mod2
mod._default.FileSwapData.Add( gamePath, swapPath );
}
HandleMetaChanges( mod._default, mod.BasePath );
mod._default.IncorporateMetaChanges( mod.BasePath, false );
foreach( var group in mod.Groups )
{
IModGroup.SaveModGroup( group, mod.BasePath );
@ -119,78 +119,11 @@ public sealed partial class Mod2
}
}
private static void HandleMetaChanges( SubMod subMod, DirectoryInfo basePath )
{
foreach( var (key, file) in subMod.Files.ToList() )
{
try
{
switch( file.Extension )
{
case ".meta":
subMod.FileData.Remove( key );
if( !file.Exists )
{
continue;
}
var meta = new TexToolsMeta( File.ReadAllBytes( file.FullName ) );
foreach( var manip in meta.EqpManipulations )
{
subMod.ManipulationData.Add( manip );
}
foreach( var manip in meta.EqdpManipulations )
{
subMod.ManipulationData.Add( manip );
}
foreach( var manip in meta.EstManipulations )
{
subMod.ManipulationData.Add( manip );
}
foreach( var manip in meta.GmpManipulations )
{
subMod.ManipulationData.Add( manip );
}
foreach( var manip in meta.ImcManipulations )
{
subMod.ManipulationData.Add( manip );
}
break;
case ".rgsp":
subMod.FileData.Remove( key );
if( !file.Exists )
{
continue;
}
var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) );
foreach( var manip in rgsp.RspManipulations )
{
subMod.ManipulationData.Add( manip );
}
break;
default: continue;
}
}
catch( Exception e )
{
PluginLog.Error( $"Could not migrate meta changes in mod {basePath} from file {file.FullName}:\n{e}" );
continue;
}
}
}
private static SubMod SubModFromOption( DirectoryInfo basePath, OptionV0 option )
{
var subMod = new SubMod() { Name = option.OptionName };
AddFilesToSubMod( subMod, basePath, option );
HandleMetaChanges( subMod, basePath );
subMod.IncorporateMetaChanges( basePath, false );
return subMod;
}

File diff suppressed because it is too large Load diff

View file

@ -1,260 +0,0 @@
using System;
using System.Linq;
namespace Penumbra.Mods;
public delegate void OnModFileSystemChange();
public static partial class ModFileSystem
{
// The root folder that should be used as the base for all structured mods.
public static ModFolder Root = ModFolder.CreateRoot();
// Gets invoked every time the file system changes.
public static event OnModFileSystemChange? ModFileSystemChanged;
internal static void InvokeChange()
=> ModFileSystemChanged?.Invoke();
// Find a specific mod folder by its path from Root.
// Returns true if the folder was found, and false if not.
// The out parameter will contain the furthest existing folder.
public static bool Find( string path, out ModFolder folder )
{
var split = path.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries );
folder = Root;
foreach( var part in split )
{
if( !folder.FindSubFolder( part, out folder ) )
{
return false;
}
}
return true;
}
// Rename the SortOrderName of a single mod. Slashes are replaced by Backslashes.
// Saves and returns true if anything changed.
public static bool Rename( this global::Penumbra.Mods.Mod mod, string newName )
{
if( RenameNoSave( mod, newName ) )
{
SaveMod( mod );
return true;
}
return false;
}
// Rename the target folder, merging it and its subfolders if the new name already exists.
// Saves all mods manipulated thus, and returns true if anything changed.
public static bool Rename( this ModFolder target, string newName )
{
if( RenameNoSave( target, newName ) )
{
SaveModChildren( target );
return true;
}
return false;
}
// Move a single mod to the target folder.
// Returns true and saves if anything changed.
public static bool Move( this global::Penumbra.Mods.Mod mod, ModFolder target )
{
if( MoveNoSave( mod, target ) )
{
SaveMod( mod );
return true;
}
return false;
}
// Move a mod to the filesystem location specified by sortOrder and rename its SortOrderName.
// Creates all necessary Subfolders.
public static void Move( this global::Penumbra.Mods.Mod mod, string sortOrder )
{
var split = sortOrder.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries );
var folder = Root;
for( var i = 0; i < split.Length - 1; ++i )
{
folder = folder.FindOrCreateSubFolder( split[ i ] ).Item1;
}
if( MoveNoSave( mod, folder ) | RenameNoSave( mod, split.Last() ) )
{
SaveMod( mod );
}
}
// Moves folder to target.
// If an identically named subfolder of target already exists, merges instead.
// Root is not movable.
public static bool Move( this ModFolder folder, ModFolder target )
{
if( MoveNoSave( folder, target ) )
{
SaveModChildren( target );
return true;
}
return false;
}
// Merge source with target, moving all direct mod children of source to target,
// and moving all subfolders of source to target, or merging them with targets subfolders if they exist.
// Returns true and saves if anything changed.
public static bool Merge( this ModFolder source, ModFolder target )
{
if( MergeNoSave( source, target ) )
{
SaveModChildren( target );
return true;
}
return false;
}
}
// Internal stuff.
public static partial class ModFileSystem
{
// Reset all sort orders for all descendants of the given folder.
// Assumes that it is not called on Root, and thus does not remove unnecessary SortOrder entries.
private static void SaveModChildren( ModFolder target )
{
foreach( var mod in target.AllMods( true ) )
{
Penumbra.ModManager.TemporaryModSortOrder[ mod.BasePath.Name ] = mod.Order.FullName;
}
Penumbra.Config.Save();
InvokeChange();
}
// Sets and saves the sort order of a single mod, removing the entry if it is unnecessary.
private static void SaveMod( Mod mod )
{
if( ReferenceEquals( mod.Order.ParentFolder, Root )
&& string.Equals( mod.Order.SortOrderName, mod.Meta.Name.Text.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) )
{
Penumbra.ModManager.TemporaryModSortOrder.Remove( mod.BasePath.Name );
}
else
{
Penumbra.ModManager.TemporaryModSortOrder[ mod.BasePath.Name ] = mod.Order.FullName;
}
Penumbra.Config.Save();
InvokeChange();
}
private static bool RenameNoSave( this ModFolder target, string newName )
{
if( ReferenceEquals( target, Root ) )
{
throw new InvalidOperationException( "Can not rename root." );
}
newName = newName.Replace( '/', '\\' );
if( target.Name == newName )
{
return false;
}
ModFolder.FolderComparer.CompareType = StringComparison.InvariantCulture;
if( target.Parent!.FindSubFolder( newName, out var preExisting ) )
{
MergeNoSave( target, preExisting );
ModFolder.FolderComparer.CompareType = StringComparison.InvariantCultureIgnoreCase;
}
else
{
ModFolder.FolderComparer.CompareType = StringComparison.InvariantCultureIgnoreCase;
var parent = target.Parent;
parent.RemoveFolderIgnoreEmpty( target );
target.Name = newName;
parent.FindOrAddSubFolder( target );
}
return true;
}
private static bool RenameNoSave( Mod mod, string newName )
{
newName = newName.Replace( '/', '\\' );
if( mod.Order.SortOrderName == newName )
{
return false;
}
mod.Order.ParentFolder.RemoveModIgnoreEmpty( mod );
mod.Order = new Mod.SortOrder( mod.Order.ParentFolder, newName );
mod.Order.ParentFolder.AddMod( mod );
return true;
}
private static bool MoveNoSave( Mod mod, ModFolder target )
{
var oldParent = mod.Order.ParentFolder;
if( ReferenceEquals( target, oldParent ) )
{
return false;
}
oldParent.RemoveMod( mod );
mod.Order = new Mod.SortOrder( target, mod.Order.SortOrderName );
target.AddMod( mod );
return true;
}
private static bool MergeNoSave( ModFolder source, ModFolder target )
{
if( ReferenceEquals( source, target ) )
{
return false;
}
var any = false;
while( source.SubFolders.Count > 0 )
{
any |= MoveNoSave( source.SubFolders.First(), target );
}
while( source.Mods.Count > 0 )
{
any |= MoveNoSave( source.Mods.First(), target );
}
source.Parent?.RemoveSubFolder( source );
return any || source.Parent != null;
}
private static bool MoveNoSave( ModFolder folder, ModFolder target )
{
// Moving a folder into itself is not permitted.
if( ReferenceEquals( folder, target ) )
{
return false;
}
if( ReferenceEquals( target, folder.Parent! ) )
{
return false;
}
folder.Parent!.RemoveSubFolder( folder );
var subFolderIdx = target.FindOrAddSubFolder( folder );
if( subFolderIdx > 0 )
{
var main = target.SubFolders[ subFolderIdx ];
MergeNoSave( folder, main );
}
return true;
}
}

View file

@ -4,13 +4,13 @@ using OtterGui.Filesystem;
namespace Penumbra.Mods;
public sealed class ModFileSystemA : FileSystem< Mod >, IDisposable
public sealed class ModFileSystemA : FileSystem< Mod2 >, IDisposable
{
// 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( Mod.Manager.SortOrderFile ), SaveMod, true );
=> SaveToFile( new FileInfo( Mod2.Manager.ModFileSystemFile ), SaveMod, true );
// Create a new ModFileSystem from the currently loaded mods and the current sort order file.
public static ModFileSystemA Load()
@ -31,7 +31,7 @@ public sealed class ModFileSystemA : FileSystem< Mod >, IDisposable
// Used on construction and on mod rediscoveries.
private void Reload()
{
if( Load( new FileInfo( Mod.Manager.SortOrderFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) )
if( Load( new FileInfo( Mod2.Manager.ModFileSystemFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) )
{
Save();
}
@ -47,13 +47,13 @@ public sealed class ModFileSystemA : FileSystem< Mod >, IDisposable
}
// Used for saving and loading.
private static string ModToIdentifier( Mod mod )
private static string ModToIdentifier( Mod2 mod )
=> mod.BasePath.Name;
private static string ModToName( Mod mod )
=> mod.Meta.Name.Text;
private static string ModToName( Mod2 mod )
=> mod.Name.Text;
private static (string, bool) SaveMod( Mod mod, string fullPath )
private static (string, bool) SaveMod( Mod2 mod, string fullPath )
{
// Only save pairs with non-default paths.
if( fullPath == ModToName( mod ) )

View file

@ -1,245 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Penumbra.Mods;
public partial class ModFolder
{
public ModFolder? Parent;
public string FullName
{
get
{
var parentPath = Parent?.FullName ?? string.Empty;
return parentPath.Any() ? $"{parentPath}/{Name}" : Name;
}
}
private string _name = string.Empty;
public string Name
{
get => _name;
set => _name = value.Replace( '/', '\\' );
}
public List< ModFolder > SubFolders { get; } = new();
public List< Mod > Mods { get; } = new();
public ModFolder( ModFolder parent, string name )
{
Parent = parent;
Name = name;
}
public override string ToString()
=> FullName;
public int TotalDescendantMods()
=> Mods.Count + SubFolders.Sum( f => f.TotalDescendantMods() );
public int TotalDescendantFolders()
=> SubFolders.Sum( f => f.TotalDescendantFolders() );
// Return all descendant mods in the specified order.
public IEnumerable< Mod > AllMods( bool foldersFirst )
{
if( foldersFirst )
{
return SubFolders.SelectMany( f => f.AllMods( foldersFirst ) ).Concat( Mods );
}
return GetSortedEnumerator().SelectMany( f =>
{
if( f is ModFolder folder )
{
return folder.AllMods( false );
}
return new[] { ( Mod )f };
} );
}
// Return all descendant subfolders.
public IEnumerable< ModFolder > AllFolders()
=> SubFolders.SelectMany( f => f.AllFolders() ).Prepend( this );
// Iterate through all descendants in the specified order, returning subfolders as well as mods.
public IEnumerable< object > GetItems( bool foldersFirst )
=> foldersFirst ? SubFolders.Cast< object >().Concat( Mods ) : GetSortedEnumerator();
// Find a subfolder by name. Returns true and sets folder to it if it exists.
public bool FindSubFolder( string name, out ModFolder folder )
{
var subFolder = new ModFolder( this, name );
var idx = SubFolders.BinarySearch( subFolder, FolderComparer );
folder = idx >= 0 ? SubFolders[ idx ] : this;
return idx >= 0;
}
// Checks if an equivalent subfolder as folder already exists and returns its index.
// If it does not exist, inserts folder as a subfolder and returns the new index.
// Also sets this as folders parent.
public int FindOrAddSubFolder( ModFolder folder )
{
var idx = SubFolders.BinarySearch( folder, FolderComparer );
if( idx >= 0 )
{
return idx;
}
idx = ~idx;
SubFolders.Insert( idx, folder );
folder.Parent = this;
return idx;
}
// Checks if a subfolder with the given name already exists and returns it and its index.
// If it does not exists, creates and inserts it and returns the new subfolder and its index.
public (ModFolder, int) FindOrCreateSubFolder( string name )
{
var subFolder = new ModFolder( this, name );
var idx = FindOrAddSubFolder( subFolder );
return ( SubFolders[ idx ], idx );
}
// Remove folder as a subfolder if it exists.
// If this folder is empty afterwards, remove it from its parent.
public void RemoveSubFolder( ModFolder folder )
{
RemoveFolderIgnoreEmpty( folder );
CheckEmpty();
}
// Add the given mod as a child, if it is not already a child.
// Returns the index of the found or inserted mod.
public int AddMod( Mod mod )
{
var idx = Mods.BinarySearch( mod, ModComparer );
if( idx >= 0 )
{
return idx;
}
idx = ~idx;
Mods.Insert( idx, mod );
return idx;
}
// Remove mod as a child if it exists.
// If this folder is empty afterwards, remove it from its parent.
public void RemoveMod( Mod mod )
{
RemoveModIgnoreEmpty( mod );
CheckEmpty();
}
}
// Internals
public partial class ModFolder
{
// Create a Root folder without parent.
internal static ModFolder CreateRoot()
=> new(null!, string.Empty);
internal class ModFolderComparer : IComparer< ModFolder >
{
public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase;
// Compare only the direct folder names since this is only used inside an enumeration of subfolders of one folder.
public int Compare( ModFolder? x, ModFolder? y )
=> ReferenceEquals( x, y )
? 0
: string.Compare( x?.Name ?? string.Empty, y?.Name ?? string.Empty, CompareType );
}
internal class ModDataComparer : IComparer< Mod >
{
public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase;
// Compare only the direct SortOrderNames since this is only used inside an enumeration of direct mod children of one folder.
// Since mod SortOrderNames do not have to be unique inside a folder, also compare their BasePaths (and thus their identity) if necessary.
public int Compare( Mod? x, Mod? y )
{
if( ReferenceEquals( x, y ) )
{
return 0;
}
var cmp = string.Compare( x?.Order.SortOrderName, y?.Order.SortOrderName, CompareType );
if( cmp != 0 )
{
return cmp;
}
return string.Compare( x?.BasePath.Name, y?.BasePath.Name, StringComparison.InvariantCulture );
}
}
internal static readonly ModFolderComparer FolderComparer = new();
internal static readonly ModDataComparer ModComparer = new();
// Get an enumerator for actually sorted objects instead of folder-first objects.
private IEnumerable< object > GetSortedEnumerator()
{
var modIdx = 0;
foreach( var folder in SubFolders )
{
var folderString = folder.Name;
for( ; modIdx < Mods.Count; ++modIdx )
{
var mod = Mods[ modIdx ];
var modString = mod.Order.SortOrderName;
if( string.Compare( folderString, modString, StringComparison.InvariantCultureIgnoreCase ) > 0 )
{
yield return mod;
}
else
{
break;
}
}
yield return folder;
}
for( ; modIdx < Mods.Count; ++modIdx )
{
yield return Mods[ modIdx ];
}
}
private void CheckEmpty()
{
if( Mods.Count == 0 && SubFolders.Count == 0 )
{
Parent?.RemoveSubFolder( this );
}
}
// Remove a subfolder but do not remove this folder from its parent if it is empty afterwards.
internal void RemoveFolderIgnoreEmpty( ModFolder folder )
{
var idx = SubFolders.BinarySearch( folder, FolderComparer );
if( idx < 0 )
{
return;
}
SubFolders[ idx ].Parent = null;
SubFolders.RemoveAt( idx );
}
// Remove a mod, but do not remove this folder from its parent if it is empty afterwards.
internal void RemoveModIgnoreEmpty( Mod mod )
{
var idx = Mods.BinarySearch( mod, ModComparer );
if( idx >= 0 )
{
Mods.RemoveAt( idx );
}
}
}

View file

@ -1,100 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.GameData.ByteString;
namespace Penumbra.Mods;
// Functions that do not really depend on only one component of a mod.
public static class ModFunctions
{
public static bool CleanUpCollection( Dictionary< string, ModSettings > settings, IEnumerable< DirectoryInfo > modPaths )
{
var hashes = modPaths.Select( p => p.Name ).ToHashSet();
var missingMods = settings.Keys.Where( k => !hashes.Contains( k ) ).ToArray();
var anyChanges = false;
foreach( var toRemove in missingMods )
{
anyChanges |= settings.Remove( toRemove );
}
return anyChanges;
}
public static HashSet< Utf8GamePath > GetFilesForConfig( Utf8RelPath relPath, ModSettings settings, ModMeta meta )
{
var doNotAdd = false;
var files = new HashSet< Utf8GamePath >();
foreach( var group in meta.Groups.Values.Where( g => g.Options.Count > 0 ) )
{
doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files );
}
if( !doNotAdd )
{
files.Add( relPath.ToGamePath() );
}
return files;
}
public static HashSet< Utf8GamePath > GetAllFiles( Utf8RelPath relPath, ModMeta meta )
{
var ret = new HashSet< Utf8GamePath >();
foreach( var option in meta.Groups.Values.SelectMany( g => g.Options ) )
{
if( option.OptionFiles.TryGetValue( relPath, out var files ) )
{
ret.UnionWith( files );
}
}
if( ret.Count == 0 )
{
ret.Add( relPath.ToGamePath() );
}
return ret;
}
public static ModSettings ConvertNamedSettings( NamedModSettings namedSettings, ModMeta meta )
{
ModSettings ret = new()
{
Priority = namedSettings.Priority,
Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ),
};
foreach( var setting in namedSettings.Settings.Keys )
{
if( !meta.Groups.TryGetValue( setting, out var info ) )
{
continue;
}
if( info.SelectionType == SelectType.Single )
{
if( namedSettings.Settings[ setting ].Count == 0 )
{
ret.Settings[ setting ] = 0;
}
else
{
var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ setting ].Last() );
ret.Settings[ setting ] = idx < 0 ? 0 : idx;
}
}
else
{
foreach( var idx in namedSettings.Settings[ setting ]
.Select( option => info.Options.FindIndex( o => o.OptionName == option ) )
.Where( idx => idx >= 0 ) )
{
ret.Settings[ setting ] |= 1 << idx;
}
}
}
return ret;
}
}

View file

@ -1,335 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Logging;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.ByteString;
using Penumbra.Meta;
using Penumbra.Util;
namespace Penumbra.Mods;
public partial class Mod
{
public enum ChangeType
{
Added,
Removed,
Changed,
}
// The ModManager handles the basic mods installed to the mod directory.
// It also contains the CollectionManager that handles all collections.
public class Manager : IEnumerable< Mod >
{
public DirectoryInfo BasePath { get; private set; } = null!;
private readonly List< Mod > _mods = new();
public Mod this[ int idx ]
=> _mods[ idx ];
public IReadOnlyList< Mod > Mods
=> _mods;
public IEnumerator< Mod > GetEnumerator()
=> _mods.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public ModFolder StructuredMods { get; } = ModFileSystem.Root;
public delegate void ModChangeDelegate( ChangeType type, Mod mod );
public event ModChangeDelegate? ModChange;
public event Action? ModDiscoveryStarted;
public event Action? ModDiscoveryFinished;
public bool Valid { get; private set; }
public int Count
=> _mods.Count;
public Configuration Config
=> Penumbra.Config;
public void DiscoverMods( string newDir )
{
SetBaseDirectory( newDir, false );
DiscoverMods();
}
private void SetBaseDirectory( string newPath, bool firstTime )
{
if( !firstTime && string.Equals( newPath, Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) )
{
return;
}
if( newPath.Length == 0 )
{
Valid = false;
BasePath = new DirectoryInfo( "." );
}
else
{
var newDir = new DirectoryInfo( newPath );
if( !newDir.Exists )
{
try
{
Directory.CreateDirectory( newDir.FullName );
newDir.Refresh();
}
catch( Exception e )
{
PluginLog.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" );
}
}
if( !firstTime )
{
HandleSortOrderFiles( newDir );
}
BasePath = newDir;
Valid = true;
if( Config.ModDirectory != BasePath.FullName )
{
Config.ModDirectory = BasePath.FullName;
Config.Save();
}
}
}
private const string SortOrderFileName = "sort_order.json";
public static string SortOrderFile = Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), SortOrderFileName );
private void HandleSortOrderFiles( DirectoryInfo newDir )
{
try
{
var mainFile = SortOrderFile;
// Copy old sort order to backup.
var oldSortOrderFile = Path.Combine( BasePath.FullName, SortOrderFileName );
PluginLog.Debug( "Copying current sort older file to {BackupFile}...", oldSortOrderFile );
File.Copy( mainFile, oldSortOrderFile, true );
BasePath = newDir;
var newSortOrderFile = Path.Combine( newDir.FullName, SortOrderFileName );
// Copy new sort order to main, if it exists.
if( File.Exists( newSortOrderFile ) )
{
File.Copy( newSortOrderFile, mainFile, true );
PluginLog.Debug( "Copying stored sort order file from {BackupFile}...", newSortOrderFile );
}
else
{
File.Delete( mainFile );
PluginLog.Debug( "Deleting current sort order file...", newSortOrderFile );
}
}
catch( Exception e )
{
PluginLog.Error( $"Could not swap Sort Order files:\n{e}" );
}
}
public Manager()
{
SetBaseDirectory( Config.ModDirectory, true );
// TODO
try
{
var data = JObject.Parse( File.ReadAllText( SortOrderFile ) );
TemporaryModSortOrder = data[ "Data" ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >();
}
catch
{
TemporaryModSortOrder = new Dictionary< string, string >();
}
}
public Dictionary< string, string > TemporaryModSortOrder;
private bool SetSortOrderPath( Mod mod, string path )
{
mod.Move( path );
var fixedPath = mod.Order.FullPath;
if( fixedPath.Length == 0 || string.Equals( fixedPath, mod.Meta.Name, StringComparison.InvariantCultureIgnoreCase ) )
{
Penumbra.ModManager.TemporaryModSortOrder.Remove( mod.BasePath.Name );
return true;
}
if( path != fixedPath )
{
TemporaryModSortOrder[ mod.BasePath.Name ] = fixedPath;
return true;
}
return false;
}
private void SetModStructure( bool removeOldPaths = false )
{
var changes = false;
foreach( var (folder, path) in TemporaryModSortOrder.ToArray() )
{
if( path.Length > 0 && _mods.FindFirst( m => m.BasePath.Name == folder, out var mod ) )
{
changes |= SetSortOrderPath( mod, path );
}
else if( removeOldPaths )
{
changes = true;
TemporaryModSortOrder.Remove( folder );
}
}
if( changes )
{
Config.Save();
}
}
public void DiscoverMods()
{
ModDiscoveryStarted?.Invoke();
_mods.Clear();
BasePath.Refresh();
StructuredMods.SubFolders.Clear();
StructuredMods.Mods.Clear();
if( Valid && BasePath.Exists )
{
foreach( var modFolder in BasePath.EnumerateDirectories() )
{
var mod = LoadMod( StructuredMods, modFolder );
if( mod == null )
{
continue;
}
mod.Index = _mods.Count;
_mods.Add( mod );
}
SetModStructure();
}
ModDiscoveryFinished?.Invoke();
}
public void DeleteMod( DirectoryInfo modFolder )
{
if( Directory.Exists( modFolder.FullName ) )
{
try
{
Directory.Delete( modFolder.FullName, true );
}
catch( Exception e )
{
PluginLog.Error( $"Could not delete the mod {modFolder.Name}:\n{e}" );
}
}
var idx = _mods.FindIndex( m => m.BasePath.Name == modFolder.Name );
if( idx >= 0 )
{
var mod = _mods[ idx ];
mod.Order.ParentFolder.RemoveMod( mod );
_mods.RemoveAt( idx );
for( var i = idx; i < _mods.Count; ++i )
{
--_mods[ i ].Index;
}
ModChange?.Invoke( ChangeType.Removed, mod );
}
}
public int AddMod( DirectoryInfo modFolder )
{
var mod = LoadMod( StructuredMods, modFolder );
if( mod == null )
{
return -1;
}
if( TemporaryModSortOrder.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, mod );
return _mods.Count - 1;
}
public bool UpdateMod( int idx, bool reloadMeta = false, bool recomputeMeta = false, bool force = false )
{
var mod = Mods[ idx ];
var oldName = mod.Meta.Name;
var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) != 0 || force;
var fileChanges = mod.Resources.RefreshModFiles( mod.BasePath );
if( !recomputeMeta && !reloadMeta && !metaChanges && fileChanges == 0 )
{
return false;
}
if( metaChanges || fileChanges.HasFlag( ResourceChange.Files ) )
{
mod.ComputeChangedItems();
if( TemporaryModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) )
{
mod.Move( sortOrder );
var path = mod.Order.FullPath;
if( path != sortOrder )
{
TemporaryModSortOrder[ mod.BasePath.Name ] = path;
Config.Save();
}
}
else
{
mod.Order = new SortOrder( StructuredMods, mod.Meta.Name );
}
}
var nameChange = !string.Equals( oldName, mod.Meta.Name, StringComparison.InvariantCulture );
recomputeMeta |= fileChanges.HasFlag( ResourceChange.Meta );
if( recomputeMeta )
{
mod.Resources.MetaManipulations.Update( mod.Resources.MetaFiles, mod.BasePath, mod.Meta );
mod.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( mod.BasePath ) );
}
// TODO: more specific mod changes?
ModChange?.Invoke( ChangeType.Changed, mod );
return true;
}
public bool UpdateMod( Mod mod, bool reloadMeta = false, bool recomputeMeta = false, bool force = false )
=> UpdateMod( Mods.IndexOf( mod ), reloadMeta, recomputeMeta, force );
public static FullPath? ResolvePath( Utf8GamePath gameResourcePath )
=> Penumbra.CollectionManager.Default.ResolvePath( gameResourcePath );
}
}

View file

@ -1,211 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using Dalamud.Logging;
using Penumbra.Util;
namespace Penumbra.Mods;
// Extracted to keep the main file a bit more clean.
// Contains all change functions on a specific mod that also require corresponding changes to collections.
public static class ModManagerEditExtensions
{
public static bool RenameMod( this Mod.Manager manager, string newName, Mod mod )
{
if( newName.Length == 0 || string.Equals( newName, mod.Meta.Name, StringComparison.InvariantCulture ) )
{
return false;
}
mod.Meta.Name = newName;
mod.SaveMeta();
return true;
}
public static bool ChangeSortOrder( this Mod.Manager manager, Mod mod, string newSortOrder )
{
if( string.Equals( mod.Order.FullPath, newSortOrder, StringComparison.InvariantCultureIgnoreCase ) )
{
return false;
}
var inRoot = new Mod.SortOrder( manager.StructuredMods, mod.Meta.Name );
if( newSortOrder == string.Empty || newSortOrder == inRoot.SortOrderName )
{
mod.Order = inRoot;
manager.TemporaryModSortOrder.Remove( mod.BasePath.Name );
}
else
{
mod.Move( newSortOrder );
manager.TemporaryModSortOrder[ mod.BasePath.Name ] = mod.Order.FullPath;
}
Penumbra.Config.Save();
return true;
}
public static bool RenameModFolder( this Mod.Manager manager, Mod mod, DirectoryInfo newDir, bool move = true )
{
if( move )
{
newDir.Refresh();
if( newDir.Exists )
{
return false;
}
var oldDir = new DirectoryInfo( mod.BasePath.FullName );
try
{
oldDir.MoveTo( newDir.FullName );
}
catch( Exception e )
{
PluginLog.Error( $"Error while renaming directory {oldDir.FullName} to {newDir.FullName}:\n{e}" );
return false;
}
}
var oldBasePath = mod.BasePath;
mod.BasePath = newDir;
mod.MetaFile = Mod.MetaFileInfo( newDir );
manager.UpdateMod( mod );
if( manager.TemporaryModSortOrder.ContainsKey( oldBasePath.Name ) )
{
manager.TemporaryModSortOrder[ newDir.Name ] = manager.TemporaryModSortOrder[ oldBasePath.Name ];
manager.TemporaryModSortOrder.Remove( oldBasePath.Name );
Penumbra.Config.Save();
}
var idx = manager.Mods.IndexOf( mod );
foreach( var collection in Penumbra.CollectionManager )
{
if( collection.Settings[ idx ] != null )
{
collection.Save();
}
}
return true;
}
public static bool ChangeModGroup( this Mod.Manager manager, string oldGroupName, string newGroupName, Mod mod,
SelectType type = SelectType.Single )
{
if( newGroupName == oldGroupName || mod.Meta.Groups.ContainsKey( newGroupName ) )
{
return false;
}
if( mod.Meta.Groups.TryGetValue( oldGroupName, out var oldGroup ) )
{
if( newGroupName.Length > 0 )
{
mod.Meta.Groups[ newGroupName ] = new OptionGroup()
{
GroupName = newGroupName,
SelectionType = oldGroup.SelectionType,
Options = oldGroup.Options,
};
}
mod.Meta.Groups.Remove( oldGroupName );
}
else
{
if( newGroupName.Length == 0 )
{
return false;
}
mod.Meta.Groups[ newGroupName ] = new OptionGroup()
{
GroupName = newGroupName,
SelectionType = type,
Options = new List< Option >(),
};
}
mod.SaveMeta();
// TODO to indices
var idx = Penumbra.ModManager.Mods.IndexOf( mod );
foreach( var collection in Penumbra.CollectionManager )
{
var settings = collection.Settings[ idx ];
if( settings == null )
{
continue;
}
if( newGroupName.Length > 0 )
{
settings.Settings[ newGroupName ] = settings.Settings.TryGetValue( oldGroupName, out var value ) ? value : 0;
}
settings.Settings.Remove( oldGroupName );
collection.Save();
}
return true;
}
public static bool RemoveModOption( this Mod.Manager manager, int optionIdx, OptionGroup group, Mod mod )
{
if( optionIdx < 0 || optionIdx >= group.Options.Count )
{
return false;
}
group.Options.RemoveAt( optionIdx );
mod.SaveMeta();
static int MoveMultiSetting( int oldSetting, int idx )
{
var bitmaskFront = ( 1 << idx ) - 1;
var bitmaskBack = ~( bitmaskFront | ( 1 << idx ) );
return ( oldSetting & bitmaskFront ) | ( ( oldSetting & bitmaskBack ) >> 1 );
}
var idx = Penumbra.ModManager.Mods.IndexOf( mod ); // TODO
foreach( var collection in Penumbra.CollectionManager )
{
var settings = collection.Settings[ idx ];
if( settings == null )
{
continue;
}
if( !settings.Settings.TryGetValue( group.GroupName, out var setting ) )
{
setting = 0;
}
var newSetting = group.SelectionType switch
{
SelectType.Single => setting >= optionIdx ? setting - 1 : setting,
SelectType.Multi => MoveMultiSetting( setting, optionIdx ),
_ => throw new InvalidEnumArgumentException(),
};
if( newSetting != setting )
{
settings.Settings[ group.GroupName ] = newSetting;
collection.Save();
if( collection.HasCache && settings.Enabled )
{
collection.CalculateEffectiveFileList( mod.Resources.MetaManipulations.Count > 0,
Penumbra.CollectionManager.Default == collection );
}
}
}
return true;
}
}

View file

@ -1,184 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.GameData.ByteString;
using Penumbra.Util;
namespace Penumbra.Mods;
// Contains descriptive data about the mod as well as possible settings and fileswaps.
public class ModMeta
{
public const uint CurrentFileVersion = 1;
[Flags]
public enum ChangeType : byte
{
Name = 0x01,
Author = 0x02,
Description = 0x04,
Version = 0x08,
Website = 0x10,
Deletion = 0x20,
}
public uint FileVersion { get; set; } = CurrentFileVersion;
public LowerString Name { get; set; } = "Mod";
public LowerString Author { get; set; } = LowerString.Empty;
public string Description { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public string Website { get; set; } = string.Empty;
public bool HasGroupsWithConfig = false;
public bool RefreshHasGroupsWithConfig()
{
var oldValue = HasGroupsWithConfig;
HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 );
return oldValue != HasGroupsWithConfig;
}
public ChangeType RefreshFromFile( FileInfo filePath )
{
var newMeta = LoadFromFile( filePath );
if( newMeta == null )
{
return ChangeType.Deletion;
}
ChangeType changes = 0;
if( Name != newMeta.Name )
{
changes |= ChangeType.Name;
Name = newMeta.Name;
}
if( Author != newMeta.Author )
{
changes |= ChangeType.Author;
Author = newMeta.Author;
}
if( Description != newMeta.Description )
{
changes |= ChangeType.Description;
Description = newMeta.Description;
}
if( Version != newMeta.Version )
{
changes |= ChangeType.Version;
Version = newMeta.Version;
}
if( Website != newMeta.Website )
{
changes |= ChangeType.Website;
Website = newMeta.Website;
}
return changes;
}
[JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )]
public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new();
public Dictionary< string, OptionGroup > Groups { get; set; } = new();
public static ModMeta? LoadFromFile( FileInfo filePath )
{
try
{
var text = File.ReadAllText( filePath.FullName );
var meta = JsonConvert.DeserializeObject< ModMeta >( text,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } );
if( meta != null )
{
meta.RefreshHasGroupsWithConfig();
Migration.Migrate( meta, text );
}
return meta;
}
catch( Exception e )
{
PluginLog.Error( $"Could not load mod meta:\n{e}" );
return null;
}
}
public void SaveToFile( FileInfo filePath )
{
try
{
var text = JsonConvert.SerializeObject( this, Formatting.Indented );
File.WriteAllText( filePath.FullName, text );
}
catch( Exception e )
{
PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" );
}
}
private static class Migration
{
public static void Migrate( ModMeta meta, string text )
{
MigrateV0ToV1( meta, text );
}
private static void MigrateV0ToV1( ModMeta meta, string text )
{
if( meta.FileVersion > 0 )
{
return;
}
var data = JObject.Parse( text );
var swaps = data[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >()
?? new Dictionary< Utf8GamePath, FullPath >();
var groups = data[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >();
foreach( var group in groups.Values )
{ }
foreach( var swap in swaps )
{ }
//var meta =
}
private struct OptionV0
{
public string OptionName = string.Empty;
public string OptionDesc = string.Empty;
[JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )]
public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles = new();
public OptionV0()
{ }
}
private struct OptionGroupV0
{
public string GroupName = string.Empty;
[JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )]
public SelectType SelectionType = SelectType.Single;
public List< OptionV0 > Options = new();
public OptionGroupV0()
{ }
}
}
}

View file

@ -1,89 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.GameData.ByteString;
using Penumbra.Meta;
namespace Penumbra.Mods;
[Flags]
public enum ResourceChange
{
None = 0,
Files = 1,
Meta = 2,
}
// Contains static mod data that should only change on filesystem changes.
public class ModResources
{
public List< FullPath > ModFiles { get; private set; } = new();
public List< FullPath > MetaFiles { get; private set; } = new();
public MetaCollection MetaManipulations { get; private set; } = new();
private void ForceManipulationsUpdate( ModMeta meta, DirectoryInfo basePath )
{
MetaManipulations.Update( MetaFiles, basePath, meta );
MetaManipulations.SaveToFile( MetaCollection.FileName( basePath ) );
}
public void SetManipulations( ModMeta meta, DirectoryInfo basePath, bool validate = true )
{
var newManipulations = MetaCollection.LoadFromFile( MetaCollection.FileName( basePath ) );
if( newManipulations == null )
{
ForceManipulationsUpdate( meta, basePath );
}
else
{
MetaManipulations = newManipulations;
if( validate && !MetaManipulations.Validate( meta ) )
{
ForceManipulationsUpdate( meta, basePath );
}
}
}
// Update the current set of files used by the mod,
// returns true if anything changed.
public ResourceChange RefreshModFiles( DirectoryInfo basePath )
{
List< FullPath > tmpFiles = new(ModFiles.Count);
List< FullPath > tmpMetas = new(MetaFiles.Count);
// we don't care about any _files_ in the root dir, but any folders should be a game folder/file combo
foreach( var file in basePath.EnumerateDirectories()
.SelectMany( dir => dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
.Select( f => new FullPath( f ) )
.OrderBy( f => f.FullName ) )
{
switch( file.Extension.ToLowerInvariant() )
{
case ".meta":
case ".rgsp":
tmpMetas.Add( file );
break;
default:
tmpFiles.Add( file );
break;
}
}
ResourceChange changes = 0;
if( !tmpFiles.Select( f => f.FullName ).SequenceEqual( ModFiles.Select( f => f.FullName ) ) )
{
ModFiles = tmpFiles;
changes |= ResourceChange.Files;
}
if( !tmpMetas.Select( f => f.FullName ).SequenceEqual( MetaFiles.Select( f => f.FullName ) ) )
{
MetaFiles = tmpMetas;
changes |= ResourceChange.Meta;
}
return changes;
}
}

View file

@ -1,46 +0,0 @@
using System.Collections.Generic;
using System.Linq;
namespace Penumbra.Mods;
// Contains settings with the option selections stored by names instead of index.
// This is meant to make them possibly more portable when we support importing collections from other users.
// Enabled does not exist, because disabled mods would not be exported in this way.
public class NamedModSettings
{
public int Priority { get; set; }
public Dictionary< string, HashSet< string > > Settings { get; set; } = new();
public void AddFromModSetting( ModSettings s, ModMeta meta )
{
Priority = s.Priority;
Settings = s.Settings.Keys.ToDictionary( k => k, _ => new HashSet< string >() );
foreach( var kvp in Settings )
{
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
{
continue;
}
var setting = s.Settings[ kvp.Key ];
if( info.SelectionType == SelectType.Single )
{
var name = setting < info.Options.Count
? info.Options[ setting ].OptionName
: info.Options[ 0 ].OptionName;
kvp.Value.Add( name );
}
else
{
for( var i = 0; i < info.Options.Count; ++i )
{
if( ( ( setting >> i ) & 1 ) != 0 )
{
kvp.Value.Add( info.Options[ i ].OptionName );
}
}
}
}
}
}

View file

@ -6,6 +6,7 @@ using Dalamud.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.ByteString;
using Penumbra.Importer;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Mods;
@ -113,5 +114,82 @@ public partial class Mod2
}
}
}
public void IncorporateMetaChanges( DirectoryInfo basePath, bool delete )
{
foreach( var (key, file) in Files.ToList() )
{
try
{
switch( file.Extension )
{
case ".meta":
FileData.Remove( key );
if( !file.Exists )
{
continue;
}
var meta = new TexToolsMeta( File.ReadAllBytes( file.FullName ) );
if( delete )
{
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 );
}
break;
case ".rgsp":
FileData.Remove( key );
if( !file.Exists )
{
continue;
}
var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) );
if( delete )
{
File.Delete( file.FullName );
}
foreach( var manip in rgsp.RspManipulations )
{
ManipulationData.Add( manip );
}
break;
default: continue;
}
}
catch( Exception e )
{
PluginLog.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" );
continue;
}
}
}
}
}

View file

@ -4,72 +4,140 @@ using System.Linq;
namespace Penumbra.Mods;
// Contains the settings for a given mod.
public class ModSettings
public class ModSettings2
{
public static readonly ModSettings Empty = new();
public bool Enabled { get; set; }
public static readonly ModSettings2 Empty = new();
public List< uint > Settings { get; init; } = new();
public int Priority { get; set; }
public Dictionary< string, int > Settings { get; set; } = new();
public bool Enabled { get; set; }
// For backwards compatibility
private Dictionary< string, int > Conf
{
set => Settings = value;
}
public ModSettings DeepCopy()
{
var settings = new ModSettings
public ModSettings2 DeepCopy()
=> new()
{
Enabled = Enabled,
Priority = Priority,
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ),
Settings = Settings.ToList(),
};
return settings;
}
public static ModSettings DefaultSettings( ModMeta meta )
{
return new ModSettings
public static ModSettings2 DefaultSettings( Mod2 mod )
=> new()
{
Enabled = false,
Priority = 0,
Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ),
Settings = Enumerable.Repeat( 0u, mod.Groups.Count ).ToList(),
};
public void HandleChanges( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx )
{
switch( type )
{
case ModOptionChangeType.GroupAdded:
Settings.Insert( groupIdx, 0 );
break;
case ModOptionChangeType.GroupDeleted:
Settings.RemoveAt( groupIdx );
break;
case ModOptionChangeType.OptionDeleted:
var group = mod.Groups[ groupIdx ];
var config = Settings[ groupIdx ];
Settings[ groupIdx ] = group.Type switch
{
SelectType.Single => config >= optionIdx ? Math.Max( 0, config - 1 ) : config,
SelectType.Multi => RemoveBit( config, optionIdx ),
_ => config,
};
break;
}
}
public void SetValue( Mod2 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,
};
}
public bool FixSpecificSetting( string name, ModMeta meta )
private static uint RemoveBit( uint config, int bit )
{
if( !meta.Groups.TryGetValue( name, out var group ) )
{
return Settings.Remove( name );
}
if( Settings.TryGetValue( name, out var oldSetting ) )
{
Settings[ name ] = group.SelectionType switch
{
SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ),
SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ),
_ => Settings[ group.GroupName ],
};
return oldSetting != Settings[ group.GroupName ];
}
Settings[ name ] = 0;
return true;
var lowMask = ( 1u << bit ) - 1u;
var highMask = ~( ( 1u << ( bit + 1 ) ) - 1u );
var low = config & lowMask;
var high = ( config & highMask ) >> 1;
return low | high;
}
public bool FixInvalidSettings( ModMeta meta )
internal bool AddMissingSettings( int totalCount )
{
if( meta.Groups.Count == 0 )
if( totalCount <= Settings.Count )
{
return false;
}
return Settings.Keys.ToArray().Union( meta.Groups.Keys )
.Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, meta ) );
Settings.AddRange( Enumerable.Repeat( 0u, totalCount - Settings.Count ) );
return true;
}
public struct SavedSettings
{
public Dictionary< string, uint > Settings;
public int Priority;
public bool Enabled;
public SavedSettings DeepCopy()
=> new()
{
Enabled = Enabled,
Priority = Priority,
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ),
};
public SavedSettings( ModSettings2 settings, Mod2 mod )
{
Priority = settings.Priority;
Enabled = settings.Enabled;
Settings = new Dictionary< string, uint >( mod.Groups.Count );
settings.AddMissingSettings( mod.Groups.Count );
foreach( var (group, setting) in mod.Groups.Zip( settings.Settings ) )
{
Settings.Add( group.Name, setting );
}
}
public bool ToSettings( Mod2 mod, out ModSettings2 settings )
{
var list = new List< uint >( mod.Groups.Count );
var changes = Settings.Count != mod.Groups.Count;
foreach( var group in mod.Groups )
{
if( Settings.TryGetValue( group.Name, out var config ) )
{
list.Add( config );
}
else
{
list.Add( 0 );
changes = true;
}
}
settings = new ModSettings2
{
Enabled = Enabled,
Priority = Priority,
Settings = list,
};
return changes;
}
}
}

View file

@ -0,0 +1,7 @@
namespace Penumbra.Mods;
public enum SelectType
{
Single,
Multi,
}