mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-01-03 06:13:45 +01:00
Complete mod collection cleanup, initial stuff for inheritance. Some further cleanup.
This commit is contained in:
parent
7915d516e2
commit
1861c40a4f
48 changed files with 1151 additions and 898 deletions
31
Penumbra/Mods/FullMod.cs
Normal file
31
Penumbra/Mods/FullMod.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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;
|
||||
}
|
||||
102
Penumbra/Mods/GroupInformation.cs
Normal file
102
Penumbra/Mods/GroupInformation.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
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." ),
|
||||
};
|
||||
}
|
||||
}
|
||||
44
Penumbra/Mods/Mod.SortOrder.cs
Normal file
44
Penumbra/Mods/Mod.SortOrder.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
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 );
|
||||
}
|
||||
}
|
||||
95
Penumbra/Mods/Mod.cs
Normal file
95
Penumbra/Mods/Mod.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
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 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;
|
||||
}
|
||||
531
Penumbra/Mods/ModCleanup.cs
Normal file
531
Penumbra/Mods/ModCleanup.cs
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Importer;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public class ModCleanup
|
||||
{
|
||||
private const string Duplicates = "Duplicates";
|
||||
private const string Required = "Required";
|
||||
|
||||
private readonly DirectoryInfo _baseDir;
|
||||
private readonly ModMeta _mod;
|
||||
private SHA256? _hasher;
|
||||
|
||||
private readonly Dictionary< long, List< FileInfo > > _filesBySize = new();
|
||||
|
||||
private SHA256 Sha()
|
||||
{
|
||||
_hasher ??= SHA256.Create();
|
||||
return _hasher;
|
||||
}
|
||||
|
||||
private ModCleanup( DirectoryInfo baseDir, ModMeta mod )
|
||||
{
|
||||
_baseDir = baseDir;
|
||||
_mod = mod;
|
||||
BuildDict();
|
||||
}
|
||||
|
||||
private void BuildDict()
|
||||
{
|
||||
foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||
{
|
||||
var fileLength = file.Length;
|
||||
if( _filesBySize.TryGetValue( fileLength, out var files ) )
|
||||
{
|
||||
files.Add( file );
|
||||
}
|
||||
else
|
||||
{
|
||||
_filesBySize[ fileLength ] = new List< FileInfo > { file };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static DirectoryInfo CreateNewModDir( Mod mod, string optionGroup, string option )
|
||||
{
|
||||
var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}";
|
||||
return TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config.ModDirectory ), newName );
|
||||
}
|
||||
|
||||
private static Mod CreateNewMod( DirectoryInfo newDir, string newSortOrder )
|
||||
{
|
||||
var idx = Penumbra.ModManager.AddMod( newDir );
|
||||
var newMod = Penumbra.ModManager.Mods[ idx ];
|
||||
newMod.Move( newSortOrder );
|
||||
newMod.ComputeChangedItems();
|
||||
ModFileSystem.InvokeChange();
|
||||
return newMod;
|
||||
}
|
||||
|
||||
private static ModMeta CreateNewMeta( DirectoryInfo newDir, Mod mod, string name, string optionGroup, string option )
|
||||
{
|
||||
var newMeta = new ModMeta
|
||||
{
|
||||
Author = mod.Meta.Author,
|
||||
Name = name,
|
||||
Description = $"Split from {mod.Meta.Name} Group {optionGroup} Option {option}.",
|
||||
};
|
||||
var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) );
|
||||
newMeta.SaveToFile( metaFile );
|
||||
return newMeta;
|
||||
}
|
||||
|
||||
private static void CreateModSplit( HashSet< string > unseenPaths, Mod mod, OptionGroup group, Option option )
|
||||
{
|
||||
try
|
||||
{
|
||||
var newDir = CreateNewModDir( mod, group.GroupName, option.OptionName );
|
||||
var newName = group.SelectionType == SelectType.Multi ? $"{group.GroupName} - {option.OptionName}" : option.OptionName;
|
||||
var newMeta = CreateNewMeta( newDir, mod, newName, group.GroupName, option.OptionName );
|
||||
foreach( var (fileName, paths) in option.OptionFiles )
|
||||
{
|
||||
var oldPath = Path.Combine( mod.BasePath.FullName, fileName.ToString() );
|
||||
unseenPaths.Remove( oldPath );
|
||||
if( File.Exists( oldPath ) )
|
||||
{
|
||||
foreach( var path in paths )
|
||||
{
|
||||
var newPath = Path.Combine( newDir.FullName, path.ToString() );
|
||||
Directory.CreateDirectory( Path.GetDirectoryName( newPath )! );
|
||||
File.Copy( oldPath, newPath, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newSortOrder = group.SelectionType == SelectType.Single
|
||||
? $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}"
|
||||
: $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}";
|
||||
CreateNewMod( newDir, newSortOrder );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not split Mod:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
public static void SplitMod( Mod mod )
|
||||
{
|
||||
if( mod.Meta.Groups.Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var unseenPaths = mod.Resources.ModFiles.Select( f => f.FullName ).ToHashSet();
|
||||
foreach( var group in mod.Meta.Groups.Values )
|
||||
{
|
||||
foreach( var option in group.Options )
|
||||
{
|
||||
CreateModSplit( unseenPaths, mod, group, option );
|
||||
}
|
||||
}
|
||||
|
||||
if( unseenPaths.Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var defaultGroup = new OptionGroup()
|
||||
{
|
||||
GroupName = "Default",
|
||||
SelectionType = SelectType.Multi,
|
||||
};
|
||||
var defaultOption = new Option()
|
||||
{
|
||||
OptionName = "Files",
|
||||
OptionFiles = unseenPaths.ToDictionary(
|
||||
p => Utf8RelPath.FromFile( new FileInfo( p ), mod.BasePath, out var rel ) ? rel : Utf8RelPath.Empty,
|
||||
p => new HashSet< Utf8GamePath >()
|
||||
{ Utf8GamePath.FromFile( new FileInfo( p ), mod.BasePath, out var game, true ) ? game : Utf8GamePath.Empty } ),
|
||||
};
|
||||
CreateModSplit( unseenPaths, mod, defaultGroup, defaultOption );
|
||||
}
|
||||
|
||||
private static Option FindOrCreateDuplicates( ModMeta meta )
|
||||
{
|
||||
static Option RequiredOption()
|
||||
=> new()
|
||||
{
|
||||
OptionName = Required,
|
||||
OptionDesc = "",
|
||||
OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
|
||||
};
|
||||
|
||||
if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) )
|
||||
{
|
||||
var idx = duplicates.Options.FindIndex( o => o.OptionName == Required );
|
||||
if( idx >= 0 )
|
||||
{
|
||||
return duplicates.Options[ idx ];
|
||||
}
|
||||
|
||||
duplicates.Options.Add( RequiredOption() );
|
||||
return duplicates.Options.Last();
|
||||
}
|
||||
|
||||
meta.Groups.Add( Duplicates, new OptionGroup
|
||||
{
|
||||
GroupName = Duplicates,
|
||||
SelectionType = SelectType.Single,
|
||||
Options = new List< Option > { RequiredOption() },
|
||||
} );
|
||||
|
||||
return meta.Groups[ Duplicates ].Options.First();
|
||||
}
|
||||
|
||||
public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod )
|
||||
{
|
||||
var dedup = new ModCleanup( baseDir, mod );
|
||||
foreach( var (key, value) in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) )
|
||||
{
|
||||
if( value.Count == 2 )
|
||||
{
|
||||
if( CompareFilesDirectly( value[ 0 ], value[ 1 ] ) )
|
||||
{
|
||||
dedup.ReplaceFile( value[ 0 ], value[ 1 ] );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var deleted = Enumerable.Repeat( false, value.Count ).ToArray();
|
||||
var hashes = value.Select( dedup.ComputeHash ).ToArray();
|
||||
|
||||
for( var i = 0; i < value.Count; ++i )
|
||||
{
|
||||
if( deleted[ i ] )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for( var j = i + 1; j < value.Count; ++j )
|
||||
{
|
||||
if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dedup.ReplaceFile( value[ i ], value[ j ] );
|
||||
deleted[ j ] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CleanUpDuplicates( mod );
|
||||
ClearEmptySubDirectories( dedup._baseDir );
|
||||
}
|
||||
|
||||
private void ReplaceFile( FileInfo f1, FileInfo f2 )
|
||||
{
|
||||
if( !Utf8RelPath.FromFile( f1, _baseDir, out var relName1 )
|
||||
|| !Utf8RelPath.FromFile( f2, _baseDir, out var relName2 ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var inOption1 = false;
|
||||
var inOption2 = false;
|
||||
foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) )
|
||||
{
|
||||
if( option.OptionFiles.ContainsKey( relName1 ) )
|
||||
{
|
||||
inOption1 = true;
|
||||
}
|
||||
|
||||
if( !option.OptionFiles.TryGetValue( relName2, out var values ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
inOption2 = true;
|
||||
|
||||
foreach( var value in values )
|
||||
{
|
||||
option.AddFile( relName1, value );
|
||||
}
|
||||
|
||||
option.OptionFiles.Remove( relName2 );
|
||||
}
|
||||
|
||||
if( !inOption1 || !inOption2 )
|
||||
{
|
||||
var duplicates = FindOrCreateDuplicates( _mod );
|
||||
if( !inOption1 )
|
||||
{
|
||||
duplicates.AddFile( relName1, relName2.ToGamePath() );
|
||||
}
|
||||
|
||||
if( !inOption2 )
|
||||
{
|
||||
duplicates.AddFile( relName1, relName1.ToGamePath() );
|
||||
}
|
||||
}
|
||||
|
||||
PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." );
|
||||
f2.Delete();
|
||||
}
|
||||
|
||||
public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 )
|
||||
=> File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) );
|
||||
|
||||
public static bool CompareHashes( byte[] f1, byte[] f2 )
|
||||
=> StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 );
|
||||
|
||||
public byte[] ComputeHash( FileInfo f )
|
||||
{
|
||||
var stream = File.OpenRead( f.FullName );
|
||||
var ret = Sha().ComputeHash( stream );
|
||||
stream.Dispose();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Does not delete the base directory itself even if it is completely empty at the end.
|
||||
public static void ClearEmptySubDirectories( DirectoryInfo baseDir )
|
||||
{
|
||||
foreach( var subDir in baseDir.GetDirectories() )
|
||||
{
|
||||
ClearEmptySubDirectories( subDir );
|
||||
if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 )
|
||||
{
|
||||
subDir.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool FileIsInAnyGroup( ModMeta meta, Utf8RelPath relPath, bool exceptDuplicates = false )
|
||||
{
|
||||
var groupEnumerator = exceptDuplicates
|
||||
? meta.Groups.Values.Where( g => g.GroupName != Duplicates )
|
||||
: meta.Groups.Values;
|
||||
return groupEnumerator.SelectMany( group => group.Options )
|
||||
.Any( option => option.OptionFiles.ContainsKey( relPath ) );
|
||||
}
|
||||
|
||||
private static void CleanUpDuplicates( ModMeta meta )
|
||||
{
|
||||
if( !meta.Groups.TryGetValue( Duplicates, out var info ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var requiredIdx = info.Options.FindIndex( o => o.OptionName == Required );
|
||||
if( requiredIdx >= 0 )
|
||||
{
|
||||
var required = info.Options[ requiredIdx ];
|
||||
foreach( var (key, value) in required.OptionFiles.ToArray() )
|
||||
{
|
||||
if( value.Count > 1 || FileIsInAnyGroup( meta, key, true ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( value.Count == 0 || value.First().CompareTo( key.ToGamePath() ) == 0 )
|
||||
{
|
||||
required.OptionFiles.Remove( key );
|
||||
}
|
||||
}
|
||||
|
||||
if( required.OptionFiles.Count == 0 )
|
||||
{
|
||||
info.Options.RemoveAt( requiredIdx );
|
||||
}
|
||||
}
|
||||
|
||||
if( info.Options.Count == 0 )
|
||||
{
|
||||
meta.Groups.Remove( Duplicates );
|
||||
}
|
||||
}
|
||||
|
||||
public enum GroupType
|
||||
{
|
||||
Both = 0,
|
||||
Single = 1,
|
||||
Multi = 2,
|
||||
};
|
||||
|
||||
private static void RemoveFromGroups( ModMeta meta, Utf8RelPath relPath, Utf8GamePath gamePath, GroupType type = GroupType.Both,
|
||||
bool skipDuplicates = true )
|
||||
{
|
||||
if( meta.Groups.Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var enumerator = type switch
|
||||
{
|
||||
GroupType.Both => meta.Groups.Values,
|
||||
GroupType.Single => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ),
|
||||
GroupType.Multi => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ),
|
||||
_ => throw new InvalidEnumArgumentException( "Invalid Enum in RemoveFromGroups" ),
|
||||
};
|
||||
foreach( var group in enumerator )
|
||||
{
|
||||
var optionEnum = skipDuplicates
|
||||
? group.Options.Where( o => group.GroupName != Duplicates || o.OptionName != Required )
|
||||
: group.Options;
|
||||
foreach( var option in optionEnum )
|
||||
{
|
||||
if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 )
|
||||
{
|
||||
option.OptionFiles.Remove( relPath );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool MoveFile( ModMeta meta, string basePath, Utf8RelPath oldRelPath, Utf8RelPath newRelPath )
|
||||
{
|
||||
if( oldRelPath.Equals( newRelPath ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var newFullPath = Path.Combine( basePath, newRelPath.ToString() );
|
||||
new FileInfo( newFullPath ).Directory!.Create();
|
||||
File.Move( Path.Combine( basePath, oldRelPath.ToString() ), newFullPath );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" );
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) )
|
||||
{
|
||||
if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) )
|
||||
{
|
||||
option.OptionFiles.Add( newRelPath, gamePaths );
|
||||
option.OptionFiles.Remove( oldRelPath );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static void RemoveUselessGroups( ModMeta meta )
|
||||
{
|
||||
meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) )
|
||||
.ToDictionary( kvp => kvp.Key, kvp => kvp.Value );
|
||||
}
|
||||
|
||||
// Goes through all Single-Select options and checks if file links are in each of them.
|
||||
// If they are, it moves those files to the root folder and removes them from the groups (and puts them to duplicates, if necessary).
|
||||
public static void Normalize( DirectoryInfo baseDir, ModMeta meta )
|
||||
{
|
||||
foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) )
|
||||
{
|
||||
var firstOption = true;
|
||||
HashSet< (Utf8RelPath, Utf8GamePath) > groupList = new();
|
||||
foreach( var option in group.Options )
|
||||
{
|
||||
HashSet< (Utf8RelPath, Utf8GamePath) > optionList = new();
|
||||
foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) )
|
||||
{
|
||||
optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) );
|
||||
}
|
||||
|
||||
if( firstOption )
|
||||
{
|
||||
groupList = optionList;
|
||||
}
|
||||
else
|
||||
{
|
||||
groupList.IntersectWith( optionList );
|
||||
}
|
||||
|
||||
firstOption = false;
|
||||
}
|
||||
|
||||
var newPath = new Dictionary< Utf8RelPath, Utf8GamePath >();
|
||||
foreach( var (path, gamePath) in groupList )
|
||||
{
|
||||
var relPath = new Utf8RelPath( gamePath );
|
||||
if( newPath.TryGetValue( path, out var usedGamePath ) )
|
||||
{
|
||||
var required = FindOrCreateDuplicates( meta );
|
||||
var usedRelPath = new Utf8RelPath( usedGamePath );
|
||||
required.AddFile( usedRelPath, gamePath );
|
||||
required.AddFile( usedRelPath, usedGamePath );
|
||||
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
|
||||
}
|
||||
else if( MoveFile( meta, baseDir.FullName, path, relPath ) )
|
||||
{
|
||||
newPath[ path ] = gamePath;
|
||||
if( FileIsInAnyGroup( meta, relPath ) )
|
||||
{
|
||||
FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath );
|
||||
}
|
||||
|
||||
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RemoveUselessGroups( meta );
|
||||
ClearEmptySubDirectories( baseDir );
|
||||
}
|
||||
|
||||
public static void AutoGenerateGroups( DirectoryInfo baseDir, ModMeta meta )
|
||||
{
|
||||
meta.Groups.Clear();
|
||||
ClearEmptySubDirectories( baseDir );
|
||||
foreach( var groupDir in baseDir.EnumerateDirectories() )
|
||||
{
|
||||
var group = new OptionGroup
|
||||
{
|
||||
GroupName = groupDir.Name,
|
||||
SelectionType = SelectType.Single,
|
||||
Options = new List< Option >(),
|
||||
};
|
||||
|
||||
foreach( var optionDir in groupDir.EnumerateDirectories() )
|
||||
{
|
||||
var option = new Option
|
||||
{
|
||||
OptionDesc = string.Empty,
|
||||
OptionName = optionDir.Name,
|
||||
OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
|
||||
};
|
||||
foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||
{
|
||||
if( Utf8RelPath.FromFile( file, baseDir, out var rel )
|
||||
&& Utf8GamePath.FromFile( file, optionDir, out var game ) )
|
||||
{
|
||||
option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game };
|
||||
}
|
||||
}
|
||||
|
||||
if( option.OptionFiles.Count > 0 )
|
||||
{
|
||||
group.Options.Add( option );
|
||||
}
|
||||
}
|
||||
|
||||
if( group.Options.Count > 0 )
|
||||
{
|
||||
meta.Groups.Add( groupDir.Name, group );
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
var idx = Penumbra.ModManager.Mods.IndexOf( m => m.Meta == meta );
|
||||
foreach( var collection in Penumbra.CollectionManager )
|
||||
{
|
||||
collection.Settings[ idx ]?.FixInvalidSettings( meta );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using Penumbra.Mod;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
|
|
@ -37,7 +36,7 @@ public static partial class ModFileSystem
|
|||
|
||||
// Rename the SortOrderName of a single mod. Slashes are replaced by Backslashes.
|
||||
// Saves and returns true if anything changed.
|
||||
public static bool Rename( this Mod.Mod mod, string newName )
|
||||
public static bool Rename( this global::Penumbra.Mods.Mod mod, string newName )
|
||||
{
|
||||
if( RenameNoSave( mod, newName ) )
|
||||
{
|
||||
|
|
@ -63,7 +62,7 @@ public static partial class ModFileSystem
|
|||
|
||||
// Move a single mod to the target folder.
|
||||
// Returns true and saves if anything changed.
|
||||
public static bool Move( this Mod.Mod mod, ModFolder target )
|
||||
public static bool Move( this global::Penumbra.Mods.Mod mod, ModFolder target )
|
||||
{
|
||||
if( MoveNoSave( mod, target ) )
|
||||
{
|
||||
|
|
@ -76,7 +75,7 @@ public static partial class ModFileSystem
|
|||
|
||||
// Move a mod to the filesystem location specified by sortOrder and rename its SortOrderName.
|
||||
// Creates all necessary Subfolders.
|
||||
public static void Move( this Mod.Mod mod, string sortOrder )
|
||||
public static void Move( this global::Penumbra.Mods.Mod mod, string sortOrder )
|
||||
{
|
||||
var split = sortOrder.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries );
|
||||
var folder = Root;
|
||||
|
|
@ -137,10 +136,10 @@ public static partial class ModFileSystem
|
|||
}
|
||||
|
||||
// Sets and saves the sort order of a single mod, removing the entry if it is unnecessary.
|
||||
private static void SaveMod( Mod.Mod mod )
|
||||
private static void SaveMod( global::Penumbra.Mods.Mod mod )
|
||||
{
|
||||
if( ReferenceEquals( mod.Order.ParentFolder, Root )
|
||||
&& string.Equals( mod.Order.SortOrderName, mod.Meta.Name.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) )
|
||||
&& string.Equals( mod.Order.SortOrderName, mod.Meta.Name.Text.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) )
|
||||
{
|
||||
Penumbra.Config.ModSortOrder.Remove( mod.BasePath.Name );
|
||||
}
|
||||
|
|
@ -184,7 +183,7 @@ public static partial class ModFileSystem
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool RenameNoSave( Mod.Mod mod, string newName )
|
||||
private static bool RenameNoSave( global::Penumbra.Mods.Mod mod, string newName )
|
||||
{
|
||||
newName = newName.Replace( '/', '\\' );
|
||||
if( mod.Order.SortOrderName == newName )
|
||||
|
|
@ -193,12 +192,12 @@ public static partial class ModFileSystem
|
|||
}
|
||||
|
||||
mod.Order.ParentFolder.RemoveModIgnoreEmpty( mod );
|
||||
mod.Order = new Mod.Mod.SortOrder( mod.Order.ParentFolder, newName );
|
||||
mod.Order = new global::Penumbra.Mods.Mod.SortOrder( mod.Order.ParentFolder, newName );
|
||||
mod.Order.ParentFolder.AddMod( mod );
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool MoveNoSave( Mod.Mod mod, ModFolder target )
|
||||
private static bool MoveNoSave( global::Penumbra.Mods.Mod mod, ModFolder target )
|
||||
{
|
||||
var oldParent = mod.Order.ParentFolder;
|
||||
if( ReferenceEquals( target, oldParent ) )
|
||||
|
|
@ -207,7 +206,7 @@ public static partial class ModFileSystem
|
|||
}
|
||||
|
||||
oldParent.RemoveMod( mod );
|
||||
mod.Order = new Mod.Mod.SortOrder( target, mod.Order.SortOrderName );
|
||||
mod.Order = new global::Penumbra.Mods.Mod.SortOrder( target, mod.Order.SortOrderName );
|
||||
target.AddMod( mod );
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,247 +1,245 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Penumbra.Mod;
|
||||
|
||||
namespace Penumbra.Mods
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class ModFolder
|
||||
{
|
||||
public partial class ModFolder
|
||||
public ModFolder? Parent;
|
||||
|
||||
public string FullName
|
||||
{
|
||||
public ModFolder? Parent;
|
||||
|
||||
public string FullName
|
||||
get
|
||||
{
|
||||
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.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.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.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 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 mod )
|
||||
{
|
||||
RemoveModIgnoreEmpty( mod );
|
||||
CheckEmpty();
|
||||
var parentPath = Parent?.FullName ?? string.Empty;
|
||||
return parentPath.Any() ? $"{parentPath}/{Name}" : Name;
|
||||
}
|
||||
}
|
||||
|
||||
// Internals
|
||||
public partial class ModFolder
|
||||
private string _name = string.Empty;
|
||||
|
||||
public string Name
|
||||
{
|
||||
// Create a Root folder without parent.
|
||||
internal static ModFolder CreateRoot()
|
||||
=> new( null!, string.Empty );
|
||||
get => _name;
|
||||
set => _name = value.Replace( '/', '\\' );
|
||||
}
|
||||
|
||||
internal class ModFolderComparer : IComparer< ModFolder >
|
||||
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 )
|
||||
{
|
||||
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 );
|
||||
return SubFolders.SelectMany( f => f.AllMods( foldersFirst ) ).Concat( Mods );
|
||||
}
|
||||
|
||||
internal class ModDataComparer : IComparer< Mod.Mod >
|
||||
return GetSortedEnumerator().SelectMany( f =>
|
||||
{
|
||||
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.Mod? x, Mod.Mod? y )
|
||||
if( f is ModFolder folder )
|
||||
{
|
||||
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 );
|
||||
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;
|
||||
}
|
||||
|
||||
internal static readonly ModFolderComparer FolderComparer = new();
|
||||
internal static readonly ModDataComparer ModComparer = new();
|
||||
idx = ~idx;
|
||||
SubFolders.Insert( idx, folder );
|
||||
folder.Parent = this;
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Get an enumerator for actually sorted objects instead of folder-first objects.
|
||||
private IEnumerable< object > GetSortedEnumerator()
|
||||
// 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 )
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
yield return folder;
|
||||
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 )
|
||||
{
|
||||
yield return Mods[ 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;
|
||||
}
|
||||
|
||||
private void CheckEmpty()
|
||||
for( ; modIdx < Mods.Count; ++modIdx )
|
||||
{
|
||||
if( Mods.Count == 0 && SubFolders.Count == 0 )
|
||||
{
|
||||
Parent?.RemoveSubFolder( this );
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 );
|
||||
}
|
||||
|
||||
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 mod )
|
||||
// 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 )
|
||||
{
|
||||
var idx = Mods.BinarySearch( mod, ModComparer );
|
||||
if( idx >= 0 )
|
||||
{
|
||||
Mods.RemoveAt( idx );
|
||||
}
|
||||
Mods.RemoveAt( idx );
|
||||
}
|
||||
}
|
||||
}
|
||||
100
Penumbra/Mods/ModFunctions.cs
Normal file
100
Penumbra/Mods/ModFunctions.cs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,16 +2,14 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mod;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class ModManagerNew
|
||||
{
|
||||
private readonly List< Mod.Mod > _mods = new();
|
||||
private readonly List< Mod > _mods = new();
|
||||
|
||||
public IReadOnlyList< Mod.Mod > Mods
|
||||
public IReadOnlyList< Mod > Mods
|
||||
=> _mods;
|
||||
|
||||
public void DiscoverMods()
|
||||
|
|
@ -37,6 +35,7 @@ public partial class ModManagerNew
|
|||
//Collections.RecreateCaches();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ModManagerNew
|
||||
{
|
||||
public DirectoryInfo BasePath { get; private set; } = null!;
|
||||
|
|
|
|||
285
Penumbra/Mods/ModManager.cs
Normal file
285
Penumbra/Mods/ModManager.cs
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Mods;
|
||||
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, int modIndex, Mod mod );
|
||||
|
||||
public event ModChangeDelegate? ModChange;
|
||||
public event Action? ModsRediscovered;
|
||||
|
||||
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}" );
|
||||
}
|
||||
}
|
||||
|
||||
BasePath = newDir;
|
||||
Valid = true;
|
||||
if( Config.ModDirectory != BasePath.FullName )
|
||||
{
|
||||
Config.ModDirectory = BasePath.FullName;
|
||||
Config.Save();
|
||||
}
|
||||
}
|
||||
|
||||
ModsRediscovered?.Invoke();
|
||||
}
|
||||
|
||||
public Manager()
|
||||
{
|
||||
SetBaseDirectory( Config.ModDirectory, true );
|
||||
}
|
||||
|
||||
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 ) )
|
||||
{
|
||||
Config.ModSortOrder.Remove( mod.BasePath.Name );
|
||||
return true;
|
||||
}
|
||||
|
||||
if( path != fixedPath )
|
||||
{
|
||||
Config.ModSortOrder[ mod.BasePath.Name ] = fixedPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SetModStructure( bool removeOldPaths = false )
|
||||
{
|
||||
var changes = false;
|
||||
|
||||
foreach( var (folder, path) in Config.ModSortOrder.ToArray() )
|
||||
{
|
||||
if( path.Length > 0 && _mods.FindFirst( m => m.BasePath.Name == folder, out var mod ) )
|
||||
{
|
||||
changes |= SetSortOrderPath( mod, path );
|
||||
}
|
||||
else if( removeOldPaths )
|
||||
{
|
||||
changes = true;
|
||||
Config.ModSortOrder.Remove( folder );
|
||||
}
|
||||
}
|
||||
|
||||
if( changes )
|
||||
{
|
||||
Config.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void DiscoverMods()
|
||||
{
|
||||
_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();
|
||||
}
|
||||
|
||||
ModsRediscovered?.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, idx, mod );
|
||||
}
|
||||
}
|
||||
|
||||
public int AddMod( DirectoryInfo modFolder )
|
||||
{
|
||||
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 _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 ) || 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( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) )
|
||||
{
|
||||
mod.Move( sortOrder );
|
||||
var path = mod.Order.FullPath;
|
||||
if( path != sortOrder )
|
||||
{
|
||||
Config.ModSortOrder[ 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, idx, 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 );
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
|
@ -12,7 +11,7 @@ namespace Penumbra.Mods;
|
|||
// 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.Mod.Manager manager, string newName, Mod.Mod mod )
|
||||
public static bool RenameMod( this Mod.Manager manager, string newName, Mod mod )
|
||||
{
|
||||
if( newName.Length == 0 || string.Equals( newName, mod.Meta.Name, StringComparison.InvariantCulture ) )
|
||||
{
|
||||
|
|
@ -25,14 +24,14 @@ public static class ModManagerEditExtensions
|
|||
return true;
|
||||
}
|
||||
|
||||
public static bool ChangeSortOrder( this Mod.Mod.Manager manager, Mod.Mod mod, string newSortOrder )
|
||||
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.Mod.SortOrder( manager.StructuredMods, mod.Meta.Name );
|
||||
var inRoot = new Mod.SortOrder( manager.StructuredMods, mod.Meta.Name );
|
||||
if( newSortOrder == string.Empty || newSortOrder == inRoot.SortOrderName )
|
||||
{
|
||||
mod.Order = inRoot;
|
||||
|
|
@ -49,7 +48,7 @@ public static class ModManagerEditExtensions
|
|||
return true;
|
||||
}
|
||||
|
||||
public static bool RenameModFolder( this Mod.Mod.Manager manager, Mod.Mod mod, DirectoryInfo newDir, bool move = true )
|
||||
public static bool RenameModFolder( this Mod.Manager manager, Mod mod, DirectoryInfo newDir, bool move = true )
|
||||
{
|
||||
if( move )
|
||||
{
|
||||
|
|
@ -73,7 +72,7 @@ public static class ModManagerEditExtensions
|
|||
|
||||
var oldBasePath = mod.BasePath;
|
||||
mod.BasePath = newDir;
|
||||
mod.MetaFile = Mod.Mod.MetaFileInfo( newDir );
|
||||
mod.MetaFile = Mod.MetaFileInfo( newDir );
|
||||
manager.UpdateMod( mod );
|
||||
|
||||
if( manager.Config.ModSortOrder.ContainsKey( oldBasePath.Name ) )
|
||||
|
|
@ -95,7 +94,7 @@ public static class ModManagerEditExtensions
|
|||
return true;
|
||||
}
|
||||
|
||||
public static bool ChangeModGroup( this Mod.Mod.Manager manager, string oldGroupName, string newGroupName, Mod.Mod mod,
|
||||
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 ) )
|
||||
|
|
@ -157,7 +156,7 @@ public static class ModManagerEditExtensions
|
|||
return true;
|
||||
}
|
||||
|
||||
public static bool RemoveModOption( this Mod.Mod.Manager manager, int optionIdx, OptionGroup group, Mod.Mod mod )
|
||||
public static bool RemoveModOption( this Mod.Manager manager, int optionIdx, OptionGroup group, Mod mod )
|
||||
{
|
||||
if( optionIdx < 0 || optionIdx >= group.Options.Count )
|
||||
{
|
||||
|
|
|
|||
108
Penumbra/Mods/ModMeta.cs
Normal file
108
Penumbra/Mods/ModMeta.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Newtonsoft.Json;
|
||||
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 uint FileVersion { get; set; }
|
||||
|
||||
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;
|
||||
|
||||
[JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )]
|
||||
public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new();
|
||||
|
||||
public Dictionary< string, OptionGroup > Groups { get; set; } = new();
|
||||
|
||||
[JsonIgnore]
|
||||
private int FileHash { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasGroupsWithConfig { get; private set; }
|
||||
|
||||
public bool RefreshFromFile( FileInfo filePath )
|
||||
{
|
||||
var newMeta = LoadFromFile( filePath );
|
||||
if( newMeta == null )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if( newMeta.FileHash == FileHash )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FileVersion = newMeta.FileVersion;
|
||||
Name = newMeta.Name;
|
||||
Author = newMeta.Author;
|
||||
Description = newMeta.Description;
|
||||
Version = newMeta.Version;
|
||||
Website = newMeta.Website;
|
||||
FileSwaps = newMeta.FileSwaps;
|
||||
Groups = newMeta.Groups;
|
||||
FileHash = newMeta.FileHash;
|
||||
HasGroupsWithConfig = newMeta.HasGroupsWithConfig;
|
||||
return true;
|
||||
}
|
||||
|
||||
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.FileHash = text.GetHashCode();
|
||||
meta.RefreshHasGroupsWithConfig();
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not load mod meta:\n{e}" );
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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 void SaveToFile( FileInfo filePath )
|
||||
{
|
||||
try
|
||||
{
|
||||
var text = JsonConvert.SerializeObject( this, Formatting.Indented );
|
||||
var newHash = text.GetHashCode();
|
||||
if( newHash != FileHash )
|
||||
{
|
||||
File.WriteAllText( filePath.FullName, text );
|
||||
FileHash = newHash;
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
89
Penumbra/Mods/ModResources.cs
Normal file
89
Penumbra/Mods/ModResources.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
73
Penumbra/Mods/ModSettings.cs
Normal file
73
Penumbra/Mods/ModSettings.cs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
// Contains the settings for a given mod.
|
||||
public class ModSettings
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public Dictionary< string, int > Settings { get; set; } = new();
|
||||
|
||||
// For backwards compatibility
|
||||
private Dictionary< string, int > Conf
|
||||
{
|
||||
set => Settings = value;
|
||||
}
|
||||
|
||||
public ModSettings DeepCopy()
|
||||
{
|
||||
var settings = new ModSettings
|
||||
{
|
||||
Enabled = Enabled,
|
||||
Priority = Priority,
|
||||
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ),
|
||||
};
|
||||
return settings;
|
||||
}
|
||||
|
||||
public static ModSettings DefaultSettings( ModMeta meta )
|
||||
{
|
||||
return new ModSettings
|
||||
{
|
||||
Enabled = false,
|
||||
Priority = 0,
|
||||
Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ),
|
||||
};
|
||||
}
|
||||
|
||||
public bool FixSpecificSetting( string name, ModMeta meta )
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
public bool FixInvalidSettings( ModMeta meta )
|
||||
{
|
||||
if( meta.Groups.Count == 0 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Settings.Keys.ToArray().Union( meta.Groups.Keys )
|
||||
.Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, meta ) );
|
||||
}
|
||||
}
|
||||
46
Penumbra/Mods/NamedModSettings.cs
Normal file
46
Penumbra/Mods/NamedModSettings.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue