mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
ModManager2
This commit is contained in:
parent
1541cdb78d
commit
70c1a2604f
11 changed files with 815 additions and 117 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -7,7 +8,9 @@ namespace Penumbra.Mods;
|
|||
|
||||
/// <summary> Utility to create and apply a zipped backup of a mod. </summary>
|
||||
public class ModBackup
|
||||
{
|
||||
{
|
||||
/// <summary> Set when reading Config and migrating from v4 to v5. </summary>
|
||||
public static bool MigrateModBackups = false;
|
||||
public static bool CreatingBackup { get; private set; }
|
||||
|
||||
private readonly Mod _mod;
|
||||
|
|
@ -22,9 +25,9 @@ public class ModBackup
|
|||
}
|
||||
|
||||
/// <summary> Migrate file extensions. </summary>
|
||||
public static void MigrateZipToPmp(ModManager modManager)
|
||||
public static void MigrateZipToPmp(IEnumerable<Mod> modStorage)
|
||||
{
|
||||
foreach (var mod in modManager)
|
||||
foreach (var mod in modStorage)
|
||||
{
|
||||
var pmpName = mod.ModPath + ".pmp";
|
||||
var zipName = mod.ModPath + ".zip";
|
||||
|
|
|
|||
|
|
@ -1,74 +1,325 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Util;
|
||||
|
||||
using Penumbra.Util;
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public sealed class ModManager2 : IReadOnlyList<Mod>, IDisposable
|
||||
|
||||
/// <summary> Describes the state of a potential move-target for a mod. </summary>
|
||||
public enum NewDirectoryState
|
||||
{
|
||||
NonExisting,
|
||||
ExistsEmpty,
|
||||
ExistsNonEmpty,
|
||||
ExistsAsFile,
|
||||
ContainsInvalidSymbols,
|
||||
Identical,
|
||||
Empty,
|
||||
}
|
||||
|
||||
public sealed class ModManager2 : ModStorage
|
||||
{
|
||||
private readonly Configuration _config;
|
||||
private readonly CommunicatorService _communicator;
|
||||
|
||||
public readonly ModDataEditor DataEditor;
|
||||
public readonly ModOptionEditor OptionEditor;
|
||||
|
||||
/// <summary>
|
||||
/// An easily accessible set of new mods.
|
||||
/// Mods are added when they are created or imported.
|
||||
/// Mods are removed when they are deleted or when they are toggled in any collection.
|
||||
/// Also gets cleared on mod rediscovery.
|
||||
/// </summary>
|
||||
public readonly HashSet<Mod> NewMods = new();
|
||||
public DirectoryInfo BasePath { get; private set; } = null!;
|
||||
public bool Valid { get; private set; }
|
||||
|
||||
public Mod this[int idx]
|
||||
=> _mods[idx];
|
||||
|
||||
public Mod this[Index idx]
|
||||
=> _mods[idx];
|
||||
|
||||
public int Count
|
||||
=> _mods.Count;
|
||||
|
||||
public IEnumerator<Mod> GetEnumerator()
|
||||
=> _mods.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Try to obtain a mod by its directory name (unique identifier, preferred),
|
||||
/// or the first mod of the given name if no directory fits.
|
||||
/// </summary>
|
||||
public bool TryGetMod(string identifier, string modName, [NotNullWhen(true)] out Mod? mod)
|
||||
public ModManager2(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor)
|
||||
{
|
||||
mod = null;
|
||||
foreach (var m in _mods)
|
||||
{
|
||||
if (string.Equals(m.Identifier, identifier, StringComparison.OrdinalIgnoreCase))
|
||||
_config = config;
|
||||
_communicator = communicator;
|
||||
DataEditor = dataEditor;
|
||||
OptionEditor = optionEditor;
|
||||
}
|
||||
|
||||
/// <summary> Change the mod base directory and discover available mods. </summary>
|
||||
public void DiscoverMods(string newDir)
|
||||
{
|
||||
SetBaseDirectory(newDir, false);
|
||||
DiscoverMods();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discover mods without changing the root directory.
|
||||
/// </summary>
|
||||
public void DiscoverMods()
|
||||
{
|
||||
_communicator.ModDiscoveryStarted.Invoke();
|
||||
NewMods.Clear();
|
||||
Mods.Clear();
|
||||
BasePath.Refresh();
|
||||
|
||||
if (Valid && BasePath.Exists)
|
||||
ScanMods();
|
||||
|
||||
_communicator.ModDiscoveryFinished.Invoke();
|
||||
Penumbra.Log.Information("Rediscovered mods.");
|
||||
|
||||
if (ModBackup.MigrateModBackups)
|
||||
ModBackup.MigrateZipToPmp(this);
|
||||
}
|
||||
|
||||
/// <summary> Load a new mod and add it to the manager if successful. </summary>
|
||||
public void AddMod(DirectoryInfo modFolder)
|
||||
{
|
||||
if (this.Any(m => m.ModPath.Name == modFolder.Name))
|
||||
return;
|
||||
|
||||
Mod.Creator.SplitMultiGroups(modFolder);
|
||||
var mod = Mod.LoadMod(Penumbra.ModManager, modFolder, true);
|
||||
if (mod == null)
|
||||
return;
|
||||
|
||||
mod.Index = Count;
|
||||
Mods.Add(mod);
|
||||
_communicator.ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath);
|
||||
Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a mod. The event is invoked before the mod is removed from the list.
|
||||
/// Deletes from filesystem as well as from internal data.
|
||||
/// Updates indices of later mods.
|
||||
/// </summary>
|
||||
public void DeleteMod(Mod mod)
|
||||
{
|
||||
if (Directory.Exists(mod.ModPath.FullName))
|
||||
try
|
||||
{
|
||||
mod = m;
|
||||
return true;
|
||||
Directory.Delete(mod.ModPath.FullName, true);
|
||||
Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}");
|
||||
}
|
||||
|
||||
if (m.Name == modName)
|
||||
mod ??= m;
|
||||
_communicator.ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null);
|
||||
Mods.RemoveAt(mod.Index);
|
||||
foreach (var remainingMod in this.Skip(mod.Index))
|
||||
--remainingMod.Index;
|
||||
|
||||
Penumbra.Log.Debug($"Deleted mod {mod.Name}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reload a mod without changing its base directory.
|
||||
/// If the base directory does not exist anymore, the mod will be deleted.
|
||||
/// </summary>
|
||||
public void ReloadMod(Mod mod)
|
||||
{
|
||||
var oldName = mod.Name;
|
||||
|
||||
_communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
|
||||
if (!mod.Reload(Penumbra.ModManager, true, out var metaChange))
|
||||
{
|
||||
Penumbra.Log.Warning(mod.Name.Length == 0
|
||||
? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead."
|
||||
: $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead.");
|
||||
|
||||
DeleteMod(mod);
|
||||
return;
|
||||
}
|
||||
|
||||
return mod != null;
|
||||
}
|
||||
|
||||
/// <summary> The actual list of mods. </summary>
|
||||
private readonly List<Mod> _mods = new();
|
||||
|
||||
public ModManager2(ModDataEditor dataEditor, ModOptionEditor optionEditor)
|
||||
_communicator.ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath);
|
||||
if (metaChange != ModDataChangeType.None)
|
||||
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Rename/Move a mod directory.
|
||||
/// Updates all collection settings and sort order settings.
|
||||
/// </summary>
|
||||
public void MoveModDirectory(Mod mod, string newName)
|
||||
{
|
||||
DataEditor = dataEditor;
|
||||
OptionEditor = optionEditor;
|
||||
var oldName = mod.Name;
|
||||
var oldDirectory = mod.ModPath;
|
||||
|
||||
switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir))
|
||||
{
|
||||
case NewDirectoryState.NonExisting:
|
||||
// Nothing to do
|
||||
break;
|
||||
case NewDirectoryState.ExistsEmpty:
|
||||
try
|
||||
{
|
||||
Directory.Delete(dir!.FullName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}");
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
// Should be caught beforehand.
|
||||
case NewDirectoryState.ExistsNonEmpty:
|
||||
case NewDirectoryState.ExistsAsFile:
|
||||
case NewDirectoryState.ContainsInvalidSymbols:
|
||||
// Nothing to do at all.
|
||||
case NewDirectoryState.Identical:
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Move(oldDirectory.FullName, dir!.FullName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}");
|
||||
return;
|
||||
}
|
||||
|
||||
DataEditor.MoveDataFile(oldDirectory, dir);
|
||||
|
||||
dir.Refresh();
|
||||
mod.ModPath = dir;
|
||||
if (!mod.Reload(Penumbra.ModManager, false, out var metaChange))
|
||||
{
|
||||
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
|
||||
return;
|
||||
}
|
||||
|
||||
_communicator.ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir);
|
||||
if (metaChange != ModDataChangeType.None)
|
||||
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
|
||||
}
|
||||
|
||||
/// <summary> Return the state of the new potential name of a directory. </summary>
|
||||
public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory)
|
||||
{
|
||||
directory = null;
|
||||
if (newName.Length == 0)
|
||||
return NewDirectoryState.Empty;
|
||||
|
||||
if (oldName == newName)
|
||||
return NewDirectoryState.Identical;
|
||||
|
||||
var fixedNewName = Mod.Creator.ReplaceBadXivSymbols(newName);
|
||||
if (fixedNewName != newName)
|
||||
return NewDirectoryState.ContainsInvalidSymbols;
|
||||
|
||||
directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName));
|
||||
if (File.Exists(directory.FullName))
|
||||
return NewDirectoryState.ExistsAsFile;
|
||||
|
||||
if (!Directory.Exists(directory.FullName))
|
||||
return NewDirectoryState.NonExisting;
|
||||
|
||||
if (directory.EnumerateFileSystemInfos().Any())
|
||||
return NewDirectoryState.ExistsNonEmpty;
|
||||
|
||||
return NewDirectoryState.ExistsEmpty;
|
||||
}
|
||||
|
||||
|
||||
/// <summary> Add new mods to NewMods and remove deleted mods from NewMods. </summary>
|
||||
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
||||
DirectoryInfo? newDirectory)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ModPathChangeType.Added:
|
||||
NewMods.Add(mod);
|
||||
break;
|
||||
case ModPathChangeType.Deleted:
|
||||
NewMods.Remove(mod);
|
||||
break;
|
||||
case ModPathChangeType.Moved:
|
||||
if (oldDirectory != null && newDirectory != null)
|
||||
DataEditor.MoveDataFile(oldDirectory, newDirectory);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Set the mod base directory.
|
||||
/// If its not the first time, check if it is the same directory as before.
|
||||
/// Also checks if the directory is available and tries to create it if it is not.
|
||||
/// </summary>
|
||||
private void SetBaseDirectory(string newPath, bool firstTime)
|
||||
{
|
||||
if (!firstTime && string.Equals(newPath, _config.ModDirectory, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
if (newPath.Length == 0)
|
||||
{
|
||||
Valid = false;
|
||||
BasePath = new DirectoryInfo(".");
|
||||
if (_config.ModDirectory != BasePath.FullName)
|
||||
TriggerModDirectoryChange(string.Empty, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newDir = new DirectoryInfo(newPath);
|
||||
if (!newDir.Exists)
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(newDir.FullName);
|
||||
newDir.Refresh();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not create specified mod directory {newDir.FullName}:\n{e}");
|
||||
}
|
||||
|
||||
BasePath = newDir;
|
||||
Valid = Directory.Exists(newDir.FullName);
|
||||
if (_config.ModDirectory != BasePath.FullName)
|
||||
TriggerModDirectoryChange(BasePath.FullName, Valid);
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerModDirectoryChange(string newPath, bool valid)
|
||||
{
|
||||
_config.ModDirectory = newPath;
|
||||
_config.Save();
|
||||
Penumbra.Log.Information($"Set new mod base directory from {_config.ModDirectory} to {newPath}.");
|
||||
_communicator.ModDirectoryChanged.Invoke(newPath, valid);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Iterate through available mods with multiple threads and queue their loads,
|
||||
/// then add the mods from the queue.
|
||||
/// </summary>
|
||||
private void ScanMods()
|
||||
{
|
||||
var options = new ParallelOptions()
|
||||
{
|
||||
MaxDegreeOfParallelism = Environment.ProcessorCount / 2,
|
||||
};
|
||||
var queue = new ConcurrentQueue<Mod>();
|
||||
Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir =>
|
||||
{
|
||||
var mod = Mod.LoadMod(Penumbra.ModManager, dir, false);
|
||||
if (mod != null)
|
||||
queue.Enqueue(mod);
|
||||
});
|
||||
|
||||
foreach (var mod in queue)
|
||||
{
|
||||
mod.Index = Count;
|
||||
Mods.Add(mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class ModManager : IReadOnlyList<Mod>, IDisposable
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ public class ModOptionEditor
|
|||
|
||||
mod._groups[groupIdx] = group.Convert(type);
|
||||
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
|
||||
mod.HasOptions = mod.Groups.Any(o => o.IsOption);
|
||||
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1);
|
||||
}
|
||||
|
||||
|
|
|
|||
66
Penumbra/Mods/Manager/ModStorage.cs
Normal file
66
Penumbra/Mods/Manager/ModStorage.cs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public class ModStorage : IReadOnlyList<Mod>
|
||||
{
|
||||
/// <summary> The actual list of mods. </summary>
|
||||
protected readonly List<Mod> Mods = new();
|
||||
|
||||
public int Count
|
||||
=> Mods.Count;
|
||||
|
||||
public Mod this[int idx]
|
||||
=> Mods[idx];
|
||||
|
||||
public Mod this[Index idx]
|
||||
=> Mods[idx];
|
||||
|
||||
public IEnumerator<Mod> GetEnumerator()
|
||||
=> Mods.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Try to obtain a mod by its directory name (unique identifier, preferred),
|
||||
/// or the first mod of the given name if no directory fits.
|
||||
/// </summary>
|
||||
public bool TryGetMod(string identifier, string modName, [NotNullWhen(true)] out Mod? mod)
|
||||
{
|
||||
mod = null;
|
||||
foreach (var m in Mods)
|
||||
{
|
||||
if (string.Equals(m.Identifier, identifier, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mod = m;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m.Name == modName)
|
||||
mod ??= m;
|
||||
}
|
||||
|
||||
return mod != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An easily accessible set of new mods.
|
||||
/// Mods are added when they are created or imported.
|
||||
/// Mods are removed when they are deleted or when they are toggled in any collection.
|
||||
/// Also gets cleared on mod rediscovery.
|
||||
/// </summary>
|
||||
protected readonly HashSet<Mod> NewMods = new();
|
||||
|
||||
public bool IsNew(Mod mod)
|
||||
=> NewMods.Contains(mod);
|
||||
|
||||
public void SetNew(Mod mod)
|
||||
=> NewMods.Add(mod);
|
||||
|
||||
public void SetKnown(Mod mod)
|
||||
=> NewMods.Remove(mod);
|
||||
}
|
||||
|
|
@ -14,80 +14,78 @@ public partial class Mod
|
|||
public ISubMod Default
|
||||
=> _default;
|
||||
|
||||
public IReadOnlyList< IModGroup > Groups
|
||||
public IReadOnlyList<IModGroup> Groups
|
||||
=> _groups;
|
||||
|
||||
internal readonly SubMod _default;
|
||||
internal readonly List< IModGroup > _groups = new();
|
||||
internal readonly SubMod _default;
|
||||
internal readonly List<IModGroup> _groups = new();
|
||||
|
||||
public int TotalFileCount { get; internal set; }
|
||||
public int TotalSwapCount { get; internal set; }
|
||||
public int TotalManipulations { get; internal set; }
|
||||
public bool HasOptions { get; internal set; }
|
||||
public int TotalFileCount { get; internal set; }
|
||||
public int TotalSwapCount { get; internal set; }
|
||||
public int TotalManipulations { get; internal set; }
|
||||
public bool HasOptions { get; internal set; }
|
||||
|
||||
internal bool SetCounts()
|
||||
{
|
||||
TotalFileCount = 0;
|
||||
TotalSwapCount = 0;
|
||||
TotalManipulations = 0;
|
||||
foreach( var s in AllSubMods )
|
||||
foreach (var s in AllSubMods)
|
||||
{
|
||||
TotalFileCount += s.Files.Count;
|
||||
TotalSwapCount += s.FileSwaps.Count;
|
||||
TotalManipulations += s.Manipulations.Count;
|
||||
}
|
||||
|
||||
HasOptions = _groups.Any( o
|
||||
HasOptions = _groups.Any(o
|
||||
=> o is MultiModGroup m && m.PrioritizedOptions.Count > 0
|
||||
|| o is SingleModGroup s && s.OptionData.Count > 1 );
|
||||
|| o is SingleModGroup s && s.OptionData.Count > 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEnumerable< ISubMod > AllSubMods
|
||||
=> _groups.SelectMany( o => o ).Prepend( _default );
|
||||
public IEnumerable<ISubMod> AllSubMods
|
||||
=> _groups.SelectMany(o => o).Prepend(_default);
|
||||
|
||||
public IEnumerable< MetaManipulation > AllManipulations
|
||||
=> AllSubMods.SelectMany( s => s.Manipulations );
|
||||
public IEnumerable<MetaManipulation> AllManipulations
|
||||
=> AllSubMods.SelectMany(s => s.Manipulations);
|
||||
|
||||
public IEnumerable< Utf8GamePath > AllRedirects
|
||||
=> AllSubMods.SelectMany( s => s.Files.Keys.Concat( s.FileSwaps.Keys ) );
|
||||
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<FullPath> AllFiles
|
||||
=> AllSubMods.SelectMany(o => o.Files)
|
||||
.Select(p => p.Value);
|
||||
|
||||
public IEnumerable< FileInfo > GroupFiles
|
||||
=> ModPath.EnumerateFiles( "group_*.json" );
|
||||
public IEnumerable<FileInfo> GroupFiles
|
||||
=> ModPath.EnumerateFiles("group_*.json");
|
||||
|
||||
public List< FullPath > FindUnusedFiles()
|
||||
public List<FullPath> FindUnusedFiles()
|
||||
{
|
||||
var modFiles = AllFiles.ToHashSet();
|
||||
return ModPath.EnumerateDirectories()
|
||||
.SelectMany( f => f.EnumerateFiles( "*", SearchOption.AllDirectories ) )
|
||||
.Select( f => new FullPath( f ) )
|
||||
.Where( f => !modFiles.Contains( f ) )
|
||||
.ToList();
|
||||
.SelectMany(f => f.EnumerateFiles("*", SearchOption.AllDirectories))
|
||||
.Select(f => new FullPath(f))
|
||||
.Where(f => !modFiles.Contains(f))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IModGroup? LoadModGroup( Mod mod, FileInfo file, int groupIdx )
|
||||
private static IModGroup? LoadModGroup(Mod mod, FileInfo file, int groupIdx)
|
||||
{
|
||||
if( !File.Exists( file.FullName ) )
|
||||
{
|
||||
if (!File.Exists(file.FullName))
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = JObject.Parse( File.ReadAllText( file.FullName ) );
|
||||
switch( json[ nameof( Type ) ]?.ToObject< GroupType >() ?? GroupType.Single )
|
||||
var json = JObject.Parse(File.ReadAllText(file.FullName));
|
||||
switch (json[nameof(Type)]?.ToObject<GroupType>() ?? GroupType.Single)
|
||||
{
|
||||
case GroupType.Multi: return MultiModGroup.Load( mod, json, groupIdx );
|
||||
case GroupType.Single: return SingleModGroup.Load( mod, json, groupIdx );
|
||||
case GroupType.Multi: return MultiModGroup.Load(mod, json, groupIdx);
|
||||
case GroupType.Single: return SingleModGroup.Load(mod, json, groupIdx);
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not read mod group from {file.FullName}:\n{e}" );
|
||||
Penumbra.Log.Error($"Could not read mod group from {file.FullName}:\n{e}");
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -97,13 +95,13 @@ public partial class Mod
|
|||
{
|
||||
_groups.Clear();
|
||||
var changes = false;
|
||||
foreach( var file in GroupFiles )
|
||||
foreach (var file in GroupFiles)
|
||||
{
|
||||
var group = LoadModGroup( this, file, _groups.Count );
|
||||
if( group != null && _groups.All( g => g.Name != group.Name ) )
|
||||
var group = LoadModGroup(this, file, _groups.Count);
|
||||
if (group != null && _groups.All(g => g.Name != group.Name))
|
||||
{
|
||||
changes = changes || Penumbra.Filenames.OptionGroupFile(ModPath.FullName, Groups.Count, group.Name) != file.FullName;
|
||||
_groups.Add( group );
|
||||
_groups.Add(group);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -111,7 +109,7 @@ public partial class Mod
|
|||
}
|
||||
}
|
||||
|
||||
if( changes )
|
||||
if (changes)
|
||||
Penumbra.SaveService.SaveAllOptionGroups(this);
|
||||
}
|
||||
|
||||
|
|
@ -122,13 +120,9 @@ public partial class Mod
|
|||
try
|
||||
{
|
||||
if (!File.Exists(defaultFile))
|
||||
{
|
||||
_default.Load(ModPath, new JObject(), out _);
|
||||
}
|
||||
else
|
||||
{
|
||||
_default.Load(ModPath, JObject.Parse(File.ReadAllText(defaultFile)), out _);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -145,17 +139,13 @@ public partial class Mod
|
|||
{
|
||||
var dir = Creator.NewOptionDirectory(ModPath, group.Name);
|
||||
if (!dir.Exists)
|
||||
{
|
||||
dir.Create();
|
||||
}
|
||||
|
||||
foreach (var option in group.OfType<SubMod>())
|
||||
{
|
||||
var optionDir = Creator.NewOptionDirectory(dir, option.Name);
|
||||
if (!optionDir.Exists)
|
||||
{
|
||||
optionDir.Create();
|
||||
}
|
||||
|
||||
option.WriteTexToolsMeta(optionDir);
|
||||
}
|
||||
|
|
@ -166,5 +156,4 @@ public partial class Mod
|
|||
Penumbra.Log.Error($"Error writing TexToolsMeta:\n{e}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
Penumbra/Mods/ModCache.cs
Normal file
26
Penumbra/Mods/ModCache.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public class ModCache
|
||||
{
|
||||
public int TotalFileCount;
|
||||
public int TotalSwapCount;
|
||||
public int TotalManipulations;
|
||||
public bool HasOptions;
|
||||
|
||||
public SortedList<string, object?> ChangedItems = new();
|
||||
public string LowerChangedItemsString = string.Empty;
|
||||
public string AllTagsLower = string.Empty;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
TotalFileCount = 0;
|
||||
TotalSwapCount = 0;
|
||||
TotalManipulations = 0;
|
||||
HasOptions = false;
|
||||
ChangedItems.Clear();
|
||||
LowerChangedItemsString = string.Empty;
|
||||
AllTagsLower = string.Empty;
|
||||
}
|
||||
}
|
||||
272
Penumbra/Mods/ModCacheManager.cs
Normal file
272
Penumbra/Mods/ModCacheManager.cs
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public class ModCacheManager : IDisposable, IReadOnlyList<ModCache>
|
||||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly IdentifierService _identifier;
|
||||
private readonly IReadOnlyList<Mod> _modManager;
|
||||
|
||||
private readonly List<ModCache> _cache = new();
|
||||
|
||||
// TODO ModManager2
|
||||
public ModCacheManager(CommunicatorService communicator, IdentifierService identifier, ModManager modManager)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_identifier = identifier;
|
||||
_modManager = modManager;
|
||||
|
||||
_communicator.ModOptionChanged.Event += OnModOptionChange;
|
||||
_communicator.ModPathChanged.Event += OnModPathChange;
|
||||
_communicator.ModDataChanged.Event += OnModDataChange;
|
||||
_communicator.ModDiscoveryFinished.Event += OnModDiscoveryFinished;
|
||||
if (!identifier.Valid)
|
||||
identifier.FinishedCreation += OnIdentifierCreation;
|
||||
OnModDiscoveryFinished();
|
||||
}
|
||||
|
||||
public IEnumerator<ModCache> GetEnumerator()
|
||||
=> _cache.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public int Count { get; private set; }
|
||||
|
||||
public ModCache this[int index]
|
||||
=> _cache[index];
|
||||
|
||||
public ModCache this[Mod mod]
|
||||
=> _cache[mod.Index];
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.ModOptionChanged.Event -= OnModOptionChange;
|
||||
_communicator.ModPathChanged.Event -= OnModPathChange;
|
||||
_communicator.ModDataChanged.Event -= OnModDataChange;
|
||||
_communicator.ModDiscoveryFinished.Event -= OnModDiscoveryFinished;
|
||||
}
|
||||
|
||||
/// <summary> Compute the items changed by a given meta manipulation and put them into the changedItems dictionary. </summary>
|
||||
public static void ComputeChangedItems(IObjectIdentifier identifier, IDictionary<string, object?> changedItems, MetaManipulation manip)
|
||||
{
|
||||
switch (manip.ManipulationType)
|
||||
{
|
||||
case MetaManipulation.Type.Imc:
|
||||
switch (manip.Imc.ObjectType)
|
||||
{
|
||||
case ObjectType.Equipment:
|
||||
case ObjectType.Accessory:
|
||||
identifier.Identify(changedItems,
|
||||
GamePaths.Equipment.Mtrl.Path(manip.Imc.PrimaryId, GenderRace.MidlanderMale, manip.Imc.EquipSlot, manip.Imc.Variant,
|
||||
"a"));
|
||||
break;
|
||||
case ObjectType.Weapon:
|
||||
identifier.Identify(changedItems,
|
||||
GamePaths.Weapon.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a"));
|
||||
break;
|
||||
case ObjectType.DemiHuman:
|
||||
identifier.Identify(changedItems,
|
||||
GamePaths.DemiHuman.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.EquipSlot, manip.Imc.Variant,
|
||||
"a"));
|
||||
break;
|
||||
case ObjectType.Monster:
|
||||
identifier.Identify(changedItems,
|
||||
GamePaths.Monster.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a"));
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case MetaManipulation.Type.Eqdp:
|
||||
identifier.Identify(changedItems,
|
||||
GamePaths.Equipment.Mdl.Path(manip.Eqdp.SetId, Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race), manip.Eqdp.Slot));
|
||||
break;
|
||||
case MetaManipulation.Type.Eqp:
|
||||
identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Eqp.SetId, GenderRace.MidlanderMale, manip.Eqp.Slot));
|
||||
break;
|
||||
case MetaManipulation.Type.Est:
|
||||
switch (manip.Est.Slot)
|
||||
{
|
||||
case EstManipulation.EstType.Hair:
|
||||
changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null);
|
||||
break;
|
||||
case EstManipulation.EstType.Face:
|
||||
changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null);
|
||||
break;
|
||||
case EstManipulation.EstType.Body:
|
||||
identifier.Identify(changedItems,
|
||||
GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race),
|
||||
EquipSlot.Body));
|
||||
break;
|
||||
case EstManipulation.EstType.Head:
|
||||
identifier.Identify(changedItems,
|
||||
GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race),
|
||||
EquipSlot.Head));
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case MetaManipulation.Type.Gmp:
|
||||
identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Gmp.SetId, GenderRace.MidlanderMale, EquipSlot.Head));
|
||||
break;
|
||||
case MetaManipulation.Type.Rsp:
|
||||
changedItems.TryAdd($"{manip.Rsp.SubRace.ToName()} {manip.Rsp.Attribute.ToFullString()}", null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2)
|
||||
{
|
||||
ModCache cache;
|
||||
switch (type)
|
||||
{
|
||||
case ModOptionChangeType.GroupAdded:
|
||||
case ModOptionChangeType.GroupDeleted:
|
||||
case ModOptionChangeType.OptionAdded:
|
||||
case ModOptionChangeType.OptionDeleted:
|
||||
cache = EnsureCount(mod);
|
||||
UpdateChangedItems(cache, mod);
|
||||
UpdateCounts(cache, mod);
|
||||
break;
|
||||
case ModOptionChangeType.GroupTypeChanged:
|
||||
UpdateHasOptions(EnsureCount(mod), mod);
|
||||
break;
|
||||
case ModOptionChangeType.OptionFilesChanged:
|
||||
case ModOptionChangeType.OptionFilesAdded:
|
||||
cache = EnsureCount(mod);
|
||||
UpdateChangedItems(cache, mod);
|
||||
UpdateFileCount(cache, mod);
|
||||
break;
|
||||
case ModOptionChangeType.OptionSwapsChanged:
|
||||
cache = EnsureCount(mod);
|
||||
UpdateChangedItems(cache, mod);
|
||||
UpdateSwapCount(cache, mod);
|
||||
break;
|
||||
case ModOptionChangeType.OptionMetaChanged:
|
||||
cache = EnsureCount(mod);
|
||||
UpdateChangedItems(cache, mod);
|
||||
UpdateMetaCount(cache, mod);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? old, DirectoryInfo? @new)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ModPathChangeType.Added:
|
||||
case ModPathChangeType.Reloaded:
|
||||
Refresh(EnsureCount(mod), mod);
|
||||
break;
|
||||
case ModPathChangeType.Deleted:
|
||||
--Count;
|
||||
var oldCache = _cache[mod.Index];
|
||||
oldCache.Reset();
|
||||
for (var i = mod.Index; i < Count; ++i)
|
||||
_cache[i] = _cache[i + 1];
|
||||
_cache[Count] = oldCache;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnModDataChange(ModDataChangeType type, Mod mod, string? _)
|
||||
{
|
||||
if ((type & (ModDataChangeType.LocalTags | ModDataChangeType.ModTags)) != 0)
|
||||
UpdateTags(EnsureCount(mod), mod);
|
||||
}
|
||||
|
||||
private void OnModDiscoveryFinished()
|
||||
{
|
||||
if (_modManager.Count > _cache.Count)
|
||||
_cache.AddRange(Enumerable.Range(0, _modManager.Count - _cache.Count).Select(_ => new ModCache()));
|
||||
|
||||
Parallel.ForEach(Enumerable.Range(0, _modManager.Count), idx => { Refresh(_cache[idx], _modManager[idx]); });
|
||||
}
|
||||
|
||||
private void OnIdentifierCreation()
|
||||
{
|
||||
Parallel.ForEach(Enumerable.Range(0, _modManager.Count), idx => { UpdateChangedItems(_cache[idx], _modManager[idx]); });
|
||||
_identifier.FinishedCreation -= OnIdentifierCreation;
|
||||
}
|
||||
|
||||
private static void UpdateFileCount(ModCache cache, Mod mod)
|
||||
=> cache.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count);
|
||||
|
||||
private static void UpdateSwapCount(ModCache cache, Mod mod)
|
||||
=> cache.TotalFileCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count);
|
||||
|
||||
private static void UpdateMetaCount(ModCache cache, Mod mod)
|
||||
=> cache.TotalFileCount = mod.AllSubMods.Sum(s => s.Manipulations.Count);
|
||||
|
||||
private static void UpdateHasOptions(ModCache cache, Mod mod)
|
||||
=> cache.HasOptions = mod.Groups.Any(o => o.IsOption);
|
||||
|
||||
private static void UpdateTags(ModCache cache, Mod mod)
|
||||
=> cache.AllTagsLower = string.Join('\0', mod.ModTags.Concat(mod.LocalTags).Select(s => s.ToLowerInvariant()));
|
||||
|
||||
private void UpdateChangedItems(ModCache cache, Mod mod)
|
||||
{
|
||||
cache.ChangedItems.Clear();
|
||||
if (!_identifier.Valid)
|
||||
return;
|
||||
|
||||
foreach (var gamePath in mod.AllRedirects)
|
||||
_identifier.AwaitedService.Identify(cache.ChangedItems, gamePath.ToString());
|
||||
|
||||
foreach (var manip in mod.AllManipulations)
|
||||
ComputeChangedItems(_identifier.AwaitedService, cache.ChangedItems, manip);
|
||||
|
||||
cache.LowerChangedItemsString = string.Join("\0", cache.ChangedItems.Keys.Select(k => k.ToLowerInvariant()));
|
||||
}
|
||||
|
||||
private static void UpdateCounts(ModCache cache, Mod mod)
|
||||
{
|
||||
cache.TotalFileCount = mod.Default.Files.Count;
|
||||
cache.TotalSwapCount = mod.Default.FileSwaps.Count;
|
||||
cache.TotalManipulations = mod.Default.Manipulations.Count;
|
||||
cache.HasOptions = false;
|
||||
foreach (var group in mod.Groups)
|
||||
{
|
||||
cache.HasOptions |= group.IsOption;
|
||||
foreach (var s in group)
|
||||
{
|
||||
cache.TotalFileCount += s.Files.Count;
|
||||
cache.TotalSwapCount += s.FileSwaps.Count;
|
||||
cache.TotalManipulations += s.Manipulations.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Refresh(ModCache cache, Mod mod)
|
||||
{
|
||||
UpdateTags(cache, mod);
|
||||
UpdateCounts(cache, mod);
|
||||
UpdateChangedItems(cache, mod);
|
||||
}
|
||||
|
||||
private ModCache EnsureCount(Mod mod)
|
||||
{
|
||||
if (mod.Index < Count)
|
||||
return _cache[mod.Index];
|
||||
|
||||
|
||||
if (mod.Index >= _cache.Count)
|
||||
_cache.AddRange(Enumerable.Range(0, mod.Index - _cache.Count).Select(_ => new ModCache()));
|
||||
else if (mod.Index >= Count)
|
||||
for (var i = Count; i <= mod.Index; ++i)
|
||||
_cache[i].Reset();
|
||||
|
||||
return _cache[mod.Index];
|
||||
}
|
||||
}
|
||||
|
|
@ -105,6 +105,7 @@ public class Penumbra : IDalamudPlugin
|
|||
RedrawService = _tmp.Services.GetRequiredService<RedrawService>();
|
||||
_tmp.Services.GetRequiredService<ResourceService>();
|
||||
ResourceLoader = _tmp.Services.GetRequiredService<ResourceLoader>();
|
||||
_tmp.Services.GetRequiredService<ModCacheManager>();
|
||||
using (var t = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.PathResolver))
|
||||
{
|
||||
PathResolver = _tmp.Services.GetRequiredService<PathResolver>();
|
||||
|
|
|
|||
|
|
@ -149,8 +149,9 @@ public class PenumbraNew
|
|||
.AddSingleton<ModFileEditor>()
|
||||
.AddSingleton<ModMetaEditor>()
|
||||
.AddSingleton<ModSwapEditor>()
|
||||
.AddSingleton<ModNormalizer>()
|
||||
.AddSingleton<ModEditor>();
|
||||
.AddSingleton<ModNormalizer>()
|
||||
.AddSingleton<ModEditor>()
|
||||
.AddSingleton<ModCacheManager>();
|
||||
|
||||
// Add API
|
||||
services.AddSingleton<PenumbraApi>()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Util;
|
||||
|
|
@ -7,7 +8,9 @@ namespace Penumbra.Services;
|
|||
|
||||
public class CommunicatorService : IDisposable
|
||||
{
|
||||
/// <summary> <list type="number">
|
||||
/// <summary>
|
||||
/// Triggered whenever collection setup is changed.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the type of the changed collection. (Inactive or Temporary for additions or deletions)</item>
|
||||
/// <item>Parameter is the old collection, or null on additions.</item>
|
||||
/// <item>Parameter is the new collection, or null on deletions.</item>
|
||||
|
|
@ -15,21 +18,18 @@ public class CommunicatorService : IDisposable
|
|||
/// </list> </summary>
|
||||
public readonly EventWrapper<CollectionType, ModCollection?, ModCollection?, string> CollectionChange = new(nameof(CollectionChange));
|
||||
|
||||
/// <summary> <list type="number">
|
||||
/// <summary>
|
||||
/// Triggered whenever a temporary mod for all collections is changed.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter added, deleted or edited temporary mod.</item>
|
||||
/// <item>Parameter is whether the mod was newly created.</item>
|
||||
/// <item>Parameter is whether the mod was deleted.</item>
|
||||
/// </list> </summary>
|
||||
public readonly EventWrapper<TemporaryMod, bool, bool> TemporaryGlobalModChange = new(nameof(TemporaryGlobalModChange));
|
||||
|
||||
/// <summary> <list type="number">
|
||||
/// <item>Parameter is the type of change. </item>
|
||||
/// <item>Parameter is the affected mod. </item>
|
||||
/// <item>Parameter is either null or the old name of the mod. </item>
|
||||
/// </list> </summary>
|
||||
public readonly EventWrapper<ModDataChangeType, Mod, string?> ModMetaChange = new(nameof(ModMetaChange));
|
||||
|
||||
/// <summary> <list type="number">
|
||||
/// <summary>
|
||||
/// Triggered whenever a character base draw object is being created by the game.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the game object for which a draw object is created. </item>
|
||||
/// <item>Parameter is the name of the applied collection. </item>
|
||||
/// <item>Parameter is a pointer to the model id (an uint). </item>
|
||||
|
|
@ -45,14 +45,18 @@ public class CommunicatorService : IDisposable
|
|||
/// </list> </summary>
|
||||
public readonly EventWrapper<nint, string, nint> CreatedCharacterBase = new(nameof(CreatedCharacterBase));
|
||||
|
||||
/// <summary> <list type="number">
|
||||
/// <summary>
|
||||
/// Triggered whenever mod meta data or local data is changed.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the type of data change for the mod, which can be multiple flags. </item>
|
||||
/// <item>Parameter is the changed mod. </item>
|
||||
/// <item>Parameter is the old name of the mod in case of a name change, and null otherwise. </item>
|
||||
/// </list> </summary>
|
||||
public readonly EventWrapper<ModDataChangeType, Mod, string?> ModDataChanged = new(nameof(ModDataChanged));
|
||||
|
||||
/// <summary><list type="number">
|
||||
/// <summary>
|
||||
/// Triggered whenever an option of a mod is changed inside the mod.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the type option change. </item>
|
||||
/// <item>Parameter is the changed mod. </item>
|
||||
/// <item>Parameter is the index of the changed group inside the mod. </item>
|
||||
|
|
@ -61,14 +65,44 @@ public class CommunicatorService : IDisposable
|
|||
/// </list> </summary>
|
||||
public readonly EventWrapper<ModOptionChangeType, Mod, int, int, int> ModOptionChanged = new(nameof(ModOptionChanged));
|
||||
|
||||
|
||||
/// <summary> Triggered whenever mods are prepared to be rediscovered. </summary>
|
||||
public readonly EventWrapper ModDiscoveryStarted = new(nameof(ModDiscoveryStarted));
|
||||
|
||||
/// <summary> Triggered whenever a new mod discovery has finished. </summary>
|
||||
public readonly EventWrapper ModDiscoveryFinished = new(nameof(ModDiscoveryFinished));
|
||||
|
||||
/// <summary>
|
||||
/// Triggered whenever the mod root directory changes.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the full path of the new directory. </item>
|
||||
/// <item>Parameter is whether the new directory is valid. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public readonly EventWrapper<string, bool> ModDirectoryChanged = new(nameof(ModDirectoryChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Triggered whenever a mod is added, deleted, moved or reloaded.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the type of change. </item>
|
||||
/// <item>Parameter is the changed mod. </item>
|
||||
/// <item>Parameter is the old directory on deletion, move or reload and null on addition. </item>
|
||||
/// <item>Parameter is the new directory on addition, move or reload and null on deletion. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public EventWrapper<ModPathChangeType, Mod, DirectoryInfo?, DirectoryInfo?> ModPathChanged = new(nameof(ModPathChanged));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CollectionChange.Dispose();
|
||||
TemporaryGlobalModChange.Dispose();
|
||||
ModMetaChange.Dispose();
|
||||
CreatingCharacterBase.Dispose();
|
||||
CreatedCharacterBase.Dispose();
|
||||
ModDataChanged.Dispose();
|
||||
ModOptionChanged.Dispose();
|
||||
ModDiscoveryStarted.Dispose();
|
||||
ModDiscoveryFinished.Dispose();
|
||||
ModDirectoryChanged.Dispose();
|
||||
ModPathChanged.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,60 @@ public readonly struct EventWrapper : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public readonly struct EventWrapper<T1> : IDisposable
|
||||
{
|
||||
private readonly string _name;
|
||||
private readonly List<Action<T1>> _event = new();
|
||||
|
||||
public EventWrapper(string name)
|
||||
=> _name = name;
|
||||
|
||||
public void Invoke(T1 arg1)
|
||||
{
|
||||
lock (_event)
|
||||
{
|
||||
foreach (var action in _event)
|
||||
{
|
||||
try
|
||||
{
|
||||
action.Invoke(arg1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_event)
|
||||
{
|
||||
_event.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<T1> Event
|
||||
{
|
||||
add
|
||||
{
|
||||
lock (_event)
|
||||
{
|
||||
if (_event.All(a => a != value))
|
||||
_event.Add(value);
|
||||
}
|
||||
}
|
||||
remove
|
||||
{
|
||||
lock (_event)
|
||||
{
|
||||
_event.Remove(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct EventWrapper<T1, T2> : IDisposable
|
||||
{
|
||||
private readonly string _name;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue