diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs
index 083fb803..b675ca6b 100644
--- a/Penumbra/Mods/Editor/ModBackup.cs
+++ b/Penumbra/Mods/Editor/ModBackup.cs
@@ -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;
/// Utility to create and apply a zipped backup of a mod.
public class ModBackup
-{
+{
+ /// Set when reading Config and migrating from v4 to v5.
+ public static bool MigrateModBackups = false;
public static bool CreatingBackup { get; private set; }
private readonly Mod _mod;
@@ -22,9 +25,9 @@ public class ModBackup
}
/// Migrate file extensions.
- public static void MigrateZipToPmp(ModManager modManager)
+ public static void MigrateZipToPmp(IEnumerable modStorage)
{
- foreach (var mod in modManager)
+ foreach (var mod in modStorage)
{
var pmpName = mod.ModPath + ".pmp";
var zipName = mod.ModPath + ".zip";
diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs
index 95fbe2ac..febe8209 100644
--- a/Penumbra/Mods/Manager/Mod.Manager.cs
+++ b/Penumbra/Mods/Manager/Mod.Manager.cs
@@ -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, IDisposable
+
+/// Describes the state of a potential move-target for a mod.
+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;
- ///
- /// 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.
- ///
- public readonly HashSet 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 GetEnumerator()
- => _mods.GetEnumerator();
-
- IEnumerator IEnumerable.GetEnumerator()
- => GetEnumerator();
-
- ///
- /// Try to obtain a mod by its directory name (unique identifier, preferred),
- /// or the first mod of the given name if no directory fits.
- ///
- 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;
+ }
+
+ /// Change the mod base directory and discover available mods.
+ public void DiscoverMods(string newDir)
+ {
+ SetBaseDirectory(newDir, false);
+ DiscoverMods();
+ }
+
+ ///
+ /// Discover mods without changing the root directory.
+ ///
+ 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);
+ }
+
+ /// Load a new mod and add it to the manager if successful.
+ 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}.");
+ }
+
+ ///
+ /// 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.
+ ///
+ 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}.");
+ }
+
+ ///
+ /// Reload a mod without changing its base directory.
+ /// If the base directory does not exist anymore, the mod will be deleted.
+ ///
+ 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;
- }
-
- /// The actual list of mods.
- private readonly List _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);
+ }
+
+
+ ///
+ /// Rename/Move a mod directory.
+ /// Updates all collection settings and sort order settings.
+ ///
+ 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);
+ }
+
+ /// Return the state of the new potential name of a directory.
+ 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;
}
+
+ /// Add new mods to NewMods and remove deleted mods from NewMods.
+ 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()
{ }
+
+ ///
+ /// 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.
+ ///
+ 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);
+ }
+
+
+
+ ///
+ /// Iterate through available mods with multiple threads and queue their loads,
+ /// then add the mods from the queue.
+ ///
+ private void ScanMods()
+ {
+ var options = new ParallelOptions()
+ {
+ MaxDegreeOfParallelism = Environment.ProcessorCount / 2,
+ };
+ var queue = new ConcurrentQueue();
+ 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, IDisposable
diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs
index 8e96b3e2..88cc9e75 100644
--- a/Penumbra/Mods/Manager/ModOptionEditor.cs
+++ b/Penumbra/Mods/Manager/ModOptionEditor.cs
@@ -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);
}
diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs
new file mode 100644
index 00000000..dbf5c46a
--- /dev/null
+++ b/Penumbra/Mods/Manager/ModStorage.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Penumbra.Mods;
+
+public class ModStorage : IReadOnlyList
+{
+ /// The actual list of mods.
+ protected readonly List Mods = new();
+
+ public int Count
+ => Mods.Count;
+
+ public Mod this[int idx]
+ => Mods[idx];
+
+ public Mod this[Index idx]
+ => Mods[idx];
+
+ public IEnumerator GetEnumerator()
+ => Mods.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => GetEnumerator();
+
+ ///
+ /// Try to obtain a mod by its directory name (unique identifier, preferred),
+ /// or the first mod of the given name if no directory fits.
+ ///
+ 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ protected readonly HashSet 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);
+}
diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs
index 97910191..bd11d477 100644
--- a/Penumbra/Mods/Mod.Files.cs
+++ b/Penumbra/Mods/Mod.Files.cs
@@ -14,80 +14,78 @@ public partial class Mod
public ISubMod Default
=> _default;
- public IReadOnlyList< IModGroup > Groups
+ public IReadOnlyList Groups
=> _groups;
- internal readonly SubMod _default;
- internal readonly List< IModGroup > _groups = new();
+ internal readonly SubMod _default;
+ internal readonly List _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 AllSubMods
+ => _groups.SelectMany(o => o).Prepend(_default);
- public IEnumerable< MetaManipulation > AllManipulations
- => AllSubMods.SelectMany( s => s.Manipulations );
+ public IEnumerable AllManipulations
+ => AllSubMods.SelectMany(s => s.Manipulations);
- public IEnumerable< Utf8GamePath > AllRedirects
- => AllSubMods.SelectMany( s => s.Files.Keys.Concat( s.FileSwaps.Keys ) );
+ public IEnumerable 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 AllFiles
+ => AllSubMods.SelectMany(o => o.Files)
+ .Select(p => p.Value);
- public IEnumerable< FileInfo > GroupFiles
- => ModPath.EnumerateFiles( "group_*.json" );
+ public IEnumerable GroupFiles
+ => ModPath.EnumerateFiles("group_*.json");
- public List< FullPath > FindUnusedFiles()
+ public List 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.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())
{
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}");
}
}
-
-}
\ No newline at end of file
+}
diff --git a/Penumbra/Mods/ModCache.cs b/Penumbra/Mods/ModCache.cs
new file mode 100644
index 00000000..3fb6d3f0
--- /dev/null
+++ b/Penumbra/Mods/ModCache.cs
@@ -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 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;
+ }
+}
diff --git a/Penumbra/Mods/ModCacheManager.cs b/Penumbra/Mods/ModCacheManager.cs
new file mode 100644
index 00000000..69787644
--- /dev/null
+++ b/Penumbra/Mods/ModCacheManager.cs
@@ -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
+{
+ private readonly CommunicatorService _communicator;
+ private readonly IdentifierService _identifier;
+ private readonly IReadOnlyList _modManager;
+
+ private readonly List _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 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;
+ }
+
+ /// Compute the items changed by a given meta manipulation and put them into the changedItems dictionary.
+ public static void ComputeChangedItems(IObjectIdentifier identifier, IDictionary 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];
+ }
+}
diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs
index 78931cf0..283de2bb 100644
--- a/Penumbra/Penumbra.cs
+++ b/Penumbra/Penumbra.cs
@@ -105,6 +105,7 @@ public class Penumbra : IDalamudPlugin
RedrawService = _tmp.Services.GetRequiredService();
_tmp.Services.GetRequiredService();
ResourceLoader = _tmp.Services.GetRequiredService();
+ _tmp.Services.GetRequiredService();
using (var t = _tmp.Services.GetRequiredService().Measure(StartTimeType.PathResolver))
{
PathResolver = _tmp.Services.GetRequiredService();
diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs
index afba2adf..73c642ac 100644
--- a/Penumbra/PenumbraNew.cs
+++ b/Penumbra/PenumbraNew.cs
@@ -149,8 +149,9 @@ public class PenumbraNew
.AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton()
- .AddSingleton();
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton();
// Add API
services.AddSingleton()
diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs
index 18c64eac..55f057a3 100644
--- a/Penumbra/Services/CommunicatorService.cs
+++ b/Penumbra/Services/CommunicatorService.cs
@@ -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
{
- ///
+ ///
+ /// Triggered whenever collection setup is changed.
+ ///
/// - Parameter is the type of the changed collection. (Inactive or Temporary for additions or deletions)
/// - Parameter is the old collection, or null on additions.
/// - Parameter is the new collection, or null on deletions.
@@ -15,21 +18,18 @@ public class CommunicatorService : IDisposable
///
public readonly EventWrapper CollectionChange = new(nameof(CollectionChange));
- ///
+ ///
+ /// Triggered whenever a temporary mod for all collections is changed.
+ ///
/// - Parameter added, deleted or edited temporary mod.
/// - Parameter is whether the mod was newly created.
/// - Parameter is whether the mod was deleted.
///
public readonly EventWrapper TemporaryGlobalModChange = new(nameof(TemporaryGlobalModChange));
- ///
- /// - Parameter is the type of change.
- /// - Parameter is the affected mod.
- /// - Parameter is either null or the old name of the mod.
- ///
- public readonly EventWrapper ModMetaChange = new(nameof(ModMetaChange));
-
- ///
+ ///
+ /// Triggered whenever a character base draw object is being created by the game.
+ ///
/// - Parameter is the game object for which a draw object is created.
/// - Parameter is the name of the applied collection.
/// - Parameter is a pointer to the model id (an uint).
@@ -45,14 +45,18 @@ public class CommunicatorService : IDisposable
///
public readonly EventWrapper CreatedCharacterBase = new(nameof(CreatedCharacterBase));
- ///
+ ///
+ /// Triggered whenever mod meta data or local data is changed.
+ ///
/// - Parameter is the type of data change for the mod, which can be multiple flags.
/// - Parameter is the changed mod.
/// - Parameter is the old name of the mod in case of a name change, and null otherwise.
///
public readonly EventWrapper ModDataChanged = new(nameof(ModDataChanged));
- ///
+ ///
+ /// Triggered whenever an option of a mod is changed inside the mod.
+ ///
/// - Parameter is the type option change.
/// - Parameter is the changed mod.
/// - Parameter is the index of the changed group inside the mod.
@@ -61,14 +65,44 @@ public class CommunicatorService : IDisposable
///
public readonly EventWrapper ModOptionChanged = new(nameof(ModOptionChanged));
+
+ /// Triggered whenever mods are prepared to be rediscovered.
+ public readonly EventWrapper ModDiscoveryStarted = new(nameof(ModDiscoveryStarted));
+
+ /// Triggered whenever a new mod discovery has finished.
+ public readonly EventWrapper ModDiscoveryFinished = new(nameof(ModDiscoveryFinished));
+
+ ///
+ /// Triggered whenever the mod root directory changes.
+ ///
+ /// - Parameter is the full path of the new directory.
+ /// - Parameter is whether the new directory is valid.
+ ///
+ ///
+ public readonly EventWrapper ModDirectoryChanged = new(nameof(ModDirectoryChanged));
+
+ ///
+ /// Triggered whenever a mod is added, deleted, moved or reloaded.
+ ///
+ /// - Parameter is the type of change.
+ /// - Parameter is the changed mod.
+ /// - Parameter is the old directory on deletion, move or reload and null on addition.
+ /// - Parameter is the new directory on addition, move or reload and null on deletion.
+ ///
+ ///
+ public EventWrapper 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();
}
}
diff --git a/Penumbra/Util/EventWrapper.cs b/Penumbra/Util/EventWrapper.cs
index e25cc99c..2472e74d 100644
--- a/Penumbra/Util/EventWrapper.cs
+++ b/Penumbra/Util/EventWrapper.cs
@@ -58,6 +58,60 @@ public readonly struct EventWrapper : IDisposable
}
}
+public readonly struct EventWrapper : IDisposable
+{
+ private readonly string _name;
+ private readonly List> _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 Event
+ {
+ add
+ {
+ lock (_event)
+ {
+ if (_event.All(a => a != value))
+ _event.Add(value);
+ }
+ }
+ remove
+ {
+ lock (_event)
+ {
+ _event.Remove(value);
+ }
+ }
+ }
+}
+
public readonly struct EventWrapper : IDisposable
{
private readonly string _name;