mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-31 21:03:48 +01:00
Complete refactoring of most code, indiscriminate application of .editorconfig and general cleanup.
This commit is contained in:
parent
5332119a63
commit
a19ec226c5
84 changed files with 3168 additions and 1709 deletions
32
Penumbra/Mod/Mod.cs
Normal file
32
Penumbra/Mod/Mod.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
{
|
||||
public class Mod
|
||||
{
|
||||
public ModSettings Settings { get; }
|
||||
public ModData Data { get; }
|
||||
public ModCache Cache { get; }
|
||||
|
||||
public Mod( ModSettings settings, ModData data )
|
||||
{
|
||||
Settings = settings;
|
||||
Data = data;
|
||||
Cache = new ModCache();
|
||||
}
|
||||
|
||||
public bool FixSettings()
|
||||
=> Settings.FixInvalidSettings( Data.Meta );
|
||||
|
||||
public HashSet< GamePath > GetFiles( FileInfo file )
|
||||
{
|
||||
var relPath = new RelPath( file, Data.BasePath );
|
||||
return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta );
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> Data.Meta.Name;
|
||||
}
|
||||
}
|
||||
57
Penumbra/Mod/ModCache.cs
Normal file
57
Penumbra/Mod/ModCache.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
{
|
||||
public class ModCache
|
||||
{
|
||||
public Dictionary< Mod, (List< GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new();
|
||||
|
||||
public void AddConflict( Mod precedingMod, GamePath gamePath )
|
||||
{
|
||||
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) )
|
||||
{
|
||||
conflicts.Files.Add( gamePath );
|
||||
}
|
||||
else
|
||||
{
|
||||
Conflicts[ precedingMod ] = ( new List< GamePath > { gamePath }, new List< MetaManipulation >() );
|
||||
}
|
||||
}
|
||||
|
||||
public void AddConflict( Mod precedingMod, MetaManipulation manipulation )
|
||||
{
|
||||
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Manipulations.Contains( manipulation ) )
|
||||
{
|
||||
conflicts.Manipulations.Add( manipulation );
|
||||
}
|
||||
else
|
||||
{
|
||||
Conflicts[ precedingMod ] = ( new List< GamePath >(), new List< MetaManipulation > { manipulation } );
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearConflicts()
|
||||
=> Conflicts.Clear();
|
||||
|
||||
public void ClearFileConflicts()
|
||||
{
|
||||
Conflicts = Conflicts.Where( kvp => kvp.Value.Manipulations.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp =>
|
||||
{
|
||||
kvp.Value.Files.Clear();
|
||||
return kvp.Value;
|
||||
} );
|
||||
}
|
||||
|
||||
public void ClearMetaConflicts()
|
||||
{
|
||||
Conflicts = Conflicts.Where( kvp => kvp.Value.Files.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp =>
|
||||
{
|
||||
kvp.Value.Manipulations.Clear();
|
||||
return kvp.Value;
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
||||
380
Penumbra/Mod/ModCleanup.cs
Normal file
380
Penumbra/Mod/ModCleanup.cs
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using Dalamud.Plugin;
|
||||
using Penumbra.Structs;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
{
|
||||
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 Option FindOrCreateDuplicates( ModMeta meta )
|
||||
{
|
||||
static Option RequiredOption()
|
||||
=> new()
|
||||
{
|
||||
OptionName = Required,
|
||||
OptionDesc = "",
|
||||
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
|
||||
};
|
||||
|
||||
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 pair in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) )
|
||||
{
|
||||
if( pair.Value.Count == 2 )
|
||||
{
|
||||
if( CompareFilesDirectly( pair.Value[ 0 ], pair.Value[ 1 ] ) )
|
||||
{
|
||||
dedup.ReplaceFile( pair.Value[ 0 ], pair.Value[ 1 ] );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var deleted = Enumerable.Repeat( false, pair.Value.Count ).ToArray();
|
||||
var hashes = pair.Value.Select( dedup.ComputeHash ).ToArray();
|
||||
|
||||
for( var i = 0; i < pair.Value.Count; ++i )
|
||||
{
|
||||
if( deleted[ i ] )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for( var j = i + 1; j < pair.Value.Count; ++j )
|
||||
{
|
||||
if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dedup.ReplaceFile( pair.Value[ i ], pair.Value[ j ] );
|
||||
deleted[ j ] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CleanUpDuplicates( mod );
|
||||
ClearEmptySubDirectories( dedup._baseDir );
|
||||
}
|
||||
|
||||
private void ReplaceFile( FileInfo f1, FileInfo f2 )
|
||||
{
|
||||
RelPath relName1 = new( f1, _baseDir );
|
||||
RelPath relName2 = new( f2, _baseDir );
|
||||
|
||||
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, new GamePath( relName2, 0 ) );
|
||||
}
|
||||
|
||||
if( !inOption2 )
|
||||
{
|
||||
duplicates.AddFile( relName1, new GamePath( relName1, 0 ) );
|
||||
}
|
||||
}
|
||||
|
||||
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, RelPath 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 kvp in required.OptionFiles.ToArray() )
|
||||
{
|
||||
if( kvp.Value.Count > 1 || FileIsInAnyGroup( meta, kvp.Key, true ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( kvp.Value.Count == 0 || kvp.Value.First().CompareTo( new GamePath( kvp.Key, 0 ) ) == 0 )
|
||||
{
|
||||
required.OptionFiles.Remove( kvp.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, RelPath relPath, GamePath 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, RelPath oldRelPath, RelPath newRelPath )
|
||||
{
|
||||
if( oldRelPath == newRelPath )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var newFullPath = Path.Combine( basePath, newRelPath );
|
||||
new FileInfo( newFullPath ).Directory!.Create();
|
||||
File.Move( Path.Combine( basePath, oldRelPath ), 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< (RelPath, GamePath) > groupList = new();
|
||||
foreach( var option in group.Options )
|
||||
{
|
||||
HashSet< (RelPath, GamePath) > 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< RelPath, GamePath >();
|
||||
foreach( var (path, gamePath) in groupList )
|
||||
{
|
||||
var relPath = new RelPath( gamePath );
|
||||
if( newPath.TryGetValue( path, out var usedGamePath ) )
|
||||
{
|
||||
var required = FindOrCreateDuplicates( meta );
|
||||
var usedRelPath = new RelPath( 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
59
Penumbra/Mod/ModData.cs
Normal file
59
Penumbra/Mod/ModData.cs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
using System.IO;
|
||||
using Dalamud.Plugin;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
{
|
||||
public class ModData
|
||||
{
|
||||
public DirectoryInfo BasePath;
|
||||
public ModMeta Meta;
|
||||
public ModResources Resources;
|
||||
|
||||
public FileInfo MetaFile { get; set; }
|
||||
|
||||
private ModData( DirectoryInfo basePath, ModMeta meta, ModResources resources )
|
||||
{
|
||||
BasePath = basePath;
|
||||
Meta = meta;
|
||||
Resources = resources;
|
||||
MetaFile = MetaFileInfo( basePath );
|
||||
}
|
||||
|
||||
public static FileInfo MetaFileInfo( DirectoryInfo basePath )
|
||||
=> new( Path.Combine( basePath.FullName, "meta.json" ) );
|
||||
|
||||
public static ModData? LoadMod( 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 ModData( basePath, meta, data );
|
||||
}
|
||||
|
||||
public void SaveMeta()
|
||||
=> Meta.SaveToFile( MetaFile );
|
||||
}
|
||||
}
|
||||
82
Penumbra/Mod/ModFunctions.cs
Normal file
82
Penumbra/Mod/ModFunctions.cs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Penumbra.Structs;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.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< GamePath > GetFilesForConfig( RelPath relPath, ModSettings settings, ModMeta meta )
|
||||
{
|
||||
var doNotAdd = false;
|
||||
var files = new HashSet< GamePath >();
|
||||
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( new GamePath( relPath ) );
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public static ModSettings ConvertNamedSettings( NamedModSettings namedSettings, ModMeta meta )
|
||||
{
|
||||
ModSettings ret = new()
|
||||
{
|
||||
Priority = namedSettings.Priority,
|
||||
Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ),
|
||||
};
|
||||
|
||||
foreach( var kvp in namedSettings.Settings )
|
||||
{
|
||||
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( info.SelectionType == SelectType.Single )
|
||||
{
|
||||
if( namedSettings.Settings[ kvp.Key ].Count == 0 )
|
||||
{
|
||||
ret.Settings[ kvp.Key ] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ kvp.Key ].Last() );
|
||||
ret.Settings[ kvp.Key ] = idx < 0 ? 0 : idx;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach( var idx in namedSettings.Settings[ kvp.Key ]
|
||||
.Select( option => info.Options.FindIndex( o => o.OptionName == option ) )
|
||||
.Where( idx => idx >= 0 ) )
|
||||
{
|
||||
ret.Settings[ kvp.Key ] |= 1 << idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
103
Penumbra/Mod/ModMeta.cs
Normal file
103
Penumbra/Mod/ModMeta.cs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Plugin;
|
||||
using Newtonsoft.Json;
|
||||
using Penumbra.Structs;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
{
|
||||
// Contains descriptive data about the mod as well as possible settings and fileswaps.
|
||||
public class ModMeta
|
||||
{
|
||||
public uint FileVersion { get; set; }
|
||||
public string Name { get; set; } = "Mod";
|
||||
public string Author { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public string Version { get; set; } = "";
|
||||
public string Website { get; set; } = "";
|
||||
public List< string > ChangedItems { get; set; } = new();
|
||||
|
||||
[JsonProperty( ItemConverterType = typeof( GamePathConverter ) )]
|
||||
public Dictionary< GamePath, GamePath > 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;
|
||||
ChangedItems = newMeta.ChangedItems;
|
||||
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.HasGroupsWithConfig = meta.Groups.Values.Any( g => g.SelectionType == SelectType.Multi || g.Options.Count > 1 );
|
||||
}
|
||||
|
||||
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 );
|
||||
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}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
Penumbra/Mod/ModResources.cs
Normal file
86
Penumbra/Mod/ModResources.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Penumbra.Meta;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
{
|
||||
[Flags]
|
||||
public enum ResourceChange
|
||||
{
|
||||
Files = 1,
|
||||
Meta = 2,
|
||||
}
|
||||
|
||||
// Contains static mod data that should only change on filesystem changes.
|
||||
public class ModResources
|
||||
{
|
||||
public List< FileInfo > ModFiles { get; private set; } = new();
|
||||
public List< FileInfo > 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 )
|
||||
{
|
||||
var newManipulations = MetaCollection.LoadFromFile( MetaCollection.FileName( basePath ) );
|
||||
if( newManipulations == null )
|
||||
{
|
||||
ForceManipulationsUpdate( meta, basePath );
|
||||
}
|
||||
else
|
||||
{
|
||||
MetaManipulations = newManipulations;
|
||||
if( !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< FileInfo > tmpFiles = new( ModFiles.Count );
|
||||
List< FileInfo > 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 ) )
|
||||
.OrderBy( f => f.FullName ) )
|
||||
{
|
||||
if( file.Extension != ".meta" )
|
||||
{
|
||||
tmpFiles.Add( file );
|
||||
}
|
||||
else
|
||||
{
|
||||
tmpMetas.Add( file );
|
||||
}
|
||||
}
|
||||
|
||||
ResourceChange changes = 0;
|
||||
if( !tmpFiles.SequenceEqual( ModFiles ) )
|
||||
{
|
||||
ModFiles = tmpFiles;
|
||||
changes |= ResourceChange.Files;
|
||||
}
|
||||
|
||||
if( !tmpMetas.SequenceEqual( MetaFiles ) )
|
||||
{
|
||||
MetaFiles = tmpMetas;
|
||||
changes |= ResourceChange.Meta;
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Penumbra/Mod/ModSettings.cs
Normal file
74
Penumbra/Mod/ModSettings.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Penumbra.Structs;
|
||||
|
||||
namespace Penumbra.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()
|
||||
{
|
||||
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 ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Penumbra/Mod/NamedModSettings.cs
Normal file
45
Penumbra/Mod/NamedModSettings.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Penumbra.Structs;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
{
|
||||
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