Start for Mod rework, currently not applied.

This commit is contained in:
Ottermandias 2022-03-31 13:33:03 +02:00
parent 1861c40a4f
commit 5bfcb71f52
30 changed files with 1440 additions and 306 deletions

View file

@ -0,0 +1,53 @@
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;
}
}
}

14
Penumbra/Mods/ISubMod.cs Normal file
View file

@ -0,0 +1,14 @@
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; }
}

View file

@ -28,7 +28,6 @@ public partial class Mod
}
}
public SortOrder( ModFolder parentFolder, string name )
{
ParentFolder = parentFolder;

View file

@ -9,7 +9,7 @@ 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 sealed partial class Mod
{
public DirectoryInfo BasePath;
public ModMeta Meta;

View file

@ -0,0 +1,56 @@
using System.IO;
using Dalamud.Logging;
namespace Penumbra.Mods;
public enum ModPathChangeType
{
Added,
Deleted,
Moved,
}
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( 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 )
{
basePath.Refresh();
if( !basePath.Exists )
{
PluginLog.Error( $"Supplied mod directory {basePath} does not exist." );
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 );
if( mod.Name.Length == 0 )
{
PluginLog.Error( $"Mod at {basePath} without name is not supported." );
}
mod.ReloadFiles();
return mod;
}
}

View file

@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Linq;
namespace Penumbra.Mods;
public sealed partial class Mod2
{
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 )
{
identifier.Identify( ChangedItems, file.ToGamePath() );
}
// TODO: manipulations
LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) );
}
}

View file

@ -0,0 +1,53 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Logging;
using Newtonsoft.Json;
namespace Penumbra.Mods;
public partial class Mod2
{
private sealed class MultiModGroup : IModGroup
{
public SelectType Type
=> SelectType.Multi;
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 OptionPriority( Index idx )
=> PrioritizedOptions[ idx ].Priority;
public ISubMod this[ Index idx ]
=> PrioritizedOptions[ idx ].Mod;
public int Count
=> PrioritizedOptions.Count;
public readonly List< (SubMod Mod, int Priority) > PrioritizedOptions = new();
public IEnumerator< ISubMod > GetEnumerator()
=> PrioritizedOptions.Select( o => o.Mod ).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public void Save( DirectoryInfo basePath )
{
var path = ( ( IModGroup )this ).FileName( basePath );
try
{
var text = JsonConvert.SerializeObject( this, Formatting.Indented );
File.WriteAllText( path, text );
}
catch( Exception e )
{
PluginLog.Error( $"Could not save option group {Name} to {path}:\n{e}" );
}
}
}
}

View file

@ -0,0 +1,52 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Dalamud.Logging;
using Newtonsoft.Json;
namespace Penumbra.Mods;
public partial class Mod2
{
private sealed class SingleModGroup : IModGroup
{
public SelectType Type
=> SelectType.Single;
public string Name { get; set; } = "Option";
public string Description { get; set; } = "A mutually exclusive group of settings.";
public int Priority { get; set; } = 0;
public readonly List< SubMod > OptionData = new();
public int OptionPriority( Index _ )
=> Priority;
public ISubMod this[ Index idx ]
=> OptionData[ idx ];
public int Count
=> OptionData.Count;
public IEnumerator< ISubMod > GetEnumerator()
=> OptionData.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public void Save( DirectoryInfo basePath )
{
var path = ( ( IModGroup )this ).FileName( basePath );
try
{
var text = JsonConvert.SerializeObject( this, Formatting.Indented );
File.WriteAllText( path, text );
}
catch( Exception e )
{
PluginLog.Error( $"Could not save option group {Name} to {path}:\n{e}" );
}
}
}
}

View file

@ -0,0 +1,31 @@
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;
}
}

View file

@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.GameData.ByteString;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Mods;
public partial class Mod2
{
public IReadOnlyDictionary< Utf8GamePath, FullPath > RemainingFiles
=> _remainingFiles;
public IReadOnlyList< IModGroup > Options
=> _options;
public bool HasOptions { get; private set; } = false;
private void SetHasOptions()
{
HasOptions = _options.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 ) );
public IEnumerable< MetaManipulation > AllManipulations
=> _options.SelectMany( o => o ).SelectMany( o => o.Manipulations );
private void ReloadFiles()
{
// _remainingFiles.Clear();
// _options.Clear();
// HasOptions = false;
// if( !Directory.Exists( BasePath.FullName ) )
// return;
}
}

View file

@ -0,0 +1,77 @@
using System;
using System.IO;
using Dalamud.Logging;
namespace Penumbra.Mods;
public partial class Mod2
{
public partial class Manager
{
public delegate void ModPathChangeDelegate( ModPathChangeType type, Mod2 mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory );
public event ModPathChangeDelegate? ModPathChanged;
public void MoveModDirectory( Index idx, DirectoryInfo newDirectory )
{
var mod = this[ idx ];
// TODO
}
public void DeleteMod( Index idx )
{
var mod = this[ idx ];
if( Directory.Exists( mod.BasePath.FullName ) )
{
try
{
Directory.Delete( mod.BasePath.FullName, true );
}
catch( Exception e )
{
PluginLog.Error( $"Could not delete the mod {mod.BasePath.Name}:\n{e}" );
}
}
// TODO
// mod.Order.ParentFolder.RemoveMod( mod );
// _mods.RemoveAt( idx );
//for( var i = idx; i < _mods.Count; ++i )
//{
// --_mods[i].Index;
//}
ModPathChanged?.Invoke( ModPathChangeType.Deleted, mod, mod.BasePath, null );
}
public Mod2 AddMod( DirectoryInfo modFolder )
{
// TODO
//var mod = LoadMod( StructuredMods, modFolder );
//if( mod == null )
//{
// return -1;
//}
//
//if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) )
//{
// if( SetSortOrderPath( mod, sortOrder ) )
// {
// Config.Save();
// }
//}
//
//if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) )
//{
// return -1;
//}
//
//_mods.Add( mod );
//ModChange?.Invoke( ChangeType.Added, _mods.Count - 1, mod );
//
return this[^1];
}
}
}

View file

@ -0,0 +1,67 @@
using System;
namespace Penumbra.Mods;
public sealed partial class Mod2
{
public partial class Manager
{
public delegate void ModMetaChangeDelegate( MetaChangeType type, Mod2 mod );
public event ModMetaChangeDelegate? ModMetaChanged;
public void ChangeModName( Index idx, string newName )
{
var mod = this[ idx ];
if( mod.Name != newName )
{
mod.Name = newName;
mod.SaveMeta();
ModMetaChanged?.Invoke( MetaChangeType.Name, mod );
}
}
public void ChangeModAuthor( Index idx, string newAuthor )
{
var mod = this[ idx ];
if( mod.Author != newAuthor )
{
mod.Author = newAuthor;
mod.SaveMeta();
ModMetaChanged?.Invoke( MetaChangeType.Author, mod );
}
}
public void ChangeModDescription( Index idx, string newDescription )
{
var mod = this[ idx ];
if( mod.Description != newDescription )
{
mod.Description = newDescription;
mod.SaveMeta();
ModMetaChanged?.Invoke( MetaChangeType.Description, mod );
}
}
public void ChangeModVersion( Index idx, string newVersion )
{
var mod = this[ idx ];
if( mod.Version != newVersion )
{
mod.Version = newVersion;
mod.SaveMeta();
ModMetaChanged?.Invoke( MetaChangeType.Version, mod );
}
}
public void ChangeModWebsite( Index idx, string newWebsite )
{
var mod = this[ idx ];
if( mod.Website != newWebsite )
{
mod.Website = newWebsite;
mod.SaveMeta();
ModMetaChanged?.Invoke( MetaChangeType.Website, mod );
}
}
}
}

View file

@ -0,0 +1,301 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.Meta.Manipulations;
using Penumbra.Util;
namespace Penumbra.Mods;
public enum ModOptionChangeType
{
GroupRenamed,
GroupAdded,
GroupDeleted,
PriorityChanged,
OptionAdded,
OptionDeleted,
OptionChanged,
DisplayChange,
}
public sealed partial class Mod2
{
public sealed partial class Manager
{
public delegate void ModOptionChangeDelegate( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx );
public event ModOptionChangeDelegate ModOptionChanged;
public void RenameModGroup( Mod2 mod, int groupIdx, string newName )
{
var group = mod._options[ groupIdx ];
var oldName = group.Name;
if( oldName == newName || !VerifyFileName( mod, group, newName ) )
{
return;
}
var _ = group switch
{
SingleModGroup s => s.Name = newName,
MultiModGroup m => m.Name = newName,
_ => newName,
};
ModOptionChanged.Invoke( ModOptionChangeType.GroupRenamed, mod, groupIdx, 0 );
}
public void AddModGroup( Mod2 mod, SelectType type, string newName )
{
if( !VerifyFileName( mod, null, newName ) )
{
return;
}
var maxPriority = mod._options.Max( o => o.Priority ) + 1;
mod._options.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 );
}
public void DeleteModGroup( Mod2 mod, int groupIdx )
{
var group = mod._options[ groupIdx ];
mod._options.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 ];
if( group.Description == newDescription )
{
return;
}
var _ = group switch
{
SingleModGroup s => s.Description = newDescription,
MultiModGroup m => m.Description = newDescription,
_ => newDescription,
};
ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, 0 );
}
public void ChangeGroupPriority( Mod2 mod, int groupIdx, int newPriority )
{
var group = mod._options[ groupIdx ];
if( group.Priority == newPriority )
{
return;
}
var _ = group switch
{
SingleModGroup s => s.Priority = newPriority,
MultiModGroup m => m.Priority = newPriority,
_ => newPriority,
};
ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, -1 );
}
public void ChangeOptionPriority( Mod2 mod, int groupIdx, int optionIdx, int newPriority )
{
switch( mod._options[ groupIdx ] )
{
case SingleModGroup s:
ChangeGroupPriority( mod, groupIdx, newPriority );
break;
case MultiModGroup m:
if( m.PrioritizedOptions[ optionIdx ].Priority == newPriority )
{
return;
}
m.PrioritizedOptions[ optionIdx ] = ( m.PrioritizedOptions[ optionIdx ].Mod, newPriority );
ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx );
return;
}
}
public void RenameOption( Mod2 mod, int groupIdx, int optionIdx, string newName )
{
switch( mod._options[ groupIdx ] )
{
case SingleModGroup s:
if( s.OptionData[ optionIdx ].Name == newName )
{
return;
}
s.OptionData[ optionIdx ].Name = newName;
break;
case MultiModGroup m:
var option = m.PrioritizedOptions[ optionIdx ].Mod;
if( option.Name == newName )
{
return;
}
option.Name = newName;
return;
}
ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx );
}
public void AddOption( Mod2 mod, int groupIdx, string newName )
{
switch( mod._options[ groupIdx ] )
{
case SingleModGroup s:
s.OptionData.Add( new SubMod { Name = newName } );
break;
case MultiModGroup m:
m.PrioritizedOptions.Add( ( new SubMod { Name = newName }, 0 ) );
break;
}
ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._options[ groupIdx ].Count - 1 );
}
public void DeleteOption( Mod2 mod, int groupIdx, int optionIdx )
{
switch( mod._options[ groupIdx ] )
{
case SingleModGroup s:
s.OptionData.RemoveAt( optionIdx );
break;
case MultiModGroup m:
m.PrioritizedOptions.RemoveAt( optionIdx );
break;
}
ModOptionChanged.Invoke( ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx );
}
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 )
{
return;
}
subMod.ManipulationData.RemoveAt( idx );
}
else
{
if( idx >= 0 )
{
if( manip.EntryEquals( subMod.ManipulationData[ idx ] ) )
{
return;
}
subMod.ManipulationData[ idx ] = manip;
}
else
{
subMod.ManipulationData.Add( manip );
}
}
ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx );
}
public void OptionSetFile( Mod2 mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath )
{
var subMod = GetSubMod( mod, groupIdx, optionIdx );
if( OptionSetFile( subMod.FileData, gamePath, newPath ) )
{
ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx );
}
}
public void OptionSetFileSwap( Mod2 mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath )
{
var subMod = GetSubMod( mod, groupIdx, optionIdx );
if( OptionSetFile( subMod.FileSwapData, gamePath, newPath ) )
{
ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx );
}
}
private bool VerifyFileName( Mod2 mod, IModGroup? group, string newName )
{
var path = newName.RemoveInvalidPathSymbols();
if( mod.Options.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." );
return false;
}
return true;
}
private static SubMod GetSubMod( Mod2 mod, int groupIdx, int optionIdx )
{
return mod._options[ groupIdx ] switch
{
SingleModGroup s => s.OptionData[ optionIdx ],
MultiModGroup m => m.PrioritizedOptions[ optionIdx ].Mod,
_ => throw new InvalidOperationException(),
};
}
private static bool OptionSetFile( IDictionary< Utf8GamePath, FullPath > dict, Utf8GamePath gamePath, FullPath? newPath )
{
if( dict.TryGetValue( gamePath, out var oldPath ) )
{
if( newPath == null )
{
dict.Remove( gamePath );
return true;
}
if( newPath.Value.Equals( oldPath ) )
{
return false;
}
dict[ gamePath ] = newPath.Value;
return true;
}
if( newPath == null )
{
return false;
}
dict.Add( gamePath, newPath.Value );
return true;
}
private static void OnModOptionChange( ModOptionChangeType type, Mod2 mod, int groupIdx, int _ )
{
// File deletion is handled in the actual function.
if( type != ModOptionChangeType.GroupDeleted )
{
mod._options[groupIdx].Save( 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 ),
_ => mod.HasOptions,
};
}
}
}

View file

@ -0,0 +1,91 @@
using System;
using System.IO;
using Dalamud.Logging;
namespace Penumbra.Mods;
public sealed partial class Mod2
{
public sealed partial class Manager
{
public DirectoryInfo BasePath { get; private set; } = null!;
public bool Valid { get; private set; }
public event Action? ModDiscoveryStarted;
public event Action? ModDiscoveryFinished;
public void DiscoverMods( string newDir )
{
SetBaseDirectory( newDir, false );
DiscoverMods();
}
private void SetBaseDirectory( string newPath, bool firstTime )
{
if( !firstTime && string.Equals( newPath, Penumbra.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( Penumbra.Config.ModDirectory != BasePath.FullName )
{
Penumbra.Config.ModDirectory = BasePath.FullName;
Penumbra.Config.Save();
}
}
}
public void DiscoverMods()
{
ModDiscoveryStarted?.Invoke();
_mods.Clear();
BasePath.Refresh();
// TODO
//StructuredMods.SubFolders.Clear();
//StructuredMods.Mods.Clear();
if( Valid && BasePath.Exists )
{
foreach( var modFolder in BasePath.EnumerateDirectories() )
{
//var mod = LoadMod( StructuredMods, modFolder );
//if( mod == null )
//{
// continue;
//}
//
//mod.Index = _mods.Count;
//_mods.Add( mod );
}
//SetModStructure();
}
ModDiscoveryFinished?.Invoke();
}
}
}

View file

@ -0,0 +1,35 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Penumbra.Mods;
public sealed partial class Mod2
{
public sealed partial class Manager : IEnumerable< Mod2 >
{
private readonly List< Mod2 > _mods = new();
public Mod2 this[ Index idx ]
=> _mods[ idx ];
public IReadOnlyList< Mod2 > Mods
=> _mods;
public int Count
=> _mods.Count;
public IEnumerator< Mod2 > GetEnumerator()
=> _mods.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public Manager( string modDirectory )
{
SetBaseDirectory( modDirectory, true );
ModOptionChanged += OnModOptionChange;
}
}
}

View file

@ -0,0 +1,62 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.ByteString;
using Penumbra.Util;
namespace Penumbra.Mods;
public sealed partial class Mod2
{
private static class Migration
{
public static void Migrate( Mod2 mod, string text )
{
MigrateV0ToV1( mod, text );
}
private static void MigrateV0ToV1( Mod2 mod, string text )
{
if( mod.FileVersion > 0 )
{
return;
}
var data = JObject.Parse( text );
var swaps = data[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >()
?? new Dictionary< Utf8GamePath, FullPath >();
var groups = data[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >();
foreach( var group in groups.Values )
{ }
foreach( var swap in swaps )
{ }
}
private struct OptionV0
{
public string OptionName = string.Empty;
public string OptionDesc = string.Empty;
[JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )]
public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles = new();
public OptionV0()
{ }
}
private struct OptionGroupV0
{
public string GroupName = string.Empty;
[JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )]
public SelectType SelectionType = SelectType.Single;
public List< OptionV0 > Options = new();
public OptionGroupV0()
{ }
}
}
}

121
Penumbra/Mods/Mod2.Meta.cs Normal file
View file

@ -0,0 +1,121 @@
using System;
using System.IO;
using Dalamud.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.Util;
namespace Penumbra.Mods;
[Flags]
public enum MetaChangeType : byte
{
None = 0x00,
Name = 0x01,
Author = 0x02,
Description = 0x04,
Version = 0x08,
Website = 0x10,
Deletion = 0x20,
}
public sealed partial class Mod2
{
public const uint CurrentFileVersion = 1;
public uint FileVersion { get; private set; } = CurrentFileVersion;
public LowerString Name { get; private set; } = "Mod";
public LowerString Author { get; private set; } = LowerString.Empty;
public string Description { get; private set; } = string.Empty;
public string Version { get; private set; } = string.Empty;
public string Website { get; private set; } = string.Empty;
private void SaveMeta()
=> SaveToFile( MetaFile );
private MetaChangeType LoadMetaFromFile( FileInfo filePath )
{
if( !File.Exists( filePath.FullName ) )
{
return MetaChangeType.Deletion;
}
try
{
var text = File.ReadAllText( filePath.FullName );
var json = JObject.Parse( text );
var newName = json[ nameof( Name ) ]?.Value< string >() ?? string.Empty;
var newAuthor = json[ nameof( Author ) ]?.Value< string >() ?? string.Empty;
var newDescription = json[ nameof( Description ) ]?.Value< string >() ?? string.Empty;
var newVersion = json[ nameof( Version ) ]?.Value< string >() ?? string.Empty;
var newWebsite = json[ nameof( Website ) ]?.Value< string >() ?? string.Empty;
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;
Name = newName;
}
if( Author != newAuthor )
{
changes |= MetaChangeType.Author;
Author = newAuthor;
}
if( Description != newDescription )
{
changes |= MetaChangeType.Description;
Description = newDescription;
}
if( Version != newVersion )
{
changes |= MetaChangeType.Version;
Version = newVersion;
}
if( Website != newWebsite )
{
changes |= MetaChangeType.Website;
Website = newWebsite;
}
return changes;
}
catch( Exception e )
{
PluginLog.Error( $"Could not load mod meta:\n{e}" );
return MetaChangeType.Deletion;
}
}
private void SaveToFile( FileInfo filePath )
{
try
{
var jObject = new JObject
{
{ nameof( FileVersion ), JToken.FromObject( FileVersion ) },
{ nameof( Name ), JToken.FromObject( Name ) },
{ nameof( Author ), JToken.FromObject( Author ) },
{ nameof( Description ), JToken.FromObject( Description ) },
{ nameof( Version ), JToken.FromObject( Version ) },
{ nameof( Website ), JToken.FromObject( Website ) },
};
File.WriteAllText( filePath.FullName, jObject.ToString( Formatting.Indented ) );
}
catch( Exception e )
{
PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" );
}
}
}

View file

@ -0,0 +1,8 @@
namespace Penumbra.Mods;
public sealed partial class Mod2
{
public Mod.SortOrder Order;
public override string ToString()
=> Order.FullPath;
}

View file

@ -1,109 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Logging;
namespace Penumbra.Mods;
public partial class ModManagerNew
{
private readonly List< Mod > _mods = new();
public IReadOnlyList< Mod > Mods
=> _mods;
public void DiscoverMods()
{
//_mods.Clear();
//
//if( CheckValidity() )
//{
// foreach( var modFolder in BasePath.EnumerateDirectories() )
// {
// var mod = ModData.LoadMod( StructuredMods, modFolder );
// if( mod == null )
// {
// continue;
// }
//
// Mods.Add( modFolder.Name, mod );
// }
//
// SetModStructure();
//}
//
//Collections.RecreateCaches();
}
}
public partial class ModManagerNew
{
public DirectoryInfo BasePath { get; private set; } = null!;
public bool Valid { get; private set; }
public event Action< DirectoryInfo >? BasePathChanged;
public ModManagerNew()
{
InitBaseDirectory( Penumbra.Config.ModDirectory );
}
public bool CheckValidity()
{
if( Valid )
{
Valid = Directory.Exists( BasePath.FullName );
}
return Valid;
}
private static (DirectoryInfo, bool) CreateDirectory( string path )
{
var newDir = new DirectoryInfo( path );
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}" );
return ( newDir, false );
}
}
return ( newDir, true );
}
private void InitBaseDirectory( string path )
{
if( path.Length == 0 )
{
Valid = false;
BasePath = new DirectoryInfo( "." );
return;
}
( BasePath, Valid ) = CreateDirectory( path );
if( Penumbra.Config.ModDirectory != BasePath.FullName )
{
Penumbra.Config.ModDirectory = BasePath.FullName;
Penumbra.Config.Save();
}
}
private void ChangeBaseDirectory( string path )
{
if( string.Equals( path, Penumbra.Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) )
{
return;
}
InitBaseDirectory( path );
BasePathChanged?.Invoke( BasePath );
}
}

View file

@ -45,7 +45,8 @@ public partial class Mod
public delegate void ModChangeDelegate( ChangeType type, int modIndex, Mod mod );
public event ModChangeDelegate? ModChange;
public event Action? ModsRediscovered;
public event Action? ModDiscoveryStarted;
public event Action? ModDiscoveryFinished;
public bool Valid { get; private set; }
@ -97,8 +98,6 @@ public partial class Mod
Config.Save();
}
}
ModsRediscovered?.Invoke();
}
public Manager()
@ -150,6 +149,7 @@ public partial class Mod
public void DiscoverMods()
{
ModDiscoveryStarted?.Invoke();
_mods.Clear();
BasePath.Refresh();
@ -172,7 +172,7 @@ public partial class Mod
SetModStructure();
}
ModsRediscovered?.Invoke();
ModDiscoveryFinished?.Invoke();
}
public void DeleteMod( DirectoryInfo modFolder )
@ -235,7 +235,7 @@ public partial class Mod
{
var mod = Mods[ idx ];
var oldName = mod.Meta.Name;
var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) || force;
var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) != 0 || force;
var fileChanges = mod.Resources.RefreshModFiles( mod.BasePath );
if( !recomputeMeta && !reloadMeta && !metaChanges && fileChanges == 0 )

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using Dalamud.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.ByteString;
using Penumbra.Util;
@ -12,51 +13,83 @@ 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 const uint CurrentFileVersion = 1;
[Flags]
public enum ChangeType : byte
{
Name = 0x01,
Author = 0x02,
Description = 0x04,
Version = 0x08,
Website = 0x10,
Deletion = 0x20,
}
public uint FileVersion { get; set; } = CurrentFileVersion;
public LowerString Name { get; set; } = "Mod";
public LowerString Author { get; set; } = LowerString.Empty;
public string Description { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public string Website { get; set; } = string.Empty;
[JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )]
public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new();
public bool HasGroupsWithConfig = false;
public Dictionary< string, OptionGroup > Groups { get; set; } = new();
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;
}
[JsonIgnore]
private int FileHash { get; set; }
[JsonIgnore]
public bool HasGroupsWithConfig { get; private set; }
public bool RefreshFromFile( FileInfo filePath )
public ChangeType RefreshFromFile( FileInfo filePath )
{
var newMeta = LoadFromFile( filePath );
if( newMeta == null )
{
return true;
return ChangeType.Deletion;
}
if( newMeta.FileHash == FileHash )
ChangeType changes = 0;
if( Name != newMeta.Name )
{
return false;
changes |= ChangeType.Name;
Name = newMeta.Name;
}
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;
if( Author != newMeta.Author )
{
changes |= ChangeType.Author;
Author = newMeta.Author;
}
if( Description != newMeta.Description )
{
changes |= ChangeType.Description;
Description = newMeta.Description;
}
if( Version != newMeta.Version )
{
changes |= ChangeType.Version;
Version = newMeta.Version;
}
if( Website != newMeta.Website )
{
changes |= ChangeType.Website;
Website = newMeta.Website;
}
return changes;
}
[JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )]
public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new();
public Dictionary< string, OptionGroup > Groups { get; set; } = new();
public static ModMeta? LoadFromFile( FileInfo filePath )
{
try
@ -67,8 +100,8 @@ public class ModMeta
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } );
if( meta != null )
{
meta.FileHash = text.GetHashCode();
meta.RefreshHasGroupsWithConfig();
Migration.Migrate( meta, text );
}
return meta;
@ -80,29 +113,71 @@ public class ModMeta
}
}
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;
}
var text = JsonConvert.SerializeObject( this, Formatting.Indented );
File.WriteAllText( filePath.FullName, text );
}
catch( Exception e )
{
PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" );
}
}
private static class Migration
{
public static void Migrate( ModMeta meta, string text )
{
MigrateV0ToV1( meta, text );
}
private static void MigrateV0ToV1( ModMeta meta, string text )
{
if( meta.FileVersion > 0 )
{
return;
}
var data = JObject.Parse( text );
var swaps = data[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >()
?? new Dictionary< Utf8GamePath, FullPath >();
var groups = data[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >();
foreach( var group in groups.Values )
{ }
foreach( var swap in swaps )
{ }
//var meta =
}
private struct OptionV0
{
public string OptionName = string.Empty;
public string OptionDesc = string.Empty;
[JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )]
public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles = new();
public OptionV0()
{ }
}
private struct OptionGroupV0
{
public string GroupName = string.Empty;
[JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )]
public SelectType SelectionType = SelectType.Single;
public List< OptionV0 > Options = new();
public OptionGroupV0()
{ }
}
}
}