ModManager2

This commit is contained in:
Ottermandias 2023-03-30 23:29:01 +02:00
parent 1541cdb78d
commit 70c1a2604f
11 changed files with 815 additions and 117 deletions

View file

@ -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";

View file

@ -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

View file

@ -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);
}

View 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);
}

View file

@ -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
View 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;
}
}

View 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];
}
}

View file

@ -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>();

View file

@ -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>()

View file

@ -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();
}
}

View file

@ -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;