Move Mod.Manager and ModCollection.Manager to outer scope and required changes.

This commit is contained in:
Ottermandias 2023-03-27 15:22:39 +02:00
parent ccdafcf85d
commit 1253079968
59 changed files with 2562 additions and 2615 deletions

View file

@ -11,12 +11,12 @@ namespace Penumbra.Mods;
public class DuplicateManager
{
private readonly Mod.Manager _modManager;
private readonly ModManager _modManager;
private readonly SHA256 _hasher = SHA256.Create();
private readonly ModFileCollection _files;
private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new();
public DuplicateManager(ModFileCollection files, Mod.Manager modManager)
public DuplicateManager(ModFileCollection files, ModManager modManager)
{
_files = files;
_modManager = modManager;
@ -80,7 +80,7 @@ public class DuplicateManager
}
else
{
var sub = (Mod.SubMod)subMod;
var sub = (SubMod)subMod;
sub.FileData = dict;
if (groupIdx == -1)
mod.SaveDefaultMod();

View file

@ -10,12 +10,12 @@ public class ModBackup
{
public static bool CreatingBackup { get; private set; }
private readonly Mod.Manager _modManager;
private readonly ModManager _modManager;
private readonly Mod _mod;
public readonly string Name;
public readonly bool Exists;
public ModBackup(Mod.Manager modManager, Mod mod)
public ModBackup(ModManager modManager, Mod mod)
{
_modManager = modManager;
_mod = mod;
@ -24,9 +24,9 @@ public class ModBackup
}
/// <summary> Migrate file extensions. </summary>
public static void MigrateZipToPmp(Mod.Manager manager)
public static void MigrateZipToPmp(ModManager modManager)
{
foreach (var mod in manager)
foreach (var mod in modManager)
{
var pmpName = mod.ModPath + ".pmp";
var zipName = mod.ModPath + ".zip";

View file

@ -9,11 +9,11 @@ namespace Penumbra.Mods;
public class ModFileEditor
{
private readonly ModFileCollection _files;
private readonly Mod.Manager _modManager;
private readonly ModManager _modManager;
public bool Changes { get; private set; }
public ModFileEditor(ModFileCollection files, Mod.Manager modManager)
public ModFileEditor(ModFileCollection files, ModManager modManager)
{
_files = files;
_modManager = modManager;
@ -24,7 +24,7 @@ public class ModFileEditor
Changes = false;
}
public int Apply(Mod mod, Mod.SubMod option)
public int Apply(Mod mod, SubMod option)
{
var dict = new Dictionary<Utf8GamePath, FullPath>();
var num = 0;

View file

@ -6,7 +6,7 @@ namespace Penumbra.Mods;
public class ModMetaEditor
{
private readonly Mod.Manager _modManager;
private readonly ModManager _modManager;
private readonly HashSet<ImcManipulation> _imc = new();
private readonly HashSet<EqpManipulation> _eqp = new();
@ -15,7 +15,7 @@ public class ModMetaEditor
private readonly HashSet<EstManipulation> _est = new();
private readonly HashSet<RspManipulation> _rsp = new();
public ModMetaEditor(Mod.Manager modManager)
public ModMetaEditor(ModManager modManager)
=> _modManager = modManager;
public bool Changes { get; private set; } = false;

View file

@ -11,7 +11,7 @@ namespace Penumbra.Mods;
public class ModNormalizer
{
private readonly Mod.Manager _modManager;
private readonly ModManager _modManager;
private readonly List<List<Dictionary<Utf8GamePath, FullPath>>> _redirections = new();
public Mod Mod { get; private set; } = null!;
@ -24,7 +24,7 @@ public class ModNormalizer
public bool Running
=> Step < TotalSteps;
public ModNormalizer(Mod.Manager modManager)
public ModNormalizer(ModManager modManager)
=> _modManager = modManager;
public void Normalize(Mod mod)
@ -177,7 +177,7 @@ public class ModNormalizer
_redirections[groupIdx + 1].Add(new Dictionary<Utf8GamePath, FullPath>());
var groupDir = Mod.Creator.CreateModFolder(directory, group.Name);
foreach (var option in group.OfType<Mod.SubMod>())
foreach (var option in group.OfType<SubMod>())
{
var optionDir = Mod.Creator.CreateModFolder(groupDir, option.Name);
@ -279,7 +279,7 @@ public class ModNormalizer
private void ApplyRedirections()
{
foreach (var option in Mod.AllSubMods.OfType<Mod.SubMod>())
foreach (var option in Mod.AllSubMods.OfType<SubMod>())
{
_modManager.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, _redirections[option.GroupIdx + 1][option.OptionIdx]);
}

View file

@ -5,13 +5,13 @@ using Penumbra.Util;
public class ModSwapEditor
{
private readonly Mod.Manager _modManager;
private readonly ModManager _modManager;
private readonly Dictionary<Utf8GamePath, FullPath> _swaps = new();
public IReadOnlyDictionary<Utf8GamePath, FullPath> Swaps
=> _swaps;
public ModSwapEditor(Mod.Manager modManager)
public ModSwapEditor(ModManager modManager)
=> _modManager = modManager;
public void Revert(ISubMod option)

View file

@ -4,202 +4,199 @@ using System.Linq;
namespace Penumbra.Mods;
public partial class Mod
public partial class ModManager
{
public partial class Manager
public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory);
public event ModPathChangeDelegate ModPathChanged;
// Rename/Move a mod directory.
// Updates all collection settings and sort order settings.
public void MoveModDirectory(int idx, string newName)
{
public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory);
var mod = this[idx];
var oldName = mod.Name;
var oldDirectory = mod.ModPath;
public event ModPathChangeDelegate ModPathChanged;
// Rename/Move a mod directory.
// Updates all collection settings and sort order settings.
public void MoveModDirectory(int idx, string newName)
switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir))
{
var mod = this[idx];
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);
new ModBackup(this, mod).Move(null, dir.Name);
dir.Refresh();
mod.ModPath = dir;
if (!mod.Reload(this, false, out var metaChange))
{
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
return;
}
ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir);
if (metaChange != ModDataChangeType.None)
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
}
/// <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(int idx)
{
var mod = this[idx];
var oldName = mod.Name;
ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
if (!mod.Reload(this, 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(idx);
return;
}
ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath);
if (metaChange != ModDataChangeType.None)
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
}
/// <summary>
/// Delete a mod by its index. 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(int idx)
{
var mod = this[idx];
if (Directory.Exists(mod.ModPath.FullName))
case NewDirectoryState.NonExisting:
// Nothing to do
break;
case NewDirectoryState.ExistsEmpty:
try
{
Directory.Delete(mod.ModPath.FullName, true);
Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}.");
Directory.Delete(dir!.FullName);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}");
Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}");
return;
}
ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null);
_mods.RemoveAt(idx);
foreach (var remainingMod in _mods.Skip(idx))
--remainingMod.Index;
Penumbra.Log.Debug($"Deleted mod {mod.Name}.");
}
/// <summary> Load a new mod and add it to the manager if successful. </summary>
public void AddMod(DirectoryInfo modFolder)
{
if (_mods.Any(m => m.ModPath.Name == modFolder.Name))
break;
// Should be caught beforehand.
case NewDirectoryState.ExistsNonEmpty:
case NewDirectoryState.ExistsAsFile:
case NewDirectoryState.ContainsInvalidSymbols:
// Nothing to do at all.
case NewDirectoryState.Identical:
default:
return;
Creator.SplitMultiGroups(modFolder);
var mod = LoadMod(this, modFolder, true);
if (mod == null)
return;
mod.Index = _mods.Count;
_mods.Add(mod);
ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath);
Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}.");
}
public enum NewDirectoryState
try
{
NonExisting,
ExistsEmpty,
ExistsNonEmpty,
ExistsAsFile,
ContainsInvalidSymbols,
Identical,
Empty,
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;
}
/// <summary> Return the state of the new potential name of a directory. </summary>
public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory)
DataEditor.MoveDataFile(oldDirectory, dir);
new ModBackup(this, mod).Move(null, dir.Name);
dir.Refresh();
mod.ModPath = dir;
if (!mod.Reload(this, false, out var metaChange))
{
directory = null;
if (newName.Length == 0)
return NewDirectoryState.Empty;
if (oldName == newName)
return NewDirectoryState.Identical;
var fixedNewName = 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;
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
return;
}
ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir);
if (metaChange != ModDataChangeType.None)
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
}
/// <summary> Add new mods to NewMods and remove deleted mods from NewMods. </summary>
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory)
/// <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(int idx)
{
var mod = this[idx];
var oldName = mod.Name;
ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
if (!mod.Reload(this, true, out var metaChange))
{
switch (type)
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(idx);
return;
}
ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath);
if (metaChange != ModDataChangeType.None)
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
}
/// <summary>
/// Delete a mod by its index. 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(int idx)
{
var mod = this[idx];
if (Directory.Exists(mod.ModPath.FullName))
try
{
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;
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}");
}
ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null);
_mods.RemoveAt(idx);
foreach (var remainingMod in _mods.Skip(idx))
--remainingMod.Index;
Penumbra.Log.Debug($"Deleted mod {mod.Name}.");
}
/// <summary> Load a new mod and add it to the manager if successful. </summary>
public void AddMod(DirectoryInfo modFolder)
{
if (_mods.Any(m => m.ModPath.Name == modFolder.Name))
return;
Mod.Creator.SplitMultiGroups(modFolder);
var mod = Mod.LoadMod(this, modFolder, true);
if (mod == null)
return;
mod.Index = _mods.Count;
_mods.Add(mod);
ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath);
Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}.");
}
public enum NewDirectoryState
{
NonExisting,
ExistsEmpty,
ExistsNonEmpty,
ExistsAsFile,
ContainsInvalidSymbols,
Identical,
Empty,
}
/// <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;
}
}
}

View file

@ -1,12 +0,0 @@
using System;
using System.Linq;
namespace Penumbra.Mods;
public sealed partial class Mod
{
public partial class Manager
{
}
}

View file

@ -11,371 +11,367 @@ using Penumbra.Util;
namespace Penumbra.Mods;
public sealed partial class Mod
public sealed partial class ModManager
{
public sealed partial class Manager
public delegate void ModOptionChangeDelegate(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx);
public event ModOptionChangeDelegate ModOptionChanged;
public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type)
{
public delegate void ModOptionChangeDelegate(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx);
public event ModOptionChangeDelegate ModOptionChanged;
var group = mod._groups[groupIdx];
if (group.Type == type)
return;
public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type)
mod._groups[groupIdx] = group.Convert(type);
ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1);
}
public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption)
{
var group = mod._groups[groupIdx];
if (group.DefaultSettings == defaultOption)
return;
group.DefaultSettings = defaultOption;
ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1);
}
public void RenameModGroup(Mod mod, int groupIdx, string newName)
{
var group = mod._groups[groupIdx];
var oldName = group.Name;
if (oldName == newName || !VerifyFileName(mod, group, newName, true))
return;
group.DeleteFile(mod.ModPath, groupIdx);
var _ = group switch
{
var group = mod._groups[groupIdx];
if (group.Type == type)
return;
SingleModGroup s => s.Name = newName,
MultiModGroup m => m.Name = newName,
_ => newName,
};
mod._groups[groupIdx] = group.Convert(type);
ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1);
}
ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1);
}
public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption)
{
var group = mod._groups[groupIdx];
if (group.DefaultSettings == defaultOption)
return;
public void AddModGroup(Mod mod, GroupType type, string newName)
{
if (!VerifyFileName(mod, null, newName, true))
return;
group.DefaultSettings = defaultOption;
ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1);
}
var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1;
public void RenameModGroup(Mod mod, int groupIdx, string newName)
{
var group = mod._groups[groupIdx];
var oldName = group.Name;
if (oldName == newName || !VerifyFileName(mod, group, newName, true))
return;
group.DeleteFile(mod.ModPath, groupIdx);
var _ = group switch
mod._groups.Add(type == GroupType.Multi
? new MultiModGroup
{
SingleModGroup s => s.Name = newName,
MultiModGroup m => m.Name = newName,
_ => newName,
};
ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1);
}
public void AddModGroup(Mod mod, GroupType type, string newName)
{
if (!VerifyFileName(mod, null, newName, true))
return;
var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1;
mod._groups.Add(type == GroupType.Multi
? new MultiModGroup
{
Name = newName,
Priority = maxPriority,
}
: new SingleModGroup
{
Name = newName,
Priority = maxPriority,
});
ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1);
}
public void DeleteModGroup(Mod mod, int groupIdx)
{
var group = mod._groups[groupIdx];
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1);
mod._groups.RemoveAt(groupIdx);
UpdateSubModPositions(mod, groupIdx);
group.DeleteFile(mod.ModPath, groupIdx);
ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1);
}
public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo)
{
if (mod._groups.Move(groupIdxFrom, groupIdxTo))
{
UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo));
ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo);
Name = newName,
Priority = maxPriority,
}
}
private static void UpdateSubModPositions(Mod mod, int fromGroup)
{
foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup))
: new SingleModGroup
{
foreach (var (o, optionIdx) in group.OfType<SubMod>().WithIndex())
o.SetPosition(groupIdx, optionIdx);
}
}
Name = newName,
Priority = maxPriority,
});
ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1);
}
public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription)
public void DeleteModGroup(Mod mod, int groupIdx)
{
var group = mod._groups[groupIdx];
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1);
mod._groups.RemoveAt(groupIdx);
UpdateSubModPositions(mod, groupIdx);
group.DeleteFile(mod.ModPath, groupIdx);
ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1);
}
public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo)
{
if (mod._groups.Move(groupIdxFrom, groupIdxTo))
{
var group = mod._groups[groupIdx];
if (group.Description == newDescription)
return;
var _ = group switch
{
SingleModGroup s => s.Description = newDescription,
MultiModGroup m => m.Description = newDescription,
_ => newDescription,
};
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1);
}
public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription)
{
var group = mod._groups[groupIdx];
var option = group[optionIdx];
if (option.Description == newDescription || option is not SubMod s)
return;
s.Description = newDescription;
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
}
public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority)
{
var group = mod._groups[groupIdx];
if (group.Priority == newPriority)
return;
var _ = group switch
{
SingleModGroup s => s.Priority = newPriority,
MultiModGroup m => m.Priority = newPriority,
_ => newPriority,
};
ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1);
}
public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority)
{
switch (mod._groups[groupIdx])
{
case SingleModGroup:
ChangeGroupPriority(mod, groupIdx, newPriority);
break;
case MultiModGroup m:
if (m.PrioritizedOptions[optionIdx].Priority == newPriority)
return;
m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority);
ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1);
return;
}
}
public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName)
{
switch (mod._groups[groupIdx])
{
case SingleModGroup s:
if (s.OptionData[optionIdx].Name == newName)
return;
s.OptionData[optionIdx].Name = newName;
break;
case MultiModGroup m:
var option = m.PrioritizedOptions[optionIdx].Mod;
if (option.Name == newName)
return;
option.Name = newName;
break;
}
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
}
public void AddOption(Mod mod, int groupIdx, string newName)
{
var group = mod._groups[groupIdx];
var subMod = new SubMod(mod) { Name = newName };
subMod.SetPosition(groupIdx, group.Count);
switch (group)
{
case SingleModGroup s:
s.OptionData.Add(subMod);
break;
case MultiModGroup m:
m.PrioritizedOptions.Add((subMod, 0));
break;
}
ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
}
public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0)
{
if (option is not SubMod o)
return;
var group = mod._groups[groupIdx];
if (group.Count > 63)
{
Penumbra.Log.Error(
$"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, "
+ "since only up to 64 options are supported in one group.");
return;
}
o.SetPosition(groupIdx, group.Count);
switch (group)
{
case SingleModGroup s:
s.OptionData.Add(o);
break;
case MultiModGroup m:
m.PrioritizedOptions.Add((o, priority));
break;
}
ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
}
public void DeleteOption(Mod mod, int groupIdx, int optionIdx)
{
var group = mod._groups[groupIdx];
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
switch (group)
{
case SingleModGroup s:
s.OptionData.RemoveAt(optionIdx);
break;
case MultiModGroup m:
m.PrioritizedOptions.RemoveAt(optionIdx);
break;
}
group.UpdatePositions(optionIdx);
ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1);
}
public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo)
{
var group = mod._groups[groupIdx];
if (group.MoveOption(optionIdxFrom, optionIdxTo))
ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo);
}
public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet<MetaManipulation> manipulations)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.Manipulations.Count == manipulations.Count
&& subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m)))
return;
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.ManipulationData = manipulations;
ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1);
}
public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> replacements)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.FileData.SetEquals(replacements))
return;
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.FileData = replacements;
ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1);
}
public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> additions)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
var oldCount = subMod.FileData.Count;
subMod.FileData.AddFrom(additions);
if (oldCount != subMod.FileData.Count)
ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1);
}
public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> swaps)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.FileSwapData.SetEquals(swaps))
return;
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.FileSwapData = swaps;
ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1);
}
public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message)
{
var path = newName.RemoveInvalidPathSymbols();
if (path.Length != 0
&& !mod.Groups.Any(o => !ReferenceEquals(o, group)
&& string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase)))
return true;
if (message)
Penumbra.ChatService.NotificationMessage(
$"Could not name option {newName} because option with same filename {path} already exists.",
"Warning", NotificationType.Warning);
return false;
}
private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx)
{
if (groupIdx == -1 && optionIdx == 0)
return mod._default;
return mod._groups[groupIdx] switch
{
SingleModGroup s => s.OptionData[optionIdx],
MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod,
_ => throw new InvalidOperationException(),
};
}
private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2)
{
if (type == ModOptionChangeType.PrepareChange)
return;
// File deletion is handled in the actual function.
if (type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved)
{
mod.SaveAllGroups();
}
else
{
if (groupIdx == -1)
mod.SaveDefaultModDelayed();
else
IModGroup.SaveDelayed(mod._groups[groupIdx], mod.ModPath, groupIdx);
}
bool ComputeChangedItems()
{
mod.ComputeChangedItems();
return true;
}
// State can not change on adding groups, as they have no immediate options.
var unused = type switch
{
ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(),
ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(),
ModOptionChangeType.GroupMoved => false,
ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption),
ModOptionChangeType.PriorityChanged => false,
ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(),
ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(),
ModOptionChangeType.OptionMoved => false,
ModOptionChangeType.OptionFilesChanged => ComputeChangedItems()
& (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))),
ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems()
& (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))),
ModOptionChangeType.OptionMetaChanged => ComputeChangedItems()
& (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))),
ModOptionChangeType.DisplayChange => false,
_ => false,
};
UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo));
ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo);
}
}
private static void UpdateSubModPositions(Mod mod, int fromGroup)
{
foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup))
{
foreach (var (o, optionIdx) in group.OfType<SubMod>().WithIndex())
o.SetPosition(groupIdx, optionIdx);
}
}
public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription)
{
var group = mod._groups[groupIdx];
if (group.Description == newDescription)
return;
var _ = group switch
{
SingleModGroup s => s.Description = newDescription,
MultiModGroup m => m.Description = newDescription,
_ => newDescription,
};
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1);
}
public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription)
{
var group = mod._groups[groupIdx];
var option = group[optionIdx];
if (option.Description == newDescription || option is not SubMod s)
return;
s.Description = newDescription;
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
}
public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority)
{
var group = mod._groups[groupIdx];
if (group.Priority == newPriority)
return;
var _ = group switch
{
SingleModGroup s => s.Priority = newPriority,
MultiModGroup m => m.Priority = newPriority,
_ => newPriority,
};
ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1);
}
public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority)
{
switch (mod._groups[groupIdx])
{
case SingleModGroup:
ChangeGroupPriority(mod, groupIdx, newPriority);
break;
case MultiModGroup m:
if (m.PrioritizedOptions[optionIdx].Priority == newPriority)
return;
m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority);
ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1);
return;
}
}
public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName)
{
switch (mod._groups[groupIdx])
{
case SingleModGroup s:
if (s.OptionData[optionIdx].Name == newName)
return;
s.OptionData[optionIdx].Name = newName;
break;
case MultiModGroup m:
var option = m.PrioritizedOptions[optionIdx].Mod;
if (option.Name == newName)
return;
option.Name = newName;
break;
}
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
}
public void AddOption(Mod mod, int groupIdx, string newName)
{
var group = mod._groups[groupIdx];
var subMod = new SubMod(mod) { Name = newName };
subMod.SetPosition(groupIdx, group.Count);
switch (group)
{
case SingleModGroup s:
s.OptionData.Add(subMod);
break;
case MultiModGroup m:
m.PrioritizedOptions.Add((subMod, 0));
break;
}
ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
}
public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0)
{
if (option is not SubMod o)
return;
var group = mod._groups[groupIdx];
if (group.Count > 63)
{
Penumbra.Log.Error(
$"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, "
+ "since only up to 64 options are supported in one group.");
return;
}
o.SetPosition(groupIdx, group.Count);
switch (group)
{
case SingleModGroup s:
s.OptionData.Add(o);
break;
case MultiModGroup m:
m.PrioritizedOptions.Add((o, priority));
break;
}
ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
}
public void DeleteOption(Mod mod, int groupIdx, int optionIdx)
{
var group = mod._groups[groupIdx];
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
switch (group)
{
case SingleModGroup s:
s.OptionData.RemoveAt(optionIdx);
break;
case MultiModGroup m:
m.PrioritizedOptions.RemoveAt(optionIdx);
break;
}
group.UpdatePositions(optionIdx);
ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1);
}
public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo)
{
var group = mod._groups[groupIdx];
if (group.MoveOption(optionIdxFrom, optionIdxTo))
ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo);
}
public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet<MetaManipulation> manipulations)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.Manipulations.Count == manipulations.Count
&& subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m)))
return;
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.ManipulationData = manipulations;
ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1);
}
public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> replacements)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.FileData.SetEquals(replacements))
return;
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.FileData = replacements;
ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1);
}
public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> additions)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
var oldCount = subMod.FileData.Count;
subMod.FileData.AddFrom(additions);
if (oldCount != subMod.FileData.Count)
ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1);
}
public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> swaps)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.FileSwapData.SetEquals(swaps))
return;
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.FileSwapData = swaps;
ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1);
}
public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message)
{
var path = newName.RemoveInvalidPathSymbols();
if (path.Length != 0
&& !mod.Groups.Any(o => !ReferenceEquals(o, group)
&& string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase)))
return true;
if (message)
Penumbra.ChatService.NotificationMessage(
$"Could not name option {newName} because option with same filename {path} already exists.",
"Warning", NotificationType.Warning);
return false;
}
private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx)
{
if (groupIdx == -1 && optionIdx == 0)
return mod._default;
return mod._groups[groupIdx] switch
{
SingleModGroup s => s.OptionData[optionIdx],
MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod,
_ => throw new InvalidOperationException(),
};
}
private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2)
{
if (type == ModOptionChangeType.PrepareChange)
return;
// File deletion is handled in the actual function.
if (type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved)
{
mod.SaveAllGroups();
}
else
{
if (groupIdx == -1)
mod.SaveDefaultModDelayed();
else
IModGroup.SaveDelayed(mod._groups[groupIdx], mod.ModPath, groupIdx);
}
bool ComputeChangedItems()
{
mod.ComputeChangedItems();
return true;
}
// State can not change on adding groups, as they have no immediate options.
var unused = type switch
{
ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(),
ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(),
ModOptionChangeType.GroupMoved => false,
ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption),
ModOptionChangeType.PriorityChanged => false,
ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(),
ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(),
ModOptionChangeType.OptionMoved => false,
ModOptionChangeType.OptionFilesChanged => ComputeChangedItems()
& (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))),
ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems()
& (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))),
ModOptionChangeType.OptionMetaChanged => ComputeChangedItems()
& (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))),
ModOptionChangeType.DisplayChange => false,
_ => false,
};
}
}

View file

@ -6,169 +6,144 @@ using System.Threading.Tasks;
namespace Penumbra.Mods;
public sealed partial class Mod
public sealed partial class ModManager
{
public sealed partial class Manager
public DirectoryInfo BasePath { get; private set; } = null!;
private DirectoryInfo? _exportDirectory;
public DirectoryInfo ExportDirectory
=> _exportDirectory ?? BasePath;
public bool Valid { get; private set; }
public event Action? ModDiscoveryStarted;
public event Action? ModDiscoveryFinished;
public event Action<string, bool> ModDirectoryChanged;
// Change the mod base directory and discover available mods.
public void DiscoverMods(string newDir)
{
public DirectoryInfo BasePath { get; private set; } = null!;
private DirectoryInfo? _exportDirectory;
SetBaseDirectory(newDir, false);
DiscoverMods();
}
public DirectoryInfo ExportDirectory
=> _exportDirectory ?? BasePath;
// 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, Penumbra.Config.ModDirectory, StringComparison.OrdinalIgnoreCase))
return;
public bool Valid { get; private set; }
public event Action? ModDiscoveryStarted;
public event Action? ModDiscoveryFinished;
public event Action< string, bool > ModDirectoryChanged;
// Change the mod base directory and discover available mods.
public void DiscoverMods( string newDir )
if (newPath.Length == 0)
{
SetBaseDirectory( newDir, false );
DiscoverMods();
Valid = false;
BasePath = new DirectoryInfo(".");
if (Penumbra.Config.ModDirectory != BasePath.FullName)
ModDirectoryChanged.Invoke(string.Empty, false);
}
// 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 )
else
{
if( !firstTime && string.Equals( newPath, Penumbra.Config.ModDirectory, StringComparison.OrdinalIgnoreCase ) )
{
return;
}
if( newPath.Length == 0 )
{
Valid = false;
BasePath = new DirectoryInfo( "." );
if( Penumbra.Config.ModDirectory != BasePath.FullName )
{
ModDirectoryChanged.Invoke( 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( Penumbra.Config.ModDirectory != BasePath.FullName )
{
ModDirectoryChanged.Invoke( BasePath.FullName, Valid );
}
}
}
private static void OnModDirectoryChange( string newPath, bool _ )
{
Penumbra.Log.Information( $"Set new mod base directory from {Penumbra.Config.ModDirectory} to {newPath}." );
Penumbra.Config.ModDirectory = newPath;
Penumbra.Config.Save();
}
// Discover new mods.
public void DiscoverMods()
{
NewMods.Clear();
ModDiscoveryStarted?.Invoke();
_mods.Clear();
BasePath.Refresh();
if( Valid && BasePath.Exists )
{
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = Environment.ProcessorCount / 2,
};
var queue = new ConcurrentQueue< Mod >();
Parallel.ForEach( BasePath.EnumerateDirectories(), options, dir =>
{
var mod = LoadMod( this, dir, false );
if( mod != null )
{
queue.Enqueue( mod );
}
} );
foreach( var mod in queue )
{
mod.Index = _mods.Count;
_mods.Add( mod );
}
}
ModDiscoveryFinished?.Invoke();
Penumbra.Log.Information( "Rediscovered mods." );
if( MigrateModBackups )
{
ModBackup.MigrateZipToPmp( this );
}
}
public void UpdateExportDirectory( string newDirectory, bool change )
{
if( newDirectory.Length == 0 )
{
if( _exportDirectory == null )
{
return;
}
_exportDirectory = null;
_config.ExportDirectory = string.Empty;
_config.Save();
return;
}
var dir = new DirectoryInfo( newDirectory );
if( dir.FullName.Equals( _exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase ) )
{
return;
}
if( !dir.Exists )
{
var newDir = new DirectoryInfo(newPath);
if (!newDir.Exists)
try
{
Directory.CreateDirectory( dir.FullName );
Directory.CreateDirectory(newDir.FullName);
newDir.Refresh();
}
catch( Exception e )
catch (Exception e)
{
Penumbra.Log.Error( $"Could not create Export Directory:\n{e}" );
return;
Penumbra.Log.Error($"Could not create specified mod directory {newDir.FullName}:\n{e}");
}
}
if( change )
{
foreach( var mod in _mods )
{
new ModBackup( this, mod ).Move( dir.FullName );
}
}
_exportDirectory = dir;
if( change )
{
_config.ExportDirectory = dir.FullName;
_config.Save();
}
BasePath = newDir;
Valid = Directory.Exists(newDir.FullName);
if (Penumbra.Config.ModDirectory != BasePath.FullName)
ModDirectoryChanged.Invoke(BasePath.FullName, Valid);
}
}
}
private static void OnModDirectoryChange(string newPath, bool _)
{
Penumbra.Log.Information($"Set new mod base directory from {Penumbra.Config.ModDirectory} to {newPath}.");
Penumbra.Config.ModDirectory = newPath;
Penumbra.Config.Save();
}
// Discover new mods.
public void DiscoverMods()
{
NewMods.Clear();
ModDiscoveryStarted?.Invoke();
_mods.Clear();
BasePath.Refresh();
if (Valid && BasePath.Exists)
{
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = Environment.ProcessorCount / 2,
};
var queue = new ConcurrentQueue<Mod>();
Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir =>
{
var mod = Mod.LoadMod(this, dir, false);
if (mod != null)
queue.Enqueue(mod);
});
foreach (var mod in queue)
{
mod.Index = _mods.Count;
_mods.Add(mod);
}
}
ModDiscoveryFinished?.Invoke();
Penumbra.Log.Information("Rediscovered mods.");
if (MigrateModBackups)
ModBackup.MigrateZipToPmp(this);
}
public void UpdateExportDirectory(string newDirectory, bool change)
{
if (newDirectory.Length == 0)
{
if (_exportDirectory == null)
return;
_exportDirectory = null;
_config.ExportDirectory = string.Empty;
_config.Save();
return;
}
var dir = new DirectoryInfo(newDirectory);
if (dir.FullName.Equals(_exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase))
return;
if (!dir.Exists)
try
{
Directory.CreateDirectory(dir.FullName);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not create Export Directory:\n{e}");
return;
}
if (change)
foreach (var mod in _mods)
new ModBackup(this, mod).Move(dir.FullName);
_exportDirectory = dir;
if (change)
{
_config.ExportDirectory = dir.FullName;
_config.Save();
}
}
}

View file

@ -7,73 +7,70 @@ using Penumbra.Util;
namespace Penumbra.Mods;
public sealed partial class Mod
public sealed partial class ModManager : IReadOnlyList<Mod>
{
public sealed partial class Manager : IReadOnlyList<Mod>
// Set when reading Config and migrating from v4 to v5.
public static bool MigrateModBackups = false;
// 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<Mod> NewMods = new();
private readonly List<Mod> _mods = new();
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();
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
public readonly ModDataEditor DataEditor;
public ModManager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor)
{
// Set when reading Config and migrating from v4 to v5.
public static bool MigrateModBackups = false;
using var timer = time.Measure(StartTimeType.Mods);
_config = config;
_communicator = communicator;
DataEditor = dataEditor;
ModDirectoryChanged += OnModDirectoryChange;
SetBaseDirectory(config.ModDirectory, true);
UpdateExportDirectory(_config.ExportDirectory, false);
ModOptionChanged += OnModOptionChange;
ModPathChanged += OnModPathChange;
DiscoverMods();
}
// 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<Mod> NewMods = new();
private readonly List<Mod> _mods = new();
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();
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
public readonly ModDataEditor DataEditor;
public Manager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor)
// 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 modDirectory, string modName, [NotNullWhen(true)] out Mod? mod)
{
mod = null;
foreach (var m in _mods)
{
using var timer = time.Measure(StartTimeType.Mods);
_config = config;
_communicator = communicator;
DataEditor = dataEditor;
ModDirectoryChanged += OnModDirectoryChange;
SetBaseDirectory(config.ModDirectory, true);
UpdateExportDirectory(_config.ExportDirectory, false);
ModOptionChanged += OnModOptionChange;
ModPathChanged += OnModPathChange;
DiscoverMods();
}
// 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 modDirectory, string modName, [NotNullWhen(true)] out Mod? mod)
{
mod = null;
foreach (var m in _mods)
if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase))
{
if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase))
{
mod = m;
return true;
}
if (m.Name == modName)
mod ??= m;
mod = m;
return true;
}
return mod != null;
if (m.Name == modName)
mod ??= m;
}
return mod != null;
}
}

View file

@ -2,7 +2,6 @@ using System;
using System.IO;
using System.Linq;
using Dalamud.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.Services;

View file

@ -15,10 +15,10 @@ public enum ModPathChangeType
public partial class Mod
{
public DirectoryInfo ModPath { get; private set; }
public DirectoryInfo ModPath { get; internal set; }
public string Identifier
=> Index >= 0 ? ModPath.Name : Name;
public int Index { get; private set; } = -1;
public int Index { get; internal set; } = -1;
public bool IsTemporary
=> Index < 0;
@ -33,7 +33,7 @@ public partial class Mod
_default = new SubMod( this );
}
private static Mod? LoadMod( Manager modManager, DirectoryInfo modPath, bool incorporateMetaChanges )
public static Mod? LoadMod( ModManager modManager, DirectoryInfo modPath, bool incorporateMetaChanges )
{
modPath.Refresh();
if( !modPath.Exists )
@ -52,7 +52,7 @@ public partial class Mod
}
internal bool Reload(Manager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange )
internal bool Reload(ModManager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange )
{
modDataChange = ModDataChangeType.Deletion;
ModPath.Refresh();

View file

@ -12,7 +12,7 @@ public sealed partial class Mod
public SortedList< string, object? > ChangedItems { get; } = new();
public string LowerChangedItemsString { get; private set; } = string.Empty;
private void ComputeChangedItems()
internal void ComputeChangedItems()
{
ChangedItems.Clear();
foreach( var gamePath in AllRedirects )

View file

@ -18,15 +18,15 @@ public partial class Mod
public IReadOnlyList< IModGroup > Groups
=> _groups;
private readonly SubMod _default;
private readonly List< IModGroup > _groups = new();
internal readonly SubMod _default;
internal readonly List< IModGroup > _groups = new();
public int TotalFileCount { get; private set; }
public int TotalSwapCount { get; private set; }
public int TotalManipulations { get; private set; }
public bool HasOptions { get; private 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; }
private bool SetCounts()
internal bool SetCounts()
{
TotalFileCount = 0;
TotalSwapCount = 0;
@ -120,7 +120,7 @@ public partial class Mod
// Delete all existing group files and save them anew.
// Used when indices change in complex ways.
private void SaveAllGroups()
internal void SaveAllGroups()
{
foreach( var file in GroupFiles )
{

View file

@ -12,12 +12,12 @@ namespace Penumbra.Mods;
public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
{
private readonly Mod.Manager _modManager;
private readonly ModManager _modManager;
private readonly CommunicatorService _communicator;
private readonly FilenameService _files;
// Create a new ModFileSystem from the currently loaded mods and the current sort order file.
public ModFileSystem(Mod.Manager modManager, CommunicatorService communicator, FilenameService files)
public ModFileSystem(ModManager modManager, CommunicatorService communicator, FilenameService files)
{
_modManager = modManager;
_communicator = communicator;

View file

@ -15,103 +15,105 @@ namespace Penumbra.Mods;
public partial class Mod
{
// Groups that allow all available options to be selected at once.
private sealed class MultiModGroup : IModGroup
}
/// <summary> Groups that allow all available options to be selected at once. </summary>
public sealed class MultiModGroup : IModGroup
{
public GroupType Type
=> GroupType.Multi;
public string Name { get; set; } = "Group";
public string Description { get; set; } = "A non-exclusive group of settings.";
public int Priority { get; set; }
public uint DefaultSettings { get; set; }
public int OptionPriority(Index idx)
=> PrioritizedOptions[idx].Priority;
public ISubMod this[Index idx]
=> PrioritizedOptions[idx].Mod;
[JsonIgnore]
public int Count
=> PrioritizedOptions.Count;
public readonly List<(SubMod Mod, int Priority)> PrioritizedOptions = new();
public IEnumerator<ISubMod> GetEnumerator()
=> PrioritizedOptions.Select(o => o.Mod).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx)
{
public GroupType Type
=> GroupType.Multi;
public string Name { get; set; } = "Group";
public string Description { get; set; } = "A non-exclusive group of settings.";
public int Priority { get; set; }
public uint DefaultSettings { get; set; }
public int OptionPriority(Index idx)
=> PrioritizedOptions[idx].Priority;
public ISubMod this[Index idx]
=> PrioritizedOptions[idx].Mod;
[JsonIgnore]
public int Count
=> PrioritizedOptions.Count;
public readonly List<(SubMod Mod, int Priority)> PrioritizedOptions = new();
public IEnumerator<ISubMod> GetEnumerator()
=> PrioritizedOptions.Select(o => o.Mod).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx)
var ret = new MultiModGroup()
{
var ret = new MultiModGroup()
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
Priority = json[nameof(Priority)]?.ToObject<int>() ?? 0,
DefaultSettings = json[nameof(DefaultSettings)]?.ToObject<uint>() ?? 0,
};
if (ret.Name.Length == 0)
return null;
var options = json["Options"];
if (options != null)
foreach (var child in options.Children())
{
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
Priority = json[nameof(Priority)]?.ToObject<int>() ?? 0,
DefaultSettings = json[nameof(DefaultSettings)]?.ToObject<uint>() ?? 0,
};
if (ret.Name.Length == 0)
return null;
var options = json["Options"];
if (options != null)
foreach (var child in options.Children())
if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions)
{
if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions)
{
Penumbra.ChatService.NotificationMessage(
$"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning",
NotificationType.Warning);
break;
}
var subMod = new SubMod(mod);
subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count);
subMod.Load(mod.ModPath, child, out var priority);
ret.PrioritizedOptions.Add((subMod, priority));
Penumbra.ChatService.NotificationMessage(
$"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning",
NotificationType.Warning);
break;
}
ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1));
return ret;
}
public IModGroup Convert(GroupType type)
{
switch (type)
{
case GroupType.Multi: return this;
case GroupType.Single:
var multi = new SingleModGroup()
{
Name = Name,
Description = Description,
Priority = Priority,
DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0),
};
multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod));
return multi;
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
var subMod = new SubMod(mod);
subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count);
subMod.Load(mod.ModPath, child, out var priority);
ret.PrioritizedOptions.Add((subMod, priority));
}
}
public bool MoveOption(int optionIdxFrom, int optionIdxTo)
ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1));
return ret;
}
public IModGroup Convert(GroupType type)
{
switch (type)
{
if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo))
return false;
DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo);
UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo));
return true;
}
public void UpdatePositions(int from = 0)
{
foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from))
o.SetPosition(o.GroupIdx, i);
case GroupType.Multi: return this;
case GroupType.Single:
var multi = new SingleModGroup()
{
Name = Name,
Description = Description,
Priority = Priority,
DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0),
};
multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod));
return multi;
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
public bool MoveOption(int optionIdxFrom, int optionIdxTo)
{
if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo))
return false;
DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo);
UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo));
return true;
}
public void UpdatePositions(int from = 0)
{
foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from))
o.SetPosition(o.GroupIdx, i);
}
}

View file

@ -10,122 +10,119 @@ using Penumbra.Api.Enums;
namespace Penumbra.Mods;
public partial class Mod
/// <summary> Groups that allow only one of their available options to be selected. </summary>
public sealed class SingleModGroup : IModGroup
{
// Groups that allow only one of their available options to be selected.
private sealed class SingleModGroup : IModGroup
public GroupType Type
=> GroupType.Single;
public string Name { get; set; } = "Option";
public string Description { get; set; } = "A mutually exclusive group of settings.";
public int Priority { get; set; }
public uint DefaultSettings { get; set; }
public readonly List< SubMod > OptionData = new();
public int OptionPriority( Index _ )
=> Priority;
public ISubMod this[ Index idx ]
=> OptionData[ idx ];
[JsonIgnore]
public int Count
=> OptionData.Count;
public IEnumerator< ISubMod > GetEnumerator()
=> OptionData.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public static SingleModGroup? Load( Mod mod, JObject json, int groupIdx )
{
public GroupType Type
=> GroupType.Single;
public string Name { get; set; } = "Option";
public string Description { get; set; } = "A mutually exclusive group of settings.";
public int Priority { get; set; }
public uint DefaultSettings { get; set; }
public readonly List< SubMod > OptionData = new();
public int OptionPriority( Index _ )
=> Priority;
public ISubMod this[ Index idx ]
=> OptionData[ idx ];
[JsonIgnore]
public int Count
=> OptionData.Count;
public IEnumerator< ISubMod > GetEnumerator()
=> OptionData.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public static SingleModGroup? Load( Mod mod, JObject json, int groupIdx )
var options = json[ "Options" ];
var ret = new SingleModGroup
{
var options = json[ "Options" ];
var ret = new SingleModGroup
{
Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty,
Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty,
Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0,
DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0u,
};
if( ret.Name.Length == 0 )
{
return null;
}
Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty,
Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty,
Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0,
DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0u,
};
if( ret.Name.Length == 0 )
{
return null;
}
if( options != null )
if( options != null )
{
foreach( var child in options.Children() )
{
foreach( var child in options.Children() )
var subMod = new SubMod( mod );
subMod.SetPosition( groupIdx, ret.OptionData.Count );
subMod.Load( mod.ModPath, child, out _ );
ret.OptionData.Add( subMod );
}
}
if( ( int )ret.DefaultSettings >= ret.Count )
ret.DefaultSettings = 0;
return ret;
}
public IModGroup Convert( GroupType type )
{
switch( type )
{
case GroupType.Single: return this;
case GroupType.Multi:
var multi = new MultiModGroup()
{
var subMod = new SubMod( mod );
subMod.SetPosition( groupIdx, ret.OptionData.Count );
subMod.Load( mod.ModPath, child, out _ );
ret.OptionData.Add( subMod );
}
}
Name = Name,
Description = Description,
Priority = Priority,
DefaultSettings = 1u << ( int )DefaultSettings,
};
multi.PrioritizedOptions.AddRange( OptionData.Select( ( o, i ) => ( o, i ) ) );
return multi;
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
}
}
if( ( int )ret.DefaultSettings >= ret.Count )
ret.DefaultSettings = 0;
return ret;
public bool MoveOption( int optionIdxFrom, int optionIdxTo )
{
if( !OptionData.Move( optionIdxFrom, optionIdxTo ) )
{
return false;
}
public IModGroup Convert( GroupType type )
// Update default settings with the move.
if( DefaultSettings == optionIdxFrom )
{
switch( type )
DefaultSettings = ( uint )optionIdxTo;
}
else if( optionIdxFrom < optionIdxTo )
{
if( DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo )
{
case GroupType.Single: return this;
case GroupType.Multi:
var multi = new MultiModGroup()
{
Name = Name,
Description = Description,
Priority = Priority,
DefaultSettings = 1u << ( int )DefaultSettings,
};
multi.PrioritizedOptions.AddRange( OptionData.Select( ( o, i ) => ( o, i ) ) );
return multi;
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
--DefaultSettings;
}
}
public bool MoveOption( int optionIdxFrom, int optionIdxTo )
else if( DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo )
{
if( !OptionData.Move( optionIdxFrom, optionIdxTo ) )
{
return false;
}
// Update default settings with the move.
if( DefaultSettings == optionIdxFrom )
{
DefaultSettings = ( uint )optionIdxTo;
}
else if( optionIdxFrom < optionIdxTo )
{
if( DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo )
{
--DefaultSettings;
}
}
else if( DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo )
{
++DefaultSettings;
}
UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) );
return true;
++DefaultSettings;
}
public void UpdatePositions( int from = 0 )
UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) );
return true;
}
public void UpdatePositions( int from = 0 )
{
foreach( var (o, i) in OptionData.WithIndex().Skip( from ) )
{
foreach( var (o, i) in OptionData.WithIndex().Skip( from ) )
{
o.SetPosition( o.GroupIdx, i );
}
o.SetPosition( o.GroupIdx, i );
}
}
}

View file

@ -36,7 +36,7 @@ public partial class Mod
ISubMod.WriteSubMod( j, serializer, _default, ModPath, 0 );
}
private void SaveDefaultModDelayed()
internal void SaveDefaultModDelayed()
=> Penumbra.Framework.RegisterDelayed( nameof( SaveDefaultMod ) + ModPath.Name, SaveDefaultMod );
private void LoadDefaultOption()
@ -92,233 +92,237 @@ public partial class Mod
}
// A sub mod is a collection of
// - file replacements
// - file swaps
// - meta manipulations
// that can be used either as an option or as the default data for a mod.
// It can be loaded and reloaded from Json.
// Nothing is checked for existence or validity when loading.
// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides.
public sealed class SubMod : ISubMod
}
/// <summary>
/// A sub mod is a collection of
/// - file replacements
/// - file swaps
/// - meta manipulations
/// that can be used either as an option or as the default data for a mod.
/// It can be loaded and reloaded from Json.
/// Nothing is checked for existence or validity when loading.
/// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides.
/// </summary>
public sealed class SubMod : ISubMod
{
public string Name { get; set; } = "Default";
public string FullName
=> GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}";
public string Description { get; set; } = string.Empty;
internal IMod ParentMod { get; private init; }
internal int GroupIdx { get; private set; }
internal int OptionIdx { get; private set; }
public bool IsDefault
=> GroupIdx < 0;
public Dictionary< Utf8GamePath, FullPath > FileData = new();
public Dictionary< Utf8GamePath, FullPath > FileSwapData = new();
public HashSet< MetaManipulation > ManipulationData = new();
public SubMod( IMod parentMod )
=> ParentMod = parentMod;
public IReadOnlyDictionary< Utf8GamePath, FullPath > Files
=> FileData;
public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps
=> FileSwapData;
public IReadOnlySet< MetaManipulation > Manipulations
=> ManipulationData;
public void SetPosition( int groupIdx, int optionIdx )
{
public string Name { get; set; } = "Default";
GroupIdx = groupIdx;
OptionIdx = optionIdx;
}
public string FullName
=> GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}";
public void Load( DirectoryInfo basePath, JToken json, out int priority )
{
FileData.Clear();
FileSwapData.Clear();
ManipulationData.Clear();
public string Description { get; set; } = string.Empty;
// Every option has a name, but priorities are only relevant for multi group options.
Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty;
Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty;
priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0;
internal IMod ParentMod { get; private init; }
internal int GroupIdx { get; private set; }
internal int OptionIdx { get; private set; }
public bool IsDefault
=> GroupIdx < 0;
public Dictionary< Utf8GamePath, FullPath > FileData = new();
public Dictionary< Utf8GamePath, FullPath > FileSwapData = new();
public HashSet< MetaManipulation > ManipulationData = new();
public SubMod( IMod parentMod )
=> ParentMod = parentMod;
public IReadOnlyDictionary< Utf8GamePath, FullPath > Files
=> FileData;
public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps
=> FileSwapData;
public IReadOnlySet< MetaManipulation > Manipulations
=> ManipulationData;
public void SetPosition( int groupIdx, int optionIdx )
var files = ( JObject? )json[ nameof( Files ) ];
if( files != null )
{
GroupIdx = groupIdx;
OptionIdx = optionIdx;
}
public void Load( DirectoryInfo basePath, JToken json, out int priority )
{
FileData.Clear();
FileSwapData.Clear();
ManipulationData.Clear();
// Every option has a name, but priorities are only relevant for multi group options.
Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty;
Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty;
priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0;
var files = ( JObject? )json[ nameof( Files ) ];
if( files != null )
foreach( var property in files.Properties() )
{
foreach( var property in files.Properties() )
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
{
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
{
FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) );
}
}
}
var swaps = ( JObject? )json[ nameof( FileSwaps ) ];
if( swaps != null )
{
foreach( var property in swaps.Properties() )
{
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
{
FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) );
}
}
}
var manips = json[ nameof( Manipulations ) ];
if( manips != null )
{
foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) )
{
ManipulationData.Add( s );
FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) );
}
}
}
// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod.
// If delete is true, the files are deleted afterwards.
public (bool Changes, List< string > DeleteList) IncorporateMetaChanges( DirectoryInfo basePath, bool delete )
var swaps = ( JObject? )json[ nameof( FileSwaps ) ];
if( swaps != null )
{
var deleteList = new List< string >();
var oldSize = ManipulationData.Count;
var deleteString = delete ? "with deletion." : "without deletion.";
foreach( var (key, file) in Files.ToList() )
foreach( var property in swaps.Properties() )
{
var ext1 = key.Extension().AsciiToLower().ToString();
var ext2 = file.Extension.ToLowerInvariant();
try
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
{
if( ext1 == ".meta" || ext2 == ".meta" )
{
FileData.Remove( key );
if( !file.Exists )
{
continue;
}
var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" );
deleteList.Add( file.FullName );
ManipulationData.UnionWith( meta.MetaManipulations );
}
else if( ext1 == ".rgsp" || ext2 == ".rgsp" )
{
FileData.Remove( key );
if( !file.Exists )
{
continue;
}
var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" );
deleteList.Add( file.FullName );
ManipulationData.UnionWith( rgsp.MetaManipulations );
}
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" );
}
}
DeleteDeleteList( deleteList, delete );
return ( oldSize < ManipulationData.Count, deleteList );
}
internal static void DeleteDeleteList( IEnumerable< string > deleteList, bool delete )
{
if( !delete )
{
return;
}
foreach( var file in deleteList )
{
try
{
File.Delete( file );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" );
FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) );
}
}
}
public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false )
var manips = json[ nameof( Manipulations ) ];
if( manips != null )
{
var files = TexToolsMeta.ConvertToTexTools( Manipulations );
foreach( var (file, data) in files )
foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) )
{
var path = Path.Combine( basePath.FullName, file );
try
{
Directory.CreateDirectory( Path.GetDirectoryName( path )! );
File.WriteAllBytes( path, data );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" );
}
}
if( test )
{
TestMetaWriting( files );
}
}
[Conditional( "DEBUG" )]
private void TestMetaWriting( Dictionary< string, byte[] > files )
{
var meta = new HashSet< MetaManipulation >( Manipulations.Count );
foreach( var (file, data) in files )
{
try
{
var x = file.EndsWith( "rgsp" )
? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges )
: new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges );
meta.UnionWith( x.MetaManipulations );
}
catch
{
// ignored
}
}
if( !Manipulations.SetEquals( meta ) )
{
Penumbra.Log.Information( "Meta Sets do not equal." );
foreach( var (m1, m2) in Manipulations.Zip( meta ) )
{
Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" );
}
foreach( var m in Manipulations.Skip( meta.Count ) )
{
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
}
foreach( var m in meta.Skip( Manipulations.Count ) )
{
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
}
}
else
{
Penumbra.Log.Information( "Meta Sets are equal." );
ManipulationData.Add( s );
}
}
}
// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod.
// If delete is true, the files are deleted afterwards.
public (bool Changes, List< string > DeleteList) IncorporateMetaChanges( DirectoryInfo basePath, bool delete )
{
var deleteList = new List< string >();
var oldSize = ManipulationData.Count;
var deleteString = delete ? "with deletion." : "without deletion.";
foreach( var (key, file) in Files.ToList() )
{
var ext1 = key.Extension().AsciiToLower().ToString();
var ext2 = file.Extension.ToLowerInvariant();
try
{
if( ext1 == ".meta" || ext2 == ".meta" )
{
FileData.Remove( key );
if( !file.Exists )
{
continue;
}
var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" );
deleteList.Add( file.FullName );
ManipulationData.UnionWith( meta.MetaManipulations );
}
else if( ext1 == ".rgsp" || ext2 == ".rgsp" )
{
FileData.Remove( key );
if( !file.Exists )
{
continue;
}
var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" );
deleteList.Add( file.FullName );
ManipulationData.UnionWith( rgsp.MetaManipulations );
}
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" );
}
}
DeleteDeleteList( deleteList, delete );
return ( oldSize < ManipulationData.Count, deleteList );
}
internal static void DeleteDeleteList( IEnumerable< string > deleteList, bool delete )
{
if( !delete )
{
return;
}
foreach( var file in deleteList )
{
try
{
File.Delete( file );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" );
}
}
}
public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false )
{
var files = TexToolsMeta.ConvertToTexTools( Manipulations );
foreach( var (file, data) in files )
{
var path = Path.Combine( basePath.FullName, file );
try
{
Directory.CreateDirectory( Path.GetDirectoryName( path )! );
File.WriteAllBytes( path, data );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" );
}
}
if( test )
{
TestMetaWriting( files );
}
}
[Conditional("DEBUG" )]
private void TestMetaWriting( Dictionary< string, byte[] > files )
{
var meta = new HashSet< MetaManipulation >( Manipulations.Count );
foreach( var (file, data) in files )
{
try
{
var x = file.EndsWith( "rgsp" )
? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges )
: new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges );
meta.UnionWith( x.MetaManipulations );
}
catch
{
// ignored
}
}
if( !Manipulations.SetEquals( meta ) )
{
Penumbra.Log.Information( "Meta Sets do not equal." );
foreach( var (m1, m2) in Manipulations.Zip( meta ) )
{
Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" );
}
foreach( var m in Manipulations.Skip( meta.Count ) )
{
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
}
foreach( var m in meta.Skip( Manipulations.Count ) )
{
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
}
}
else
{
Penumbra.Log.Information( "Meta Sets are equal." );
}
}
}

View file

@ -10,7 +10,7 @@ using Penumbra.String.Classes;
namespace Penumbra.Mods;
// Contains the settings for a given mod.
/// <summary> Contains the settings for a given mod. </summary>
public class ModSettings
{
public static readonly ModSettings Empty = new();

View file

@ -27,10 +27,10 @@ public class TemporaryMod : IMod
public IEnumerable< ISubMod > AllSubMods
=> new[] { Default };
private readonly Mod.SubMod _default;
private readonly SubMod _default;
public TemporaryMod()
=> _default = new Mod.SubMod( this );
=> _default = new SubMod( this );
public void SetFile( Utf8GamePath gamePath, FullPath fullPath )
=> _default.FileData[ gamePath ] = fullPath;
@ -44,7 +44,7 @@ public class TemporaryMod : IMod
_default.ManipulationData = manips;
}
public static void SaveTempCollection( Mod.Manager modManager, ModCollection collection, string? character = null )
public static void SaveTempCollection( ModManager modManager, ModCollection collection, string? character = null )
{
DirectoryInfo? dir = null;
try
@ -54,7 +54,7 @@ public class TemporaryMod : IMod
modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor,
$"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null );
var mod = new Mod( dir );
var defaultMod = (Mod.SubMod) mod.Default;
var defaultMod = (SubMod) mod.Default;
foreach( var (gamePath, fullPath) in collection.ResolvedFiles )
{
if( gamePath.Path.EndsWith( ".imc"u8 ) )