diff --git a/OtterGui b/OtterGui index 0c32ec43..5968fc8d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0c32ec432d38093a402e0dae6dc5c62a883b163b +Subproject commit 5968fc8dde7867ec9b7216deeed93d7b59a41ab8 diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 79d7d33c..752a5349 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -172,7 +172,7 @@ public partial class ModCollection // A changed mod forces changes for all collections, active and inactive. - private void OnModChanged( Mod.ChangeType type, int idx, Mod mod ) + private void OnModChanged( Mod.ChangeType type, Mod mod ) { switch( type ) { @@ -188,15 +188,15 @@ public partial class ModCollection var settings = new List< ModSettings? >( _collections.Count ); foreach( var collection in this ) { - settings.Add( collection[ idx ].Settings ); - collection.RemoveMod( mod, idx ); + settings.Add( collection[ mod.Index ].Settings ); + collection.RemoveMod( mod, mod.Index ); } OnModRemovedActive( mod.Resources.MetaManipulations.Count > 0, settings ); break; case Mod.ChangeType.Changed: foreach( var collection in this.Where( - collection => collection.Settings[ idx ]?.FixInvalidSettings( mod.Meta ) ?? false ) ) + collection => collection.Settings[ mod.Index ]?.FixInvalidSettings( mod.Meta ) ?? false ) ) { collection.Save(); } diff --git a/Penumbra/Mods/IModGroup.cs b/Penumbra/Mods/IModGroup.cs deleted file mode 100644 index 498d17d2..00000000 --- a/Penumbra/Mods/IModGroup.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Dalamud.Logging; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public interface IModGroup : IEnumerable< ISubMod > -{ - public string Name { get; } - public string Description { get; } - public SelectType Type { get; } - public int Priority { get; } - - public int OptionPriority( Index optionIdx ); - - public ISubMod this[ Index idx ] { get; } - - public int Count { get; } - - public bool IsOption - => Type switch - { - SelectType.Single => Count > 1, - SelectType.Multi => Count > 0, - _ => false, - }; - - public void Save( DirectoryInfo basePath ); - - public string FileName( DirectoryInfo basePath ) - => Path.Combine( basePath.FullName, Name.RemoveInvalidPathSymbols() + ".json" ); - - public void DeleteFile( DirectoryInfo basePath ) - { - var file = FileName( basePath ); - if( !File.Exists( file ) ) - { - return; - } - - try - { - File.Delete( file ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete file {file}:\n{e}" ); - throw; - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ISubMod.cs b/Penumbra/Mods/ISubMod.cs deleted file mode 100644 index ee05e876..00000000 --- a/Penumbra/Mods/ISubMod.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Penumbra.GameData.ByteString; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Mods; - -public interface ISubMod -{ - public string Name { get; } - - public IReadOnlyDictionary< Utf8GamePath, FullPath > Files { get; } - public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps { get; } - public IReadOnlyList< MetaManipulation > Manipulations { get; } -} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod2.Manager.BasePath.cs similarity index 100% rename from Penumbra/Mods/Mod2.Manager.BasePath.cs rename to Penumbra/Mods/Manager/Mod2.Manager.BasePath.cs diff --git a/Penumbra/Mods/Mod2.Manager.FileSystem.cs b/Penumbra/Mods/Manager/Mod2.Manager.FileSystem.cs similarity index 100% rename from Penumbra/Mods/Mod2.Manager.FileSystem.cs rename to Penumbra/Mods/Manager/Mod2.Manager.FileSystem.cs diff --git a/Penumbra/Mods/Mod2.Manager.Meta.cs b/Penumbra/Mods/Manager/Mod2.Manager.Meta.cs similarity index 100% rename from Penumbra/Mods/Mod2.Manager.Meta.cs rename to Penumbra/Mods/Manager/Mod2.Manager.Meta.cs diff --git a/Penumbra/Mods/Mod2.Manager.Options.cs b/Penumbra/Mods/Manager/Mod2.Manager.Options.cs similarity index 87% rename from Penumbra/Mods/Mod2.Manager.Options.cs rename to Penumbra/Mods/Manager/Mod2.Manager.Options.cs index 3c26d6be..248745ee 100644 --- a/Penumbra/Mods/Mod2.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod2.Manager.Options.cs @@ -29,7 +29,7 @@ public sealed partial class Mod2 public void RenameModGroup( Mod2 mod, int groupIdx, string newName ) { - var group = mod._options[ groupIdx ]; + var group = mod._groups[ groupIdx ]; var oldName = group.Name; if( oldName == newName || !VerifyFileName( mod, group, newName ) ) { @@ -53,25 +53,25 @@ public sealed partial class Mod2 return; } - var maxPriority = mod._options.Max( o => o.Priority ) + 1; + var maxPriority = mod._groups.Max( o => o.Priority ) + 1; - mod._options.Add( type == SelectType.Multi + mod._groups.Add( type == SelectType.Multi ? new MultiModGroup { Name = newName, Priority = maxPriority } : new SingleModGroup { Name = newName, Priority = maxPriority } ); - ModOptionChanged.Invoke( ModOptionChangeType.GroupAdded, mod, mod._options.Count - 1, 0 ); + ModOptionChanged.Invoke( ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, 0 ); } public void DeleteModGroup( Mod2 mod, int groupIdx ) { - var group = mod._options[ groupIdx ]; - mod._options.RemoveAt( groupIdx ); + var group = mod._groups[ groupIdx ]; + mod._groups.RemoveAt( groupIdx ); group.DeleteFile( BasePath ); ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, 0 ); } public void ChangeGroupDescription( Mod2 mod, int groupIdx, string newDescription ) { - var group = mod._options[ groupIdx ]; + var group = mod._groups[ groupIdx ]; if( group.Description == newDescription ) { return; @@ -88,7 +88,7 @@ public sealed partial class Mod2 public void ChangeGroupPriority( Mod2 mod, int groupIdx, int newPriority ) { - var group = mod._options[ groupIdx ]; + var group = mod._groups[ groupIdx ]; if( group.Priority == newPriority ) { return; @@ -105,7 +105,7 @@ public sealed partial class Mod2 public void ChangeOptionPriority( Mod2 mod, int groupIdx, int optionIdx, int newPriority ) { - switch( mod._options[ groupIdx ] ) + switch( mod._groups[ groupIdx ] ) { case SingleModGroup s: ChangeGroupPriority( mod, groupIdx, newPriority ); @@ -124,7 +124,7 @@ public sealed partial class Mod2 public void RenameOption( Mod2 mod, int groupIdx, int optionIdx, string newName ) { - switch( mod._options[ groupIdx ] ) + switch( mod._groups[ groupIdx ] ) { case SingleModGroup s: if( s.OptionData[ optionIdx ].Name == newName ) @@ -150,7 +150,7 @@ public sealed partial class Mod2 public void AddOption( Mod2 mod, int groupIdx, string newName ) { - switch( mod._options[ groupIdx ] ) + switch( mod._groups[ groupIdx ] ) { case SingleModGroup s: s.OptionData.Add( new SubMod { Name = newName } ); @@ -160,12 +160,12 @@ public sealed partial class Mod2 break; } - ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._options[ groupIdx ].Count - 1 ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._groups[ groupIdx ].Count - 1 ); } public void DeleteOption( Mod2 mod, int groupIdx, int optionIdx ) { - switch( mod._options[ groupIdx ] ) + switch( mod._groups[ groupIdx ] ) { case SingleModGroup s: s.OptionData.RemoveAt( optionIdx ); @@ -181,26 +181,24 @@ public sealed partial class Mod2 public void OptionSetManipulation( Mod2 mod, int groupIdx, int optionIdx, MetaManipulation manip, bool delete = false ) { var subMod = GetSubMod( mod, groupIdx, optionIdx ); - var idx = subMod.ManipulationData.FindIndex( m => m.Equals( manip ) ); if( delete ) { - if( idx < 0 ) + if( !subMod.ManipulationData.Remove( manip ) ) { return; } - - subMod.ManipulationData.RemoveAt( idx ); } else { - if( idx >= 0 ) + if( subMod.ManipulationData.TryGetValue( manip, out var oldManip ) ) { - if( manip.EntryEquals( subMod.ManipulationData[ idx ] ) ) + if( manip.EntryEquals( oldManip ) ) { return; } - subMod.ManipulationData[ idx ] = manip; + subMod.ManipulationData.Remove( oldManip ); + subMod.ManipulationData.Add( manip ); } else { @@ -232,7 +230,7 @@ public sealed partial class Mod2 private bool VerifyFileName( Mod2 mod, IModGroup? group, string newName ) { var path = newName.RemoveInvalidPathSymbols(); - if( mod.Options.Any( o => !ReferenceEquals( o, group ) + if( mod.Groups.Any( o => !ReferenceEquals( o, group ) && string.Equals( o.Name.RemoveInvalidPathSymbols(), path, StringComparison.InvariantCultureIgnoreCase ) ) ) { PluginLog.Warning( $"Could not name option {newName} because option with same filename {path} already exists." ); @@ -244,7 +242,7 @@ public sealed partial class Mod2 private static SubMod GetSubMod( Mod2 mod, int groupIdx, int optionIdx ) { - return mod._options[ groupIdx ] switch + return mod._groups[ groupIdx ] switch { SingleModGroup s => s.OptionData[ optionIdx ], MultiModGroup m => m.PrioritizedOptions[ optionIdx ].Mod, @@ -285,15 +283,15 @@ public sealed partial class Mod2 // File deletion is handled in the actual function. if( type != ModOptionChangeType.GroupDeleted ) { - mod._options[groupIdx].Save( mod.BasePath ); + IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.BasePath ); } // State can not change on adding groups, as they have no immediate options. mod.HasOptions = type switch { - ModOptionChangeType.GroupDeleted => mod.HasOptions = mod.Options.Any( o => o.IsOption ), - ModOptionChangeType.OptionAdded => mod.HasOptions |= mod._options[groupIdx].IsOption, - ModOptionChangeType.OptionDeleted => mod.HasOptions = mod.Options.Any( o => o.IsOption ), + ModOptionChangeType.GroupDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), + ModOptionChangeType.OptionAdded => mod.HasOptions |= mod._groups[ groupIdx ].IsOption, + ModOptionChangeType.OptionDeleted => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), _ => mod.HasOptions, }; } diff --git a/Penumbra/Mods/Mod2.Manager.Root.cs b/Penumbra/Mods/Manager/Mod2.Manager.Root.cs similarity index 100% rename from Penumbra/Mods/Mod2.Manager.Root.cs rename to Penumbra/Mods/Manager/Mod2.Manager.Root.cs diff --git a/Penumbra/Mods/Mod2.Manager.cs b/Penumbra/Mods/Manager/Mod2.Manager.cs similarity index 94% rename from Penumbra/Mods/Mod2.Manager.cs rename to Penumbra/Mods/Manager/Mod2.Manager.cs index f385a7a0..2ba5fe27 100644 --- a/Penumbra/Mods/Mod2.Manager.cs +++ b/Penumbra/Mods/Manager/Mod2.Manager.cs @@ -1,8 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.IO; -using Newtonsoft.Json.Linq; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Mod2.BasePath.cs b/Penumbra/Mods/Mod2.BasePath.cs index 2fe9dfe3..5b086b31 100644 --- a/Penumbra/Mods/Mod2.BasePath.cs +++ b/Penumbra/Mods/Mod2.BasePath.cs @@ -15,18 +15,10 @@ public partial class Mod2 public DirectoryInfo BasePath { get; private set; } public int Index { get; private set; } = -1; - private FileInfo MetaFile - => new(Path.Combine( BasePath.FullName, "meta.json" )); + private Mod2( DirectoryInfo basePath ) + => BasePath = basePath; - private Mod2( ModFolder parentFolder, DirectoryInfo basePath ) - { - BasePath = basePath; - Order = new Mod.SortOrder( parentFolder, Name ); - //Order.ParentFolder.AddMod( this ); // TODO - ComputeChangedItems(); - } - - public static Mod2? LoadMod( ModFolder parentFolder, DirectoryInfo basePath ) + public static Mod2? LoadMod( DirectoryInfo basePath ) { basePath.Refresh(); if( !basePath.Exists ) @@ -35,22 +27,18 @@ public partial class Mod2 return null; } - var mod = new Mod2( parentFolder, basePath ); - - var metaFile = mod.MetaFile; - if( !metaFile.Exists ) - { - PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name ); - return null; - } - - mod.LoadMetaFromFile( metaFile ); + var mod = new Mod2( basePath ); + mod.LoadMeta(); if( mod.Name.Length == 0 ) { PluginLog.Error( $"Mod at {basePath} without name is not supported." ); } - mod.ReloadFiles(); + mod.LoadDefaultOption(); + mod.LoadAllGroups(); + mod.ComputeChangedItems(); + mod.SetHasOptions(); + return mod; } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.ChangedItems.cs b/Penumbra/Mods/Mod2.ChangedItems.cs index c6007321..bcd7fc4b 100644 --- a/Penumbra/Mods/Mod2.ChangedItems.cs +++ b/Penumbra/Mods/Mod2.ChangedItems.cs @@ -5,16 +5,16 @@ namespace Penumbra.Mods; public sealed partial class Mod2 { - public SortedList ChangedItems { get; } = new(); + public SortedList< string, object? > ChangedItems { get; } = new(); public string LowerChangedItemsString { get; private set; } = string.Empty; public void ComputeChangedItems() { var identifier = GameData.GameData.GetIdentifier(); ChangedItems.Clear(); - foreach( var (file, _) in AllFiles ) + foreach( var gamePath in AllRedirects ) { - identifier.Identify( ChangedItems, file.ToGamePath() ); + identifier.Identify( ChangedItems, gamePath.ToGamePath() ); } // TODO: manipulations diff --git a/Penumbra/Mods/Mod2.Files.SubMod.cs b/Penumbra/Mods/Mod2.Files.SubMod.cs deleted file mode 100644 index 245de4f8..00000000 --- a/Penumbra/Mods/Mod2.Files.SubMod.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using Penumbra.GameData.ByteString; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Mods; - -public partial class Mod2 -{ - private sealed class SubMod : ISubMod - { - public string Name { get; set; } = "Default"; - - [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] - public readonly Dictionary< Utf8GamePath, FullPath > FileData = new(); - - [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] - public readonly Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); - - public readonly List< MetaManipulation > ManipulationData = new(); - - public IReadOnlyDictionary< Utf8GamePath, FullPath > Files - => FileData; - - public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps - => FileSwapData; - - public IReadOnlyList< MetaManipulation > Manipulations - => ManipulationData; - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.cs b/Penumbra/Mods/Mod2.Files.cs index 67cd3ad4..7ba7c63f 100644 --- a/Penumbra/Mods/Mod2.Files.cs +++ b/Penumbra/Mods/Mod2.Files.cs @@ -1,6 +1,9 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Dalamud.Logging; +using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; @@ -8,36 +11,88 @@ namespace Penumbra.Mods; public partial class Mod2 { - public IReadOnlyDictionary< Utf8GamePath, FullPath > RemainingFiles - => _remainingFiles; + public ISubMod Default + => _default; - public IReadOnlyList< IModGroup > Options - => _options; + public IReadOnlyList< IModGroup > Groups + => _groups; - public bool HasOptions { get; private set; } = false; + public bool HasOptions { get; private set; } private void SetHasOptions() { - HasOptions = _options.Any( o - => o is MultiModGroup m && m.PrioritizedOptions.Count > 0 || o is SingleModGroup s && s.OptionData.Count > 1 ); + HasOptions = _groups.Any( o + => o is MultiModGroup m && m.PrioritizedOptions.Count > 0 + || o is SingleModGroup s && s.OptionData.Count > 1 ); } - private readonly Dictionary< Utf8GamePath, FullPath > _remainingFiles = new(); - private readonly List< IModGroup > _options = new(); - public IEnumerable< (Utf8GamePath, FullPath) > AllFiles - => _remainingFiles.Concat( _options.SelectMany( o => o ).SelectMany( o => o.Files.Concat( o.FileSwaps ) ) ) - .Select( kvp => ( kvp.Key, kvp.Value ) ); + private readonly SubMod _default = new(); + private readonly List< IModGroup > _groups = new(); + + public IEnumerable< ISubMod > AllSubMods + => _groups.SelectMany( o => o ).Prepend( _default ); public IEnumerable< MetaManipulation > AllManipulations - => _options.SelectMany( o => o ).SelectMany( o => o.Manipulations ); + => AllSubMods.SelectMany( s => s.Manipulations ); - private void ReloadFiles() + public IEnumerable< Utf8GamePath > AllRedirects + => AllSubMods.SelectMany( s => s.Files.Keys.Concat( s.FileSwaps.Keys ) ); + + public IEnumerable< FullPath > AllFiles + => AllSubMods.SelectMany( o => o.Files ) + .Select( p => p.Value ); + + public IEnumerable< FileInfo > GroupFiles + => BasePath.EnumerateFiles( "group_*.json" ); + + public List< FullPath > FindUnusedFiles() { - // _remainingFiles.Clear(); - // _options.Clear(); - // HasOptions = false; - // if( !Directory.Exists( BasePath.FullName ) ) - // return; + var modFiles = AllFiles.ToHashSet(); + return BasePath.EnumerateDirectories() + .SelectMany( f => f.EnumerateFiles( "*", SearchOption.AllDirectories ) ) + .Select( f => new FullPath( f ) ) + .Where( f => !modFiles.Contains( f ) ) + .ToList(); + } + + public List< FullPath > FindMissingFiles() + => AllFiles.Where( f => !f.Exists ).ToList(); + + public static IModGroup? LoadModGroup( FileInfo file, DirectoryInfo basePath ) + { + if( !File.Exists( file.FullName ) ) + { + return null; + } + + try + { + var json = JObject.Parse( File.ReadAllText( file.FullName ) ); + switch( json[ nameof( Type ) ]?.ToObject< SelectType >() ?? SelectType.Single ) + { + case SelectType.Multi: return MultiModGroup.Load( json, basePath ); + case SelectType.Single: return SingleModGroup.Load( json, basePath ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not read mod group from {file.FullName}:\n{e}" ); + } + + return null; + } + + private void LoadAllGroups() + { + _groups.Clear(); + foreach( var file in GroupFiles ) + { + var group = LoadModGroup( file, BasePath ); + if( group != null ) + { + _groups.Add( group ); + } + } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Meta.Migration.cs b/Penumbra/Mods/Mod2.Meta.Migration.cs index a0975396..f7a5e63f 100644 --- a/Penumbra/Mods/Mod2.Meta.Migration.cs +++ b/Penumbra/Mods/Mod2.Meta.Migration.cs @@ -1,7 +1,12 @@ +using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; +using Penumbra.Importer; using Penumbra.Util; namespace Penumbra.Mods; @@ -10,29 +15,184 @@ public sealed partial class Mod2 { private static class Migration { - public static void Migrate( Mod2 mod, string text ) - { - MigrateV0ToV1( mod, text ); - } + public static bool Migrate( Mod2 mod, JObject json ) + => MigrateV0ToV1( mod, json ); - private static void MigrateV0ToV1( Mod2 mod, string text ) + private static bool MigrateV0ToV1( Mod2 mod, JObject json ) { if( mod.FileVersion > 0 ) + { + return false; + } + + var swaps = json[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() + ?? new Dictionary< Utf8GamePath, FullPath >(); + var groups = json[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); + var priority = 1; + foreach( var group in groups.Values ) + { + ConvertGroup( mod, group, ref priority ); + } + + foreach( var unusedFile in mod.FindUnusedFiles() ) + { + if( unusedFile.ToGamePath( mod.BasePath, out var gamePath ) ) + { + mod._default.FileData.Add( gamePath, unusedFile ); + } + } + + mod._default.FileSwapData.Clear(); + mod._default.FileSwapData.EnsureCapacity( swaps.Count ); + foreach( var (gamePath, swapPath) in swaps ) + { + mod._default.FileSwapData.Add( gamePath, swapPath ); + } + + HandleMetaChanges( mod._default, mod.BasePath ); + foreach( var group in mod.Groups ) + { + IModGroup.SaveModGroup( group, mod.BasePath ); + } + + mod.SaveDefaultMod(); + + return true; + } + + private static void ConvertGroup( Mod2 mod, OptionGroupV0 group, ref int priority ) + { + if( group.Options.Count == 0 ) { return; } - var data = JObject.Parse( text ); - var swaps = data[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() - ?? new Dictionary< Utf8GamePath, FullPath >(); - var groups = data[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); - foreach( var group in groups.Values ) - { } + switch( group.SelectionType ) + { + case SelectType.Multi: - foreach( var swap in swaps ) - { } + var optionPriority = 0; + var newMultiGroup = new MultiModGroup() + { + Name = group.GroupName, + Priority = priority++, + Description = string.Empty, + }; + mod._groups.Add( newMultiGroup ); + foreach( var option in group.Options ) + { + newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod.BasePath, option ), optionPriority++ ) ); + } + + break; + case SelectType.Single: + if( group.Options.Count == 1 ) + { + AddFilesToSubMod( mod._default, mod.BasePath, group.Options[ 0 ] ); + return; + } + + var newSingleGroup = new SingleModGroup() + { + Name = group.GroupName, + Priority = priority++, + Description = string.Empty, + }; + mod._groups.Add( newSingleGroup ); + foreach( var option in group.Options ) + { + newSingleGroup.OptionData.Add( SubModFromOption( mod.BasePath, option ) ); + } + + break; + } } + private static void AddFilesToSubMod( SubMod mod, DirectoryInfo basePath, OptionV0 option ) + { + foreach( var (relPath, gamePaths) in option.OptionFiles ) + { + foreach( var gamePath in gamePaths ) + { + mod.FileData.TryAdd( gamePath, new FullPath( basePath, relPath ) ); + } + } + } + + private static void HandleMetaChanges( SubMod subMod, DirectoryInfo basePath ) + { + foreach( var (key, file) in subMod.Files.ToList() ) + { + try + { + switch( file.Extension ) + { + case ".meta": + subMod.FileData.Remove( key ); + if( !file.Exists ) + { + continue; + } + + var meta = new TexToolsMeta( File.ReadAllBytes( file.FullName ) ); + foreach( var manip in meta.EqpManipulations ) + { + subMod.ManipulationData.Add( manip ); + } + + foreach( var manip in meta.EqdpManipulations ) + { + subMod.ManipulationData.Add( manip ); + } + + foreach( var manip in meta.EstManipulations ) + { + subMod.ManipulationData.Add( manip ); + } + + foreach( var manip in meta.GmpManipulations ) + { + subMod.ManipulationData.Add( manip ); + } + + foreach( var manip in meta.ImcManipulations ) + { + subMod.ManipulationData.Add( manip ); + } + + break; + case ".rgsp": + subMod.FileData.Remove( key ); + if( !file.Exists ) + { + continue; + } + + var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ); + foreach( var manip in rgsp.RspManipulations ) + { + subMod.ManipulationData.Add( manip ); + } + + break; + default: continue; + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not migrate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); + continue; + } + } + } + + private static SubMod SubModFromOption( DirectoryInfo basePath, OptionV0 option ) + { + var subMod = new SubMod() { Name = option.OptionName }; + AddFilesToSubMod( subMod, basePath, option ); + HandleMetaChanges( subMod, basePath ); + return subMod; + } private struct OptionV0 { diff --git a/Penumbra/Mods/Mod2.Meta.cs b/Penumbra/Mods/Mod2.Meta.cs index 8a440829..bcc05cf8 100644 --- a/Penumbra/Mods/Mod2.Meta.cs +++ b/Penumbra/Mods/Mod2.Meta.cs @@ -17,6 +17,7 @@ public enum MetaChangeType : byte Version = 0x08, Website = 0x10, Deletion = 0x20, + Migration = 0x40, } public sealed partial class Mod2 @@ -29,19 +30,21 @@ public sealed partial class Mod2 public string Version { get; private set; } = string.Empty; public string Website { get; private set; } = string.Empty; - private void SaveMeta() - => SaveToFile( MetaFile ); + private FileInfo MetaFile + => new(Path.Combine( BasePath.FullName, "meta.json" )); - private MetaChangeType LoadMetaFromFile( FileInfo filePath ) + private MetaChangeType LoadMeta() { - if( !File.Exists( filePath.FullName ) ) + var metaFile = MetaFile; + if( !File.Exists( metaFile.FullName ) ) { + PluginLog.Debug( "No mod meta found for {ModLocation}.", BasePath.Name ); return MetaChangeType.Deletion; } try { - var text = File.ReadAllText( filePath.FullName ); + var text = File.ReadAllText( metaFile.FullName ); var json = JObject.Parse( text ); var newName = json[ nameof( Name ) ]?.Value< string >() ?? string.Empty; @@ -52,12 +55,6 @@ public sealed partial class Mod2 var newFileVersion = json[ nameof( FileVersion ) ]?.Value< uint >() ?? 0; MetaChangeType changes = 0; - if( newFileVersion < CurrentFileVersion ) - { - Migration.Migrate( this, text ); - FileVersion = newFileVersion; - } - if( Name != newName ) { changes |= MetaChangeType.Name; @@ -88,6 +85,14 @@ public sealed partial class Mod2 Website = newWebsite; } + if( FileVersion != newFileVersion ) + { + FileVersion = newFileVersion; + if( Migration.Migrate( this, json ) ) + { + changes |= MetaChangeType.Migration; + } + } return changes; } @@ -98,8 +103,9 @@ public sealed partial class Mod2 } } - private void SaveToFile( FileInfo filePath ) + private void SaveMeta() { + var metaFile = MetaFile; try { var jObject = new JObject @@ -111,11 +117,11 @@ public sealed partial class Mod2 { nameof( Version ), JToken.FromObject( Version ) }, { nameof( Website ), JToken.FromObject( Website ) }, }; - File.WriteAllText( filePath.FullName, jObject.ToString( Formatting.Indented ) ); + File.WriteAllText( metaFile.FullName, jObject.ToString( Formatting.Indented ) ); } catch( Exception e ) { - PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" ); + PluginLog.Error( $"Could not write meta file for mod {Name} to {metaFile.FullName}:\n{e}" ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.SortOrder.cs b/Penumbra/Mods/Mod2.SortOrder.cs deleted file mode 100644 index e3f1cc37..00000000 --- a/Penumbra/Mods/Mod2.SortOrder.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Penumbra.Mods; - -public sealed partial class Mod2 -{ - public Mod.SortOrder Order; - public override string ToString() - => Order.FullPath; -} \ No newline at end of file diff --git a/Penumbra/Mods/ModFileSystemA.cs b/Penumbra/Mods/ModFileSystemA.cs index 1bece099..c684d371 100644 --- a/Penumbra/Mods/ModFileSystemA.cs +++ b/Penumbra/Mods/ModFileSystemA.cs @@ -1,26 +1,52 @@ +using System; using System.IO; using OtterGui.Filesystem; namespace Penumbra.Mods; -public sealed class ModFileSystemA : FileSystem< Mod > +public sealed class ModFileSystemA : FileSystem< Mod >, IDisposable { + // Save the current sort order. + // Does not save or copy the backup in the current mod directory, + // as this is done on mod directory changes only. public void Save() => SaveToFile( new FileInfo( Mod.Manager.SortOrderFile ), SaveMod, true ); + // Create a new ModFileSystem from the currently loaded mods and the current sort order file. public static ModFileSystemA Load() { - var x = new ModFileSystemA(); - if( x.Load( new FileInfo( Mod.Manager.SortOrderFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) ) - { - x.Save(); - } + var ret = new ModFileSystemA(); + ret.Reload(); - x.Changed += ( _1, _2, _3, _4 ) => x.Save(); + ret.Changed += ret.OnChange; + Penumbra.ModManager.ModDiscoveryFinished += ret.Reload; - return x; + return ret; } + public void Dispose() + => Penumbra.ModManager.ModDiscoveryFinished -= Reload; + + // Reload the whole filesystem from currently loaded mods and the current sort order file. + // Used on construction and on mod rediscoveries. + private void Reload() + { + if( Load( new FileInfo( Mod.Manager.SortOrderFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) ) + { + Save(); + } + } + + // Save the filesystem on every filesystem change except full reloading. + private void OnChange( FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3 ) + { + if( type != FileSystemChangeType.Reload ) + { + Save(); + } + } + + // Used for saving and loading. private static string ModToIdentifier( Mod mod ) => mod.BasePath.Name; @@ -29,6 +55,7 @@ public sealed class ModFileSystemA : FileSystem< Mod > private static (string, bool) SaveMod( Mod mod, string fullPath ) { + // Only save pairs with non-default paths. if( fullPath == ModToName( mod ) ) { return ( string.Empty, false ); diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index fc126dd4..1f2f5cb7 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -42,7 +42,7 @@ public partial class Mod public ModFolder StructuredMods { get; } = ModFileSystem.Root; - public delegate void ModChangeDelegate( ChangeType type, int modIndex, Mod mod ); + public delegate void ModChangeDelegate( ChangeType type, Mod mod ); public event ModChangeDelegate? ModChange; public event Action? ModDiscoveryStarted; @@ -90,8 +90,14 @@ public partial class Mod } } + if( !firstTime ) + { + HandleSortOrderFiles( newDir ); + } + BasePath = newDir; - Valid = true; + + Valid = true; if( Config.ModDirectory != BasePath.FullName ) { Config.ModDirectory = BasePath.FullName; @@ -100,8 +106,37 @@ public partial class Mod } } - public static string SortOrderFile = Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), - "sort_order.json" ); + private const string SortOrderFileName = "sort_order.json"; + public static string SortOrderFile = Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), SortOrderFileName ); + + private void HandleSortOrderFiles( DirectoryInfo newDir ) + { + try + { + var mainFile = SortOrderFile; + // Copy old sort order to backup. + var oldSortOrderFile = Path.Combine( BasePath.FullName, SortOrderFileName ); + PluginLog.Debug( "Copying current sort older file to {BackupFile}...", oldSortOrderFile ); + File.Copy( mainFile, oldSortOrderFile, true ); + BasePath = newDir; + var newSortOrderFile = Path.Combine( newDir.FullName, SortOrderFileName ); + // Copy new sort order to main, if it exists. + if( File.Exists( newSortOrderFile ) ) + { + File.Copy( newSortOrderFile, mainFile, true ); + PluginLog.Debug( "Copying stored sort order file from {BackupFile}...", newSortOrderFile ); + } + else + { + File.Delete( mainFile ); + PluginLog.Debug( "Deleting current sort order file...", newSortOrderFile ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not swap Sort Order files:\n{e}" ); + } + } public Manager() { @@ -215,7 +250,7 @@ public partial class Mod --_mods[ i ].Index; } - ModChange?.Invoke( ChangeType.Removed, idx, mod ); + ModChange?.Invoke( ChangeType.Removed, mod ); } } @@ -241,7 +276,7 @@ public partial class Mod } _mods.Add( mod ); - ModChange?.Invoke( ChangeType.Added, _mods.Count - 1, mod ); + ModChange?.Invoke( ChangeType.Added, mod ); return _mods.Count - 1; } @@ -287,7 +322,7 @@ public partial class Mod } // TODO: more specific mod changes? - ModChange?.Invoke( ChangeType.Changed, idx, mod ); + ModChange?.Invoke( ChangeType.Changed, mod ); return true; } diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index baf8ffe6..82869ad9 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -43,7 +43,7 @@ public static class ModManagerEditExtensions manager.TemporaryModSortOrder[ mod.BasePath.Name ] = mod.Order.FullPath; } - manager.Config.Save(); + Penumbra.Config.Save(); return true; } @@ -79,7 +79,7 @@ public static class ModManagerEditExtensions { manager.TemporaryModSortOrder[ newDir.Name ] = manager.TemporaryModSortOrder[ oldBasePath.Name ]; manager.TemporaryModSortOrder.Remove( oldBasePath.Name ); - manager.Config.Save(); + Penumbra.Config.Save(); } var idx = manager.Mods.IndexOf( mod ); diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs new file mode 100644 index 00000000..ac643ecb --- /dev/null +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Dalamud.Logging; +using Newtonsoft.Json; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public interface IModGroup : IEnumerable< ISubMod > +{ + public string Name { get; } + public string Description { get; } + public SelectType Type { get; } + public int Priority { get; } + + public int OptionPriority( Index optionIdx ); + + public ISubMod this[ Index idx ] { get; } + + public int Count { get; } + + public bool IsOption + => Type switch + { + SelectType.Single => Count > 1, + SelectType.Multi => Count > 0, + _ => false, + }; + + public string FileName( DirectoryInfo basePath ) + => Path.Combine( basePath.FullName, $"group_{Name.RemoveInvalidPathSymbols().ToLowerInvariant()}.json" ); + + public void DeleteFile( DirectoryInfo basePath ) + { + var file = FileName( basePath ); + if( !File.Exists( file ) ) + { + return; + } + + try + { + File.Delete( file ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete file {file}:\n{e}" ); + throw; + } + } + + public static void SaveModGroup( IModGroup group, DirectoryInfo basePath ) + { + var file = group.FileName( basePath ); + using var s = File.Exists( file ) ? File.Open( file, FileMode.Truncate ) : File.Open( file, FileMode.CreateNew ); + using var writer = new StreamWriter( s ); + using var j = new JsonTextWriter( writer ) { Formatting = Formatting.Indented }; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + j.WriteStartObject(); + j.WritePropertyName( nameof( group.Name ) ); + j.WriteValue( group.Name ); + j.WritePropertyName( nameof( group.Description ) ); + j.WriteValue( group.Description ); + j.WritePropertyName( nameof( group.Priority ) ); + j.WriteValue( group.Priority ); + j.WritePropertyName( nameof( Type ) ); + j.WriteValue( group.Type.ToString() ); + j.WritePropertyName( "Options" ); + j.WriteStartArray(); + for( var idx = 0; idx < group.Count; ++idx ) + { + ISubMod.WriteSubMod( j, serializer, group[ idx ], basePath, group.Type == SelectType.Multi ? group.OptionPriority( idx ) : null ); + } + + j.WriteEndArray(); + j.WriteEndObject(); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs new file mode 100644 index 00000000..f6781566 --- /dev/null +++ b/Penumbra/Mods/Subclasses/ISubMod.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Mods; + +public interface ISubMod +{ + public string Name { get; } + + public IReadOnlyDictionary< Utf8GamePath, FullPath > Files { get; } + public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps { get; } + public IReadOnlySet< MetaManipulation > Manipulations { get; } + + public static void WriteSubMod( JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, int? priority ) + { + j.WriteStartObject(); + j.WritePropertyName( nameof( Name ) ); + j.WriteValue( mod.Name ); + if( priority != null ) + { + j.WritePropertyName( nameof( IModGroup.Priority ) ); + j.WriteValue( priority.Value ); + } + + j.WritePropertyName( nameof( mod.Files ) ); + j.WriteStartObject(); + foreach( var (gamePath, file) in mod.Files ) + { + if( file.ToRelPath( basePath, out var relPath ) ) + { + j.WritePropertyName( gamePath.ToString() ); + j.WriteValue( relPath.ToString() ); + } + } + + j.WriteEndObject(); + j.WritePropertyName( nameof( mod.FileSwaps ) ); + j.WriteStartObject(); + foreach( var (gamePath, file) in mod.FileSwaps ) + { + j.WritePropertyName( gamePath.ToString() ); + j.WriteValue( file.ToString() ); + } + + j.WriteEndObject(); + j.WritePropertyName( nameof( mod.Manipulations ) ); + serializer.Serialize( j, mod.Manipulations ); + j.WriteEndObject(); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod2.Files.MultiModGroup.cs similarity index 52% rename from Penumbra/Mods/Mod2.Files.MultiModGroup.cs rename to Penumbra/Mods/Subclasses/Mod2.Files.MultiModGroup.cs index 4ce3fd48..cd05a844 100644 --- a/Penumbra/Mods/Mod2.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod2.Files.MultiModGroup.cs @@ -3,8 +3,8 @@ using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; -using Dalamud.Logging; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Penumbra.Mods; @@ -17,7 +17,7 @@ public partial class Mod2 public string Name { get; set; } = "Group"; public string Description { get; set; } = "A non-exclusive group of settings."; - public int Priority { get; set; } = 0; + public int Priority { get; set; } public int OptionPriority( Index idx ) => PrioritizedOptions[ idx ].Priority; @@ -25,6 +25,7 @@ public partial class Mod2 public ISubMod this[ Index idx ] => PrioritizedOptions[ idx ].Mod; + [JsonIgnore] public int Count => PrioritizedOptions.Count; @@ -36,18 +37,31 @@ public partial class Mod2 IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public void Save( DirectoryInfo basePath ) + public static MultiModGroup? Load( JObject json, DirectoryInfo basePath ) { - var path = ( ( IModGroup )this ).FileName( basePath ); - try + var options = json[ "Options" ]; + var ret = new MultiModGroup() { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( path, text ); - } - catch( Exception e ) + Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, + Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, + Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, + }; + if( ret.Name.Length == 0 ) { - PluginLog.Error( $"Could not save option group {Name} to {path}:\n{e}" ); + return null; } + + if( options != null ) + { + foreach( var child in options.Children() ) + { + var subMod = new SubMod(); + subMod.Load( basePath, child, out var priority ); + ret.PrioritizedOptions.Add( ( subMod, priority ) ); + } + } + + return ret; } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.SingleModGroup.cs b/Penumbra/Mods/Subclasses/Mod2.Files.SingleModGroup.cs similarity index 50% rename from Penumbra/Mods/Mod2.Files.SingleModGroup.cs rename to Penumbra/Mods/Subclasses/Mod2.Files.SingleModGroup.cs index 92212ad7..40ebbcce 100644 --- a/Penumbra/Mods/Mod2.Files.SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod2.Files.SingleModGroup.cs @@ -2,8 +2,8 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; -using Dalamud.Logging; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Penumbra.Mods; @@ -16,7 +16,7 @@ public partial class Mod2 public string Name { get; set; } = "Option"; public string Description { get; set; } = "A mutually exclusive group of settings."; - public int Priority { get; set; } = 0; + public int Priority { get; set; } public readonly List< SubMod > OptionData = new(); @@ -26,6 +26,7 @@ public partial class Mod2 public ISubMod this[ Index idx ] => OptionData[ idx ]; + [JsonIgnore] public int Count => OptionData.Count; @@ -35,18 +36,31 @@ public partial class Mod2 IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public void Save( DirectoryInfo basePath ) + public static SingleModGroup? Load( JObject json, DirectoryInfo basePath ) { - var path = ( ( IModGroup )this ).FileName( basePath ); - try + var options = json[ "Options" ]; + var ret = new SingleModGroup { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( path, text ); - } - catch( Exception e ) + Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, + Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, + Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, + }; + if( ret.Name.Length == 0 ) { - PluginLog.Error( $"Could not save option group {Name} to {path}:\n{e}" ); + return null; } + + if( options != null ) + { + foreach( var child in options.Children() ) + { + var subMod = new SubMod(); + subMod.Load( basePath, child, out _ ); + ret.OptionData.Add( subMod ); + } + } + + return ret; } } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs new file mode 100644 index 00000000..766c59da --- /dev/null +++ b/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Mods; + +public partial class Mod2 +{ + private string DefaultFile + => Path.Combine( BasePath.FullName, "default_mod.json" ); + + private void SaveDefaultMod() + { + var defaultFile = DefaultFile; + + using var stream = File.Exists( defaultFile ) + ? File.Open( defaultFile, FileMode.Truncate ) + : File.Open( defaultFile, FileMode.CreateNew ); + + using var w = new StreamWriter( stream ); + using var j = new JsonTextWriter( w ); + j.Formatting = Formatting.Indented; + var serializer = new JsonSerializer + { + Formatting = Formatting.Indented, + }; + ISubMod.WriteSubMod( j, serializer, _default, BasePath, 0 ); + } + + private void LoadDefaultOption() + { + var defaultFile = DefaultFile; + try + { + if( !File.Exists( defaultFile ) ) + { + _default.Load( BasePath, new JObject(), out _ ); + } + else + { + _default.Load( BasePath, JObject.Parse( File.ReadAllText( defaultFile ) ), out _ ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not parse default file for {Name}:\n{e}" ); + } + } + + + private sealed class SubMod : ISubMod + { + public string Name { get; set; } = "Default"; + + public readonly Dictionary< Utf8GamePath, FullPath > FileData = new(); + public readonly Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); + public readonly HashSet< MetaManipulation > ManipulationData = new(); + + public IReadOnlyDictionary< Utf8GamePath, FullPath > Files + => FileData; + + public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps + => FileSwapData; + + public IReadOnlySet< MetaManipulation > Manipulations + => ManipulationData; + + public void Load( DirectoryInfo basePath, JToken json, out int priority ) + { + FileData.Clear(); + FileSwapData.Clear(); + ManipulationData.Clear(); + + Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty; + priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0; + + var files = ( JObject? )json[ nameof( Files ) ]; + if( files != null ) + { + foreach( var property in files.Properties() ) + { + if( Utf8GamePath.FromString( property.Name, out var p, true ) ) + { + FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) ); + } + } + } + + var swaps = ( JObject? )json[ nameof( FileSwaps ) ]; + if( swaps != null ) + { + foreach( var property in swaps.Properties() ) + { + if( Utf8GamePath.FromString( property.Name, out var p, true ) ) + { + FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) ); + } + } + } + + var manips = json[ nameof( Manipulations ) ]; + if( manips != null ) + { + foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ) ) + { + ManipulationData.Add( s ); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs similarity index 100% rename from Penumbra/Mods/ModSettings.cs rename to Penumbra/Mods/Subclasses/ModSettings.cs diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index ac6dcfa7..37ae5260 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -122,9 +122,11 @@ public class Penumbra : IDalamudPlugin } ResidentResources.Reload(); - //var c = ModCollection2.LoadFromFile( new FileInfo(@"C:\Users\Ozy\AppData\Roaming\XIVLauncher\pluginConfigs\Penumbra\collections\Rayla.json"), - // out var inheritance ); - //c?.Save(); + + foreach( var folder in ModManager.BasePath.EnumerateDirectories() ) + { + var m = Mod2.LoadMod( folder ); + } } public bool Enable() diff --git a/Penumbra/Penumbra.csproj.DotSettings b/Penumbra/Penumbra.csproj.DotSettings new file mode 100644 index 00000000..b43e7ec2 --- /dev/null +++ b/Penumbra/Penumbra.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabBrowser.cs b/Penumbra/UI/MenuTabs/TabBrowser.cs index 6e00a9af..1dc57a16 100644 --- a/Penumbra/UI/MenuTabs/TabBrowser.cs +++ b/Penumbra/UI/MenuTabs/TabBrowser.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using System.Runtime.InteropServices; using OtterGui.Raii; using Penumbra.Mods; +using Penumbra.UI.Classes; namespace Penumbra.UI; @@ -20,7 +22,7 @@ public partial class SettingsInterface public TabBrowser() { _fileSystem = ModFileSystemA.Load(); - _selector = new ModFileSystemSelector( _fileSystem ); + _selector = new ModFileSystemSelector( _fileSystem, new HashSet() ); } public void Draw() diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs index 69956e52..be4bf261 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs @@ -148,7 +148,7 @@ public class ModListCache : IDisposable PluginLog.Debug( "Resetting mod selector list..." ); if( _modsInOrder.Count == 0 ) { - foreach( var modData in _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ) + foreach( var modData in _manager.StructuredMods.AllMods( Penumbra.Config.SortFoldersFirst ) ) { var idx = Penumbra.ModManager.Mods.IndexOf( modData ); var mod = new FullMod( Penumbra.CollectionManager.Current[ idx ].Settings ?? ModSettings.DefaultSettings( modData.Meta ),