mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-21 15:27:51 +01:00
Move Mod.Manager and ModCollection.Manager to outer scope and required changes.
This commit is contained in:
parent
ccdafcf85d
commit
1253079968
59 changed files with 2562 additions and 2615 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public sealed partial class Mod
|
||||
{
|
||||
public partial class Manager
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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." );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 ) )
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue