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

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

View file

@ -38,7 +38,7 @@ public class IpcTester : IDisposable
private readonly ModSettings _modSettings; private readonly ModSettings _modSettings;
private readonly Temporary _temporary; private readonly Temporary _temporary;
public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, Mod.Manager modManager) public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, ModManager modManager)
{ {
_ipcProviders = ipcProviders; _ipcProviders = ipcProviders;
_pluginState = new PluginState(pi); _pluginState = new PluginState(pi);
@ -1139,9 +1139,9 @@ public class IpcTester : IDisposable
private class Temporary private class Temporary
{ {
private readonly DalamudPluginInterface _pi; private readonly DalamudPluginInterface _pi;
private readonly Mod.Manager _modManager; private readonly ModManager _modManager;
public Temporary(DalamudPluginInterface pi, Mod.Manager modManager) public Temporary(DalamudPluginInterface pi, ModManager modManager)
{ {
_pi = pi; _pi = pi;
_modManager = modManager; _modManager = modManager;

View file

@ -93,10 +93,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi
private Penumbra _penumbra; private Penumbra _penumbra;
private Lumina.GameData? _lumina; private Lumina.GameData? _lumina;
private Mod.Manager _modManager; private ModManager _modManager;
private ResourceLoader _resourceLoader; private ResourceLoader _resourceLoader;
private Configuration _config; private Configuration _config;
private ModCollection.Manager _collectionManager; private CollectionManager _collectionManager;
private DalamudServices _dalamud; private DalamudServices _dalamud;
private TempCollectionManager _tempCollections; private TempCollectionManager _tempCollections;
private TempModManager _tempMods; private TempModManager _tempMods;
@ -104,8 +104,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi
private CollectionResolver _collectionResolver; private CollectionResolver _collectionResolver;
private CutsceneService _cutsceneService; private CutsceneService _cutsceneService;
public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, Mod.Manager modManager, ResourceLoader resourceLoader, public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, ModManager modManager, ResourceLoader resourceLoader,
Configuration config, ModCollection.Manager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections,
TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService) TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService)
{ {
_communicator = communicator; _communicator = communicator;
@ -1021,7 +1021,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
// Resolve a path given by string for a specific collection. // Resolve a path given by string for a specific collection.
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private string ResolvePath(string path, Mod.Manager _, ModCollection collection) private string ResolvePath(string path, ModManager _, ModCollection collection)
{ {
if (!_config.EnableMods) if (!_config.EnableMods)
return path; return path;

View file

@ -112,7 +112,7 @@ public class PenumbraIpcProviders : IDisposable
internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll; internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll;
internal readonly FuncProvider< string, string, int, PenumbraApiEc > RemoveTemporaryMod; internal readonly FuncProvider< string, string, int, PenumbraApiEc > RemoveTemporaryMod;
public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api, Mod.Manager modManager ) public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager )
{ {
Api = api; Api = api;

View file

@ -15,408 +15,405 @@ using Penumbra.Util;
namespace Penumbra.Collections; namespace Penumbra.Collections;
public partial class ModCollection public sealed partial class CollectionManager : ISavable
{ {
public sealed partial class Manager : ISavable public const int Version = 1;
// The collection currently selected for changing settings.
public ModCollection Current { get; private set; } = ModCollection.Empty;
// The collection currently selected is in use either as an active collection or through inheritance.
public bool CurrentCollectionInUse { get; private set; }
// The collection used for general file redirections and all characters not specifically named.
public ModCollection Default { get; private set; } = ModCollection.Empty;
// The collection used for all files categorized as UI files.
public ModCollection Interface { get; private set; } = ModCollection.Empty;
// A single collection that can not be deleted as a fallback for the current collection.
private ModCollection DefaultName { get; set; } = ModCollection.Empty;
// The list of character collections.
public readonly IndividualCollections Individuals;
public ModCollection Individual(ActorIdentifier identifier)
=> Individuals.TryGetCollection(identifier, out var c) ? c : Default;
// Special Collections
private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues<Api.Enums.ApiCollectionType>().Length - 3];
// Return the configured collection for the given type or null.
// Does not handle Inactive, use ByName instead.
public ModCollection? ByType(CollectionType type)
=> ByType(type, ActorIdentifier.Invalid);
public ModCollection? ByType(CollectionType type, ActorIdentifier identifier)
{ {
public const int Version = 1; if (type.IsSpecial())
return _specialCollections[(int)type];
// The collection currently selected for changing settings. return type switch
public ModCollection Current { get; private set; } = Empty;
// The collection currently selected is in use either as an active collection or through inheritance.
public bool CurrentCollectionInUse { get; private set; }
// The collection used for general file redirections and all characters not specifically named.
public ModCollection Default { get; private set; } = Empty;
// The collection used for all files categorized as UI files.
public ModCollection Interface { get; private set; } = Empty;
// A single collection that can not be deleted as a fallback for the current collection.
private ModCollection DefaultName { get; set; } = Empty;
// The list of character collections.
public readonly IndividualCollections Individuals;
public ModCollection Individual(ActorIdentifier identifier)
=> Individuals.TryGetCollection(identifier, out var c) ? c : Default;
// Special Collections
private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues<Api.Enums.ApiCollectionType>().Length - 3];
// Return the configured collection for the given type or null.
// Does not handle Inactive, use ByName instead.
public ModCollection? ByType(CollectionType type)
=> ByType(type, ActorIdentifier.Invalid);
public ModCollection? ByType(CollectionType type, ActorIdentifier identifier)
{ {
if (type.IsSpecial()) CollectionType.Default => Default,
return _specialCollections[(int)type]; CollectionType.Interface => Interface,
CollectionType.Current => Current,
CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue(identifier, out var c) ? c : null,
_ => null,
};
}
return type switch // Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections.
{ private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1)
CollectionType.Default => Default, {
CollectionType.Interface => Interface, var oldCollectionIdx = collectionType switch
CollectionType.Current => Current,
CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue(identifier, out var c) ? c : null,
_ => null,
};
}
// Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections.
private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1)
{ {
var oldCollectionIdx = collectionType switch CollectionType.Default => Default.Index,
{ CollectionType.Interface => Interface.Index,
CollectionType.Default => Default.Index, CollectionType.Current => Current.Index,
CollectionType.Interface => Interface.Index, CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count
CollectionType.Current => Current.Index, ? -1
CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count : Individuals[individualIndex].Collection.Index,
? -1 _ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index,
: Individuals[individualIndex].Collection.Index, _ => -1,
_ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index, };
_ => -1,
};
if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx) if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx)
return; return;
var newCollection = this[newIdx]; var newCollection = this[newIdx];
if (newIdx > Empty.Index) if (newIdx > ModCollection.Empty.Index)
newCollection.CreateCache(collectionType is CollectionType.Default); newCollection.CreateCache(collectionType is CollectionType.Default);
switch (collectionType) switch (collectionType)
{
case CollectionType.Default:
Default = newCollection;
break;
case CollectionType.Interface:
Interface = newCollection;
break;
case CollectionType.Current:
Current = newCollection;
break;
case CollectionType.Individual:
if (!Individuals.ChangeCollection(individualIndex, newCollection))
{
RemoveCache(newIdx);
return;
}
break;
default:
_specialCollections[(int)collectionType] = newCollection;
break;
}
RemoveCache(oldCollectionIdx);
UpdateCurrentCollectionInUse();
_communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection,
collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty);
}
private void UpdateCurrentCollectionInUse()
=> CurrentCollectionInUse = _specialCollections
.OfType<ModCollection>()
.Prepend(Interface)
.Prepend(Default)
.Concat(Individuals.Assignments.Select(kvp => kvp.Collection))
.SelectMany(c => c.GetFlattenedInheritance()).Contains(Current);
public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1)
=> SetCollection(collection.Index, collectionType, individualIndex);
// Create a special collection if it does not exist and set it to Empty.
public bool CreateSpecialCollection(CollectionType collectionType)
{ {
if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null) case CollectionType.Default:
return false; Default = newCollection;
break;
_specialCollections[(int)collectionType] = Default; case CollectionType.Interface:
_communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty); Interface = newCollection;
return true; break;
} case CollectionType.Current:
Current = newCollection;
// Remove a special collection if it exists break;
public void RemoveSpecialCollection(CollectionType collectionType) case CollectionType.Individual:
{ if (!Individuals.ChangeCollection(individualIndex, newCollection))
if (!collectionType.IsSpecial())
return;
var old = _specialCollections[(int)collectionType];
if (old != null)
{
_specialCollections[(int)collectionType] = null;
_communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty);
}
}
// Wrappers around Individual Collection handling.
public void CreateIndividualCollection(params ActorIdentifier[] identifiers)
{
if (Individuals.Add(identifiers, Default))
_communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName);
}
public void RemoveIndividualCollection(int individualIndex)
{
if (individualIndex < 0 || individualIndex >= Individuals.Count)
return;
var (name, old) = Individuals[individualIndex];
if (Individuals.Delete(individualIndex))
_communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name);
}
public void MoveIndividualCollection(int from, int to)
{
if (Individuals.Move(from, to))
Penumbra.SaveService.QueueSave(this);
}
// Obtain the index of a collection by name.
private int GetIndexForCollectionName(string name)
=> name.Length == 0 ? Empty.Index : _collections.IndexOf(c => c.Name == name);
// Load default, current, special, and character collections from config.
// Then create caches. If a collection does not exist anymore, reset it to an appropriate default.
private void LoadCollections(FilenameService files)
{
var configChanged = !ReadActiveCollections(files, out var jObject);
// Load the default collection.
var defaultName = jObject[nameof(Default)]?.ToObject<string>() ?? (configChanged ? DefaultCollection : Empty.Name);
var defaultIdx = GetIndexForCollectionName(defaultName);
if (defaultIdx < 0)
{
Penumbra.ChatService.NotificationMessage(
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure",
NotificationType.Warning);
Default = Empty;
configChanged = true;
}
else
{
Default = this[defaultIdx];
}
// Load the interface collection.
var interfaceName = jObject[nameof(Interface)]?.ToObject<string>() ?? Default.Name;
var interfaceIdx = GetIndexForCollectionName(interfaceName);
if (interfaceIdx < 0)
{
Penumbra.ChatService.NotificationMessage(
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.",
"Load Failure", NotificationType.Warning);
Interface = Empty;
configChanged = true;
}
else
{
Interface = this[interfaceIdx];
}
// Load the current collection.
var currentName = jObject[nameof(Current)]?.ToObject<string>() ?? DefaultCollection;
var currentIdx = GetIndexForCollectionName(currentName);
if (currentIdx < 0)
{
Penumbra.ChatService.NotificationMessage(
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.",
"Load Failure", NotificationType.Warning);
Current = DefaultName;
configChanged = true;
}
else
{
Current = this[currentIdx];
}
// Load special collections.
foreach (var (type, name, _) in CollectionTypeExtensions.Special)
{
var typeName = jObject[type.ToString()]?.ToObject<string>();
if (typeName != null)
{ {
var idx = GetIndexForCollectionName(typeName); RemoveCache(newIdx);
if (idx < 0) return;
{
Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.",
"Load Failure",
NotificationType.Warning);
configChanged = true;
}
else
{
_specialCollections[(int)type] = this[idx];
}
} }
}
configChanged |= MigrateIndividualCollections(jObject); break;
configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this); default:
_specialCollections[(int)collectionType] = newCollection;
// Save any changes and create all required caches. break;
if (configChanged)
Penumbra.SaveService.ImmediateSave(this);
} }
// Migrate ungendered collections to Male and Female for 0.5.9.0. RemoveCache(oldCollectionIdx);
public static void MigrateUngenderedCollections(FilenameService fileNames)
UpdateCurrentCollectionInUse();
_communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection,
collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty);
}
private void UpdateCurrentCollectionInUse()
=> CurrentCollectionInUse = _specialCollections
.OfType<ModCollection>()
.Prepend(Interface)
.Prepend(Default)
.Concat(Individuals.Assignments.Select(kvp => kvp.Collection))
.SelectMany(c => c.GetFlattenedInheritance()).Contains(Current);
public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1)
=> SetCollection(collection.Index, collectionType, individualIndex);
// Create a special collection if it does not exist and set it to Empty.
public bool CreateSpecialCollection(CollectionType collectionType)
{
if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null)
return false;
_specialCollections[(int)collectionType] = Default;
_communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty);
return true;
}
// Remove a special collection if it exists
public void RemoveSpecialCollection(CollectionType collectionType)
{
if (!collectionType.IsSpecial())
return;
var old = _specialCollections[(int)collectionType];
if (old != null)
{ {
if (!ReadActiveCollections(fileNames, out var jObject)) _specialCollections[(int)collectionType] = null;
return; _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty);
}
}
foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) // Wrappers around Individual Collection handling.
{ public void CreateIndividualCollection(params ActorIdentifier[] identifiers)
var oldName = type.ToString()[4..]; {
var value = jObject[oldName]; if (Individuals.Add(identifiers, Default))
if (value == null) _communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName);
continue; }
jObject.Remove(oldName); public void RemoveIndividualCollection(int individualIndex)
jObject.Add("Male" + oldName, value); {
jObject.Add("Female" + oldName, value); if (individualIndex < 0 || individualIndex >= Individuals.Count)
} return;
using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate); var (name, old) = Individuals[individualIndex];
using var writer = new StreamWriter(stream); if (Individuals.Delete(individualIndex))
using var j = new JsonTextWriter(writer); _communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name);
j.Formatting = Formatting.Indented; }
jObject.WriteTo(j);
public void MoveIndividualCollection(int from, int to)
{
if (Individuals.Move(from, to))
Penumbra.SaveService.QueueSave(this);
}
// Obtain the index of a collection by name.
private int GetIndexForCollectionName(string name)
=> name.Length == 0 ? ModCollection.Empty.Index : _collections.IndexOf(c => c.Name == name);
// Load default, current, special, and character collections from config.
// Then create caches. If a collection does not exist anymore, reset it to an appropriate default.
private void LoadCollections(FilenameService files)
{
var configChanged = !ReadActiveCollections(files, out var jObject);
// Load the default collection.
var defaultName = jObject[nameof(Default)]?.ToObject<string>() ?? (configChanged ? ModCollection.DefaultCollection : ModCollection.Empty.Name);
var defaultIdx = GetIndexForCollectionName(defaultName);
if (defaultIdx < 0)
{
Penumbra.ChatService.NotificationMessage(
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
}
else
{
Default = this[defaultIdx];
} }
// Migrate individual collections to Identifiers for 0.6.0. // Load the interface collection.
private bool MigrateIndividualCollections(JObject jObject) var interfaceName = jObject[nameof(Interface)]?.ToObject<string>() ?? Default.Name;
var interfaceIdx = GetIndexForCollectionName(interfaceName);
if (interfaceIdx < 0)
{ {
var version = jObject[nameof(Version)]?.Value<int>() ?? 0; Penumbra.ChatService.NotificationMessage(
if (version > 0) $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.",
return false; "Load Failure", NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
}
else
{
Interface = this[interfaceIdx];
}
// Load character collections. If a player name comes up multiple times, the last one is applied. // Load the current collection.
var characters = jObject["Characters"]?.ToObject<Dictionary<string, string>>() ?? new Dictionary<string, string>(); var currentName = jObject[nameof(Current)]?.ToObject<string>() ?? ModCollection.DefaultCollection;
var dict = new Dictionary<string, ModCollection>(characters.Count); var currentIdx = GetIndexForCollectionName(currentName);
foreach (var (player, collectionName) in characters) if (currentIdx < 0)
{
Penumbra.ChatService.NotificationMessage(
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollection}.",
"Load Failure", NotificationType.Warning);
Current = DefaultName;
configChanged = true;
}
else
{
Current = this[currentIdx];
}
// Load special collections.
foreach (var (type, name, _) in CollectionTypeExtensions.Special)
{
var typeName = jObject[type.ToString()]?.ToObject<string>();
if (typeName != null)
{ {
var idx = GetIndexForCollectionName(collectionName); var idx = GetIndexForCollectionName(typeName);
if (idx < 0) if (idx < 0)
{ {
Penumbra.ChatService.NotificationMessage( Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.",
$"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure", "Load Failure",
NotificationType.Warning); NotificationType.Warning);
dict.Add(player, Empty); configChanged = true;
} }
else else
{ {
dict.Add(player, this[idx]); _specialCollections[(int)type] = this[idx];
} }
} }
Individuals.Migrate0To1(dict);
return true;
} }
// Read the active collection file into a jObject. configChanged |= MigrateIndividualCollections(jObject);
// Returns true if this is successful, false if the file does not exist or it is unsuccessful. configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this);
private static bool ReadActiveCollections(FilenameService files, out JObject ret)
{
var file = files.ActiveCollectionsFile;
if (File.Exists(file))
try
{
ret = JObject.Parse(File.ReadAllText(file));
return true;
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}");
}
ret = new JObject(); // Save any changes and create all required caches.
if (configChanged)
Penumbra.SaveService.ImmediateSave(this);
}
// Migrate ungendered collections to Male and Female for 0.5.9.0.
public static void MigrateUngenderedCollections(FilenameService fileNames)
{
if (!ReadActiveCollections(fileNames, out var jObject))
return;
foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male ")))
{
var oldName = type.ToString()[4..];
var value = jObject[oldName];
if (value == null)
continue;
jObject.Remove(oldName);
jObject.Add("Male" + oldName, value);
jObject.Add("Female" + oldName, value);
}
using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate);
using var writer = new StreamWriter(stream);
using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented;
jObject.WriteTo(j);
}
// Migrate individual collections to Identifiers for 0.6.0.
private bool MigrateIndividualCollections(JObject jObject)
{
var version = jObject[nameof(Version)]?.Value<int>() ?? 0;
if (version > 0)
return false; return false;
// Load character collections. If a player name comes up multiple times, the last one is applied.
var characters = jObject["Characters"]?.ToObject<Dictionary<string, string>>() ?? new Dictionary<string, string>();
var dict = new Dictionary<string, ModCollection>(characters.Count);
foreach (var (player, collectionName) in characters)
{
var idx = GetIndexForCollectionName(collectionName);
if (idx < 0)
{
Penumbra.ChatService.NotificationMessage(
$"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure",
NotificationType.Warning);
dict.Add(player, ModCollection.Empty);
}
else
{
dict.Add(player, this[idx]);
}
} }
// Save if any of the active collections is changed. Individuals.Migrate0To1(dict);
private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3) return true;
{ }
if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary)
Penumbra.SaveService.QueueSave(this);
}
// Cache handling. Usually recreate caches on the next framework tick, // Read the active collection file into a jObject.
// but at launch create all of them at once. // Returns true if this is successful, false if the file does not exist or it is unsuccessful.
public void CreateNecessaryCaches() private static bool ReadActiveCollections(FilenameService files, out JObject ret)
{ {
var tasks = _specialCollections.OfType<ModCollection>() var file = files.ActiveCollectionsFile;
.Concat(Individuals.Select(p => p.Collection)) if (File.Exists(file))
.Prepend(Current) try
.Prepend(Default) {
.Prepend(Interface) ret = JObject.Parse(File.ReadAllText(file));
.Distinct() return true;
.Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == Default))) }
.ToArray(); catch (Exception e)
{
Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}");
}
Task.WaitAll(tasks); ret = new JObject();
} return false;
}
private void RemoveCache(int idx) // Save if any of the active collections is changed.
{ private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3)
if (idx != Empty.Index {
&& idx != Default.Index if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary)
&& idx != Interface.Index Penumbra.SaveService.QueueSave(this);
&& idx != Current.Index }
&& _specialCollections.All(c => c == null || c.Index != idx)
&& Individuals.Select(p => p.Collection).All(c => c.Index != idx))
_collections[idx].ClearCache();
}
// Recalculate effective files for active collections on events. // Cache handling. Usually recreate caches on the next framework tick,
private void OnModAddedActive(Mod mod) // but at launch create all of them at once.
{ public void CreateNecessaryCaches()
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) {
collection._cache!.AddMod(mod, true); var tasks = _specialCollections.OfType<ModCollection>()
} .Concat(Individuals.Select(p => p.Collection))
.Prepend(Current)
.Prepend(Default)
.Prepend(Interface)
.Distinct()
.Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == Default)))
.ToArray();
private void OnModRemovedActive(Mod mod) Task.WaitAll(tasks);
{ }
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
collection._cache!.RemoveMod(mod, true);
}
private void OnModMovedActive(Mod mod) private void RemoveCache(int idx)
{ {
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) if (idx != ModCollection.Empty.Index
collection._cache!.ReloadMod(mod, true); && idx != Default.Index
} && idx != Interface.Index
&& idx != Current.Index
&& _specialCollections.All(c => c == null || c.Index != idx)
&& Individuals.Select(p => p.Collection).All(c => c.Index != idx))
_collections[idx].ClearCache();
}
public string ToFilename(FilenameService fileNames) // Recalculate effective files for active collections on events.
=> fileNames.ActiveCollectionsFile; private void OnModAddedActive(Mod mod)
{
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
collection._cache!.AddMod(mod, true);
}
public string TypeName private void OnModRemovedActive(Mod mod)
=> "Active Collections"; {
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
collection._cache!.RemoveMod(mod, true);
}
public string LogName(string _) private void OnModMovedActive(Mod mod)
=> "to file"; {
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
collection._cache!.ReloadMod(mod, true);
}
public void Save(StreamWriter writer) public string ToFilename(FilenameService fileNames)
{ => fileNames.ActiveCollectionsFile;
var jObj = new JObject
public string TypeName
=> "Active Collections";
public string LogName(string _)
=> "to file";
public void Save(StreamWriter writer)
{
var jObj = new JObject
{ {
{ nameof(Version), Version }, { nameof(Version), Version },
{ nameof(Default), Default.Name }, { nameof(Default), Default.Name },
{ nameof(Interface), Interface.Name }, { nameof(Interface), Interface.Name },
{ nameof(Current), Current.Name }, { nameof(Current), Current.Name },
}; };
foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null)
.Select(p => ((CollectionType)p.Index, p.Value!))) .Select(p => ((CollectionType)p.Index, p.Value!)))
jObj.Add(type.ToString(), collection.Name); jObj.Add(type.ToString(), collection.Name);
jObj.Add(nameof(Individuals), Individuals.ToJObject()); jObj.Add(nameof(Individuals), Individuals.ToJObject());
using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
jObj.WriteTo(j); jObj.WriteTo(j);
}
} }
} }

View file

@ -17,462 +17,459 @@ using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
namespace Penumbra.Collections; namespace Penumbra.Collections;
public partial class ModCollection public sealed partial class CollectionManager : IDisposable, IEnumerable<ModCollection>
{ {
public sealed partial class Manager : IDisposable, IEnumerable<ModCollection> private readonly Mods.ModManager _modManager;
private readonly CommunicatorService _communicator;
private readonly CharacterUtility _characterUtility;
private readonly ResidentResourceManager _residentResources;
private readonly Configuration _config;
// The empty collection is always available and always has index 0.
// It can not be deleted or moved.
private readonly List<ModCollection> _collections = new()
{ {
private readonly Mod.Manager _modManager; ModCollection.Empty,
private readonly CommunicatorService _communicator; };
private readonly CharacterUtility _characterUtility;
private readonly ResidentResourceManager _residentResources;
private readonly Configuration _config;
public ModCollection this[Index idx]
=> _collections[idx];
// The empty collection is always available and always has index 0. public ModCollection? this[string name]
// It can not be deleted or moved. => ByName(name, out var c) ? c : null;
private readonly List<ModCollection> _collections = new()
public int Count
=> _collections.Count;
// Obtain a collection case-independently by name.
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
=> _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
// Default enumeration skips the empty collection.
public IEnumerator<ModCollection> GetEnumerator()
=> _collections.Skip(1).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public IEnumerable<ModCollection> GetEnumeratorWithEmpty()
=> _collections;
public CollectionManager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility,
ResidentResourceManager residentResources, Configuration config, Mods.ModManager modManager, IndividualCollections individuals)
{
using var time = timer.Measure(StartTimeType.Collections);
_communicator = communicator;
_characterUtility = characterUtility;
_residentResources = residentResources;
_config = config;
_modManager = modManager;
Individuals = individuals;
// The collection manager reacts to changes in mods by itself.
_modManager.ModDiscoveryStarted += OnModDiscoveryStarted;
_modManager.ModDiscoveryFinished += OnModDiscoveryFinished;
_modManager.ModOptionChanged += OnModOptionsChanged;
_modManager.ModPathChanged += OnModPathChange;
_communicator.CollectionChange.Event += SaveOnChange;
_communicator.TemporaryGlobalModChange.Event += OnGlobalModChange;
ReadCollections(files);
LoadCollections(files);
UpdateCurrentCollectionInUse();
CreateNecessaryCaches();
}
public void Dispose()
{
_communicator.CollectionChange.Event -= SaveOnChange;
_communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange;
_modManager.ModDiscoveryStarted -= OnModDiscoveryStarted;
_modManager.ModDiscoveryFinished -= OnModDiscoveryFinished;
_modManager.ModOptionChanged -= OnModOptionsChanged;
_modManager.ModPathChanged -= OnModPathChange;
}
private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed)
=> TempModManager.OnGlobalModChange(_collections, mod, created, removed);
// Returns true if the name is not empty, it is not the name of the empty collection
// and no existing collection results in the same filename as name.
public bool CanAddCollection(string name, out string fixedName)
{
if (!ModCollection.IsValidName(name))
{ {
Empty, fixedName = string.Empty;
}; return false;
public ModCollection this[Index idx]
=> _collections[idx];
public ModCollection? this[string name]
=> ByName(name, out var c) ? c : null;
public int Count
=> _collections.Count;
// Obtain a collection case-independently by name.
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
=> _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
// Default enumeration skips the empty collection.
public IEnumerator<ModCollection> GetEnumerator()
=> _collections.Skip(1).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public IEnumerable<ModCollection> GetEnumeratorWithEmpty()
=> _collections;
public Manager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility,
ResidentResourceManager residentResources, Configuration config, Mod.Manager manager, IndividualCollections individuals)
{
using var time = timer.Measure(StartTimeType.Collections);
_communicator = communicator;
_characterUtility = characterUtility;
_residentResources = residentResources;
_config = config;
_modManager = manager;
Individuals = individuals;
// The collection manager reacts to changes in mods by itself.
_modManager.ModDiscoveryStarted += OnModDiscoveryStarted;
_modManager.ModDiscoveryFinished += OnModDiscoveryFinished;
_modManager.ModOptionChanged += OnModOptionsChanged;
_modManager.ModPathChanged += OnModPathChange;
_communicator.CollectionChange.Event += SaveOnChange;
_communicator.TemporaryGlobalModChange.Event += OnGlobalModChange;
ReadCollections(files);
LoadCollections(files);
UpdateCurrentCollectionInUse();
CreateNecessaryCaches();
} }
public void Dispose() name = name.RemoveInvalidPathSymbols().ToLowerInvariant();
if (name.Length == 0
|| name == ModCollection.Empty.Name.ToLowerInvariant()
|| _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name))
{ {
_communicator.CollectionChange.Event -= SaveOnChange; fixedName = string.Empty;
_communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; return false;
_modManager.ModDiscoveryStarted -= OnModDiscoveryStarted;
_modManager.ModDiscoveryFinished -= OnModDiscoveryFinished;
_modManager.ModOptionChanged -= OnModOptionsChanged;
_modManager.ModPathChanged -= OnModPathChange;
} }
private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) fixedName = name;
=> TempModManager.OnGlobalModChange(_collections, mod, created, removed); return true;
}
// Returns true if the name is not empty, it is not the name of the empty collection // Add a new collection of the given name.
// and no existing collection results in the same filename as name. // If duplicate is not-null, the new collection will be a duplicate of it.
public bool CanAddCollection(string name, out string fixedName) // If the name of the collection would result in an already existing filename, skip it.
// Returns true if the collection was successfully created and fires a Inactive event.
// Also sets the current collection to the new collection afterwards.
public bool AddCollection(string name, ModCollection? duplicate)
{
if (!CanAddCollection(name, out var fixedName))
{ {
if (!IsValidName(name)) Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists.");
{ return false;
fixedName = string.Empty;
return false;
}
name = name.RemoveInvalidPathSymbols().ToLowerInvariant();
if (name.Length == 0
|| name == Empty.Name.ToLowerInvariant()
|| _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name))
{
fixedName = string.Empty;
return false;
}
fixedName = name;
return true;
} }
// Add a new collection of the given name. var newCollection = duplicate?.Duplicate(name) ?? ModCollection.CreateNewEmpty(name);
// If duplicate is not-null, the new collection will be a duplicate of it. newCollection.Index = _collections.Count;
// If the name of the collection would result in an already existing filename, skip it. _collections.Add(newCollection);
// Returns true if the collection was successfully created and fires a Inactive event.
// Also sets the current collection to the new collection afterwards. Penumbra.SaveService.ImmediateSave(newCollection);
public bool AddCollection(string name, ModCollection? duplicate) Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}.");
_communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
SetCollection(newCollection.Index, CollectionType.Current);
return true;
}
// Remove the given collection if it exists and is neither the empty nor the default-named collection.
// If the removed collection was active, it also sets the corresponding collection to the appropriate default.
// Also removes the collection from inheritances of all other collections.
public bool RemoveCollection(int idx)
{
if (idx <= ModCollection.Empty.Index || idx >= _collections.Count)
{ {
if (!CanAddCollection(name, out var fixedName)) Penumbra.Log.Error("Can not remove the empty collection.");
{ return false;
Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists.");
return false;
}
var newCollection = duplicate?.Duplicate(name) ?? CreateNewEmpty(name);
newCollection.Index = _collections.Count;
_collections.Add(newCollection);
Penumbra.SaveService.ImmediateSave(newCollection);
Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}.");
_communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
SetCollection(newCollection.Index, CollectionType.Current);
return true;
} }
// Remove the given collection if it exists and is neither the empty nor the default-named collection. if (idx == DefaultName.Index)
// If the removed collection was active, it also sets the corresponding collection to the appropriate default.
// Also removes the collection from inheritances of all other collections.
public bool RemoveCollection(int idx)
{ {
if (idx <= Empty.Index || idx >= _collections.Count) Penumbra.Log.Error("Can not remove the default collection.");
{ return false;
Penumbra.Log.Error("Can not remove the empty collection.");
return false;
}
if (idx == DefaultName.Index)
{
Penumbra.Log.Error("Can not remove the default collection.");
return false;
}
if (idx == Current.Index)
SetCollection(DefaultName.Index, CollectionType.Current);
if (idx == Default.Index)
SetCollection(Empty.Index, CollectionType.Default);
for (var i = 0; i < _specialCollections.Length; ++i)
{
if (idx == _specialCollections[i]?.Index)
SetCollection(Empty, (CollectionType)i);
}
for (var i = 0; i < Individuals.Count; ++i)
{
if (Individuals[i].Collection.Index == idx)
SetCollection(Empty, CollectionType.Individual, i);
}
var collection = _collections[idx];
// Clear own inheritances.
foreach (var inheritance in collection.Inheritance)
collection.ClearSubscriptions(inheritance);
Penumbra.SaveService.ImmediateDelete(collection);
_collections.RemoveAt(idx);
// Clear external inheritances.
foreach (var c in _collections)
{
var inheritedIdx = c._inheritance.IndexOf(collection);
if (inheritedIdx >= 0)
c.RemoveInheritance(inheritedIdx);
if (c.Index > idx)
--c.Index;
}
Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}.");
_communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
return true;
} }
public bool RemoveCollection(ModCollection collection) if (idx == Current.Index)
=> RemoveCollection(collection.Index); SetCollection(DefaultName.Index, CollectionType.Current);
private void OnModDiscoveryStarted() if (idx == Default.Index)
SetCollection(ModCollection.Empty.Index, CollectionType.Default);
for (var i = 0; i < _specialCollections.Length; ++i)
{ {
foreach (var collection in this) if (idx == _specialCollections[i]?.Index)
collection.PrepareModDiscovery(); SetCollection(ModCollection.Empty, (CollectionType)i);
} }
private void OnModDiscoveryFinished() for (var i = 0; i < Individuals.Count; ++i)
{ {
// First, re-apply all mod settings. if (Individuals[i].Collection.Index == idx)
foreach (var collection in this) SetCollection(ModCollection.Empty, CollectionType.Individual, i);
collection.ApplyModSettings();
// Afterwards, we update the caches. This can not happen in the same loop due to inheritance.
foreach (var collection in this.Where(c => c.HasCache))
collection.ForceCacheUpdate();
} }
var collection = _collections[idx];
// A changed mod path forces changes for all collections, active and inactive. // Clear own inheritances.
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, foreach (var inheritance in collection.Inheritance)
DirectoryInfo? newDirectory) collection.ClearSubscriptions(inheritance);
Penumbra.SaveService.ImmediateDelete(collection);
_collections.RemoveAt(idx);
// Clear external inheritances.
foreach (var c in _collections)
{ {
switch (type) var inheritedIdx = c._inheritance.IndexOf(collection);
{ if (inheritedIdx >= 0)
case ModPathChangeType.Added: c.RemoveInheritance(inheritedIdx);
foreach (var collection in this)
collection.AddMod(mod);
OnModAddedActive(mod); if (c.Index > idx)
break; --c.Index;
case ModPathChangeType.Deleted:
OnModRemovedActive(mod);
foreach (var collection in this)
collection.RemoveMod(mod, mod.Index);
break;
case ModPathChangeType.Moved:
OnModMovedActive(mod);
foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
Penumbra.SaveService.QueueSave(collection);
break;
case ModPathChangeType.StartingReload:
OnModRemovedActive(mod);
break;
case ModPathChangeType.Reloaded:
OnModAddedActive(mod);
break;
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
} }
// Automatically update all relevant collections when a mod is changed. Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}.");
// This means saving if options change in a way where the settings may change and the collection has settings for this mod. _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
// And also updating effective file and meta manipulation lists if necessary. return true;
private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) }
public bool RemoveCollection(ModCollection collection)
=> RemoveCollection(collection.Index);
private void OnModDiscoveryStarted()
{
foreach (var collection in this)
collection.PrepareModDiscovery();
}
private void OnModDiscoveryFinished()
{
// First, re-apply all mod settings.
foreach (var collection in this)
collection.ApplyModSettings();
// Afterwards, we update the caches. This can not happen in the same loop due to inheritance.
foreach (var collection in this.Where(c => c.HasCache))
collection.ForceCacheUpdate();
}
// A changed mod path forces changes for all collections, active and inactive.
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory)
{
switch (type)
{ {
// Handle changes that break revertability. case ModPathChangeType.Added:
if (type == ModOptionChangeType.PrepareChange)
{
foreach (var collection in this.Where(c => c.HasCache))
{
if (collection[mod.Index].Settings is { Enabled: true })
collection._cache!.RemoveMod(mod, false);
}
return;
}
type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload);
// Handle changes that require overwriting the collection.
if (requiresSaving)
foreach (var collection in this) foreach (var collection in this)
{ collection.AddMod(mod);
if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false)
Penumbra.SaveService.QueueSave(collection);
}
// Handle changes that reload the mod if the changes did not need to be prepared, OnModAddedActive(mod);
// or re-add the mod if they were prepared. break;
if (recomputeList) case ModPathChangeType.Deleted:
foreach (var collection in this.Where(c => c.HasCache)) OnModRemovedActive(mod);
{ foreach (var collection in this)
if (collection[mod.Index].Settings is { Enabled: true }) collection.RemoveMod(mod, mod.Index);
{
if (reload)
collection._cache!.ReloadMod(mod, true);
else
collection._cache!.AddMod(mod, true);
}
}
}
// Add the collection with the default name if it does not exist. break;
// It should always be ensured that it exists, otherwise it will be created. case ModPathChangeType.Moved:
// This can also not be deleted, so there are always at least the empty and a collection with default name. OnModMovedActive(mod);
private void AddDefaultCollection() foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
{ Penumbra.SaveService.QueueSave(collection);
var idx = GetIndexForCollectionName(DefaultCollection);
if (idx >= 0)
{
DefaultName = this[idx];
return;
}
var defaultCollection = CreateNewEmpty(DefaultCollection); break;
Penumbra.SaveService.ImmediateSave(defaultCollection); case ModPathChangeType.StartingReload:
defaultCollection.Index = _collections.Count; OnModRemovedActive(mod);
_collections.Add(defaultCollection); break;
} case ModPathChangeType.Reloaded:
OnModAddedActive(mod);
// Inheritances can not be setup before all collections are read, break;
// so this happens after reading the collections. default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
private void ApplyInheritances(IEnumerable<IReadOnlyList<string>> inheritances)
{
foreach (var (collection, inheritance) in this.Zip(inheritances))
{
var changes = false;
foreach (var subCollectionName in inheritance)
{
if (!ByName(subCollectionName, out var subCollection))
{
changes = true;
Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed.");
}
else if (!collection.AddInheritance(subCollection, false))
{
changes = true;
Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed.");
}
}
if (changes)
Penumbra.SaveService.ImmediateSave(collection);
}
}
// Read all collection files in the Collection Directory.
// Ensure that the default named collection exists, and apply inheritances afterwards.
// Duplicate collection files are not deleted, just not added here.
private void ReadCollections(FilenameService files)
{
var inheritances = new List<IReadOnlyList<string>>();
foreach (var file in files.CollectionFiles)
{
var collection = LoadFromFile(file, out var inheritance);
if (collection == null || collection.Name.Length == 0)
continue;
if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json")
Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}.");
if (this[collection.Name] != null)
{
Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists.");
}
else
{
inheritances.Add(inheritance);
collection.Index = _collections.Count;
_collections.Add(collection);
}
}
AddDefaultCollection();
ApplyInheritances(inheritances);
}
public string RedundancyCheck(CollectionType type, ActorIdentifier id)
{
var checkAssignment = ByType(type, id);
if (checkAssignment == null)
return string.Empty;
switch (type)
{
// Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap.
case CollectionType.Individual:
switch (id.Type)
{
case IdentifierType.Player when id.HomeWorld != ushort.MaxValue:
{
var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue));
return global?.Index == checkAssignment.Index
? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."
: string.Empty;
}
case IdentifierType.Owned:
if (id.HomeWorld != ushort.MaxValue)
{
var global = ByType(CollectionType.Individual,
Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId));
if (global?.Index == checkAssignment.Index)
return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it.";
}
var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId));
return unowned?.Index == checkAssignment.Index
? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it."
: string.Empty;
}
break;
// The group of all Characters is redundant if they are all equal to Default or unassigned.
case CollectionType.MalePlayerCharacter:
case CollectionType.MaleNonPlayerCharacter:
case CollectionType.FemalePlayerCharacter:
case CollectionType.FemaleNonPlayerCharacter:
var first = ByType(CollectionType.MalePlayerCharacter) ?? Default;
var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default;
var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default;
var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default;
if (first.Index == second.Index
&& first.Index == third.Index
&& first.Index == fourth.Index
&& first.Index == Default.Index)
return
"Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n"
+ "You can keep just the Default Assignment.";
break;
// Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default.
case CollectionType.NonPlayerChild:
case CollectionType.NonPlayerElderly:
var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter);
var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter);
var collection1 = CollectionType.MaleNonPlayerCharacter;
var collection2 = CollectionType.FemaleNonPlayerCharacter;
if (maleNpc == null)
{
maleNpc = Default;
if (maleNpc.Index != checkAssignment.Index)
return string.Empty;
collection1 = CollectionType.Default;
}
if (femaleNpc == null)
{
femaleNpc = Default;
if (femaleNpc.Index != checkAssignment.Index)
return string.Empty;
collection2 = CollectionType.Default;
}
return collection1 == collection2
? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them."
: $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them.";
// For other assignments, check the inheritance order, unassigned means fall-through,
// assigned needs identical assignments to be redundant.
default:
var group = type.InheritanceOrder();
foreach (var parentType in group)
{
var assignment = ByType(parentType);
if (assignment == null)
continue;
if (assignment.Index == checkAssignment.Index)
return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it.";
}
break;
}
return string.Empty;
} }
} }
// Automatically update all relevant collections when a mod is changed.
// This means saving if options change in a way where the settings may change and the collection has settings for this mod.
// And also updating effective file and meta manipulation lists if necessary.
private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
{
// Handle changes that break revertability.
if (type == ModOptionChangeType.PrepareChange)
{
foreach (var collection in this.Where(c => c.HasCache))
{
if (collection[mod.Index].Settings is { Enabled: true })
collection._cache!.RemoveMod(mod, false);
}
return;
}
type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload);
// Handle changes that require overwriting the collection.
if (requiresSaving)
foreach (var collection in this)
{
if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false)
Penumbra.SaveService.QueueSave(collection);
}
// Handle changes that reload the mod if the changes did not need to be prepared,
// or re-add the mod if they were prepared.
if (recomputeList)
foreach (var collection in this.Where(c => c.HasCache))
{
if (collection[mod.Index].Settings is { Enabled: true })
{
if (reload)
collection._cache!.ReloadMod(mod, true);
else
collection._cache!.AddMod(mod, true);
}
}
}
// Add the collection with the default name if it does not exist.
// It should always be ensured that it exists, otherwise it will be created.
// This can also not be deleted, so there are always at least the empty and a collection with default name.
private void AddDefaultCollection()
{
var idx = GetIndexForCollectionName(ModCollection.DefaultCollection);
if (idx >= 0)
{
DefaultName = this[idx];
return;
}
var defaultCollection = ModCollection.CreateNewEmpty((string)ModCollection.DefaultCollection);
Penumbra.SaveService.ImmediateSave(defaultCollection);
defaultCollection.Index = _collections.Count;
_collections.Add(defaultCollection);
}
// Inheritances can not be setup before all collections are read,
// so this happens after reading the collections.
private void ApplyInheritances(IEnumerable<IReadOnlyList<string>> inheritances)
{
foreach (var (collection, inheritance) in this.Zip(inheritances))
{
var changes = false;
foreach (var subCollectionName in inheritance)
{
if (!ByName(subCollectionName, out var subCollection))
{
changes = true;
Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed.");
}
else if (!collection.AddInheritance(subCollection, false))
{
changes = true;
Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed.");
}
}
if (changes)
Penumbra.SaveService.ImmediateSave(collection);
}
}
// Read all collection files in the Collection Directory.
// Ensure that the default named collection exists, and apply inheritances afterwards.
// Duplicate collection files are not deleted, just not added here.
private void ReadCollections(FilenameService files)
{
var inheritances = new List<IReadOnlyList<string>>();
foreach (var file in files.CollectionFiles)
{
var collection = ModCollection.LoadFromFile(file, out var inheritance);
if (collection == null || collection.Name.Length == 0)
continue;
if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json")
Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}.");
if (this[collection.Name] != null)
{
Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists.");
}
else
{
inheritances.Add(inheritance);
collection.Index = _collections.Count;
_collections.Add(collection);
}
}
AddDefaultCollection();
ApplyInheritances(inheritances);
}
public string RedundancyCheck(CollectionType type, ActorIdentifier id)
{
var checkAssignment = ByType(type, id);
if (checkAssignment == null)
return string.Empty;
switch (type)
{
// Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap.
case CollectionType.Individual:
switch (id.Type)
{
case IdentifierType.Player when id.HomeWorld != ushort.MaxValue:
{
var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue));
return global?.Index == checkAssignment.Index
? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."
: string.Empty;
}
case IdentifierType.Owned:
if (id.HomeWorld != ushort.MaxValue)
{
var global = ByType(CollectionType.Individual,
Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId));
if (global?.Index == checkAssignment.Index)
return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it.";
}
var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId));
return unowned?.Index == checkAssignment.Index
? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it."
: string.Empty;
}
break;
// The group of all Characters is redundant if they are all equal to Default or unassigned.
case CollectionType.MalePlayerCharacter:
case CollectionType.MaleNonPlayerCharacter:
case CollectionType.FemalePlayerCharacter:
case CollectionType.FemaleNonPlayerCharacter:
var first = ByType(CollectionType.MalePlayerCharacter) ?? Default;
var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default;
var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default;
var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default;
if (first.Index == second.Index
&& first.Index == third.Index
&& first.Index == fourth.Index
&& first.Index == Default.Index)
return
"Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n"
+ "You can keep just the Default Assignment.";
break;
// Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default.
case CollectionType.NonPlayerChild:
case CollectionType.NonPlayerElderly:
var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter);
var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter);
var collection1 = CollectionType.MaleNonPlayerCharacter;
var collection2 = CollectionType.FemaleNonPlayerCharacter;
if (maleNpc == null)
{
maleNpc = Default;
if (maleNpc.Index != checkAssignment.Index)
return string.Empty;
collection1 = CollectionType.Default;
}
if (femaleNpc == null)
{
femaleNpc = Default;
if (femaleNpc.Index != checkAssignment.Index)
return string.Empty;
collection2 = CollectionType.Default;
}
return collection1 == collection2
? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them."
: $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them.";
// For other assignments, check the inheritance order, unassigned means fall-through,
// assigned needs identical assignments to be redundant.
default:
var group = type.InheritanceOrder();
foreach (var parentType in group)
{
var assignment = ByType(parentType);
if (assignment == null)
continue;
if (assignment.Index == checkAssignment.Index)
return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it.";
}
break;
}
return string.Empty;
}
} }

View file

@ -26,7 +26,7 @@ public partial class IndividualCollections
return ret; return ret;
} }
public bool ReadJObject( JArray? obj, ModCollection.Manager manager ) public bool ReadJObject( JArray? obj, CollectionManager manager )
{ {
if( obj == null ) if( obj == null )
{ {

View file

@ -8,7 +8,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using Penumbra.Interop;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
@ -20,27 +19,27 @@ namespace Penumbra.Collections;
public partial class ModCollection public partial class ModCollection
{ {
// Only active collections need to have a cache. // Only active collections need to have a cache.
private Cache? _cache; internal ModCollectionCache? _cache;
public bool HasCache public bool HasCache
=> _cache != null; => _cache != null;
// Count the number of changes of the effective file list. // Count the number of changes of the effective file list.
// This is used for material and imc changes. // This is used for material and imc changes.
public int ChangeCounter { get; private set; } public int ChangeCounter { get; internal set; }
// Only create, do not update. // Only create, do not update.
private void CreateCache(bool isDefault) internal void CreateCache(bool isDefault)
{ {
if (_cache == null) if (_cache != null)
{ return;
CalculateEffectiveFileList(isDefault);
Penumbra.Log.Verbose($"Created new cache for collection {Name}."); CalculateEffectiveFileList(isDefault);
} Penumbra.Log.Verbose($"Created new cache for collection {Name}.");
} }
// Force an update with metadata for this cache. // Force an update with metadata for this cache.
private void ForceCacheUpdate() internal void ForceCacheUpdate()
=> CalculateEffectiveFileList(this == Penumbra.CollectionManager.Default); => CalculateEffectiveFileList(this == Penumbra.CollectionManager.Default);
// Handle temporary mods for this collection. // Handle temporary mods for this collection.
@ -83,7 +82,7 @@ public partial class ModCollection
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CheckFullPath(Utf8GamePath path, FullPath fullPath) internal static bool CheckFullPath(Utf8GamePath path, FullPath fullPath)
{ {
if (fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength) if (fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength)
return true; return true;
@ -127,14 +126,14 @@ public partial class ModCollection
=> Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name, () => => Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name, () =>
CalculateEffectiveFileListInternal(isDefault)); CalculateEffectiveFileListInternal(isDefault));
private void CalculateEffectiveFileListInternal(bool isDefault) internal void CalculateEffectiveFileListInternal(bool isDefault)
{ {
// Skip the empty collection. // Skip the empty collection.
if (Index == 0) if (Index == 0)
return; return;
Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}"); Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}");
_cache ??= new Cache(this); _cache ??= new ModCollectionCache(this);
_cache.FullRecalculation(isDefault); _cache.FullRecalculation(isDefault);
Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished."); Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished.");

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,7 @@ public partial class ModCollection : ISavable
{ {
// Since inheritances depend on other collections existing, // Since inheritances depend on other collections existing,
// we return them as a list to be applied after reading all collections. // we return them as a list to be applied after reading all collections.
private static ModCollection? LoadFromFile(FileInfo file, out IReadOnlyList<string> inheritance) internal static ModCollection? LoadFromFile(FileInfo file, out IReadOnlyList<string> inheritance)
{ {
inheritance = Array.Empty<string>(); inheritance = Array.Empty<string>();
if (!file.Exists) if (!file.Exists)

View file

@ -16,7 +16,7 @@ public partial class ModCollection
// The bool signifies whether the change was in an already inherited collection. // The bool signifies whether the change was in an already inherited collection.
public event Action< bool > InheritanceChanged; public event Action< bool > InheritanceChanged;
private readonly List< ModCollection > _inheritance = new(); internal readonly List< ModCollection > _inheritance = new();
public IReadOnlyList< ModCollection > Inheritance public IReadOnlyList< ModCollection > Inheritance
=> _inheritance; => _inheritance;
@ -98,7 +98,7 @@ public partial class ModCollection
Penumbra.Log.Debug( $"Removed {inheritance.AnonymizedName} from {AnonymizedName} inheritances." ); Penumbra.Log.Debug( $"Removed {inheritance.AnonymizedName} from {AnonymizedName} inheritances." );
} }
private void ClearSubscriptions( ModCollection other ) internal void ClearSubscriptions( ModCollection other )
{ {
other.ModSettingChanged -= OnInheritedModSettingChange; other.ModSettingChanged -= OnInheritedModSettingChange;
other.InheritanceChanged -= OnInheritedInheritanceChange; other.InheritanceChanged -= OnInheritedInheritanceChange;

View file

@ -23,18 +23,18 @@ public partial class ModCollection
// The collection name can contain invalid path characters, // The collection name can contain invalid path characters,
// but after removing those and going to lower case it has to be unique. // but after removing those and going to lower case it has to be unique.
public string Name { get; private init; } public string Name { get; internal init; }
// Get the first two letters of a collection name and its Index (or None if it is the empty collection). // Get the first two letters of a collection name and its Index (or None if it is the empty collection).
public string AnonymizedName public string AnonymizedName
=> this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})"; => this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})";
public int Version { get; private set; } public int Version { get; internal set; }
public int Index { get; private set; } = -1; public int Index { get; internal set; } = -1;
// If a ModSetting is null, it can be inherited from other collections. // If a ModSetting is null, it can be inherited from other collections.
// If no collection provides a setting for the mod, it is just disabled. // If no collection provides a setting for the mod, it is just disabled.
private readonly List<ModSettings?> _settings; internal readonly List<ModSettings?> _settings;
public IReadOnlyList<ModSettings?> Settings public IReadOnlyList<ModSettings?> Settings
=> _settings; => _settings;
@ -115,7 +115,7 @@ public partial class ModCollection
} }
// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. // Add settings for a new appended mod, by checking if the mod had settings from a previous deletion.
private bool AddMod(Mod mod) internal bool AddMod(Mod mod)
{ {
if (_unusedSettings.TryGetValue(mod.ModPath.Name, out var save)) if (_unusedSettings.TryGetValue(mod.ModPath.Name, out var save))
{ {
@ -130,7 +130,7 @@ public partial class ModCollection
} }
// Move settings from the current mod list to the unused mod settings. // Move settings from the current mod list to the unused mod settings.
private void RemoveMod(Mod mod, int idx) internal void RemoveMod(Mod mod, int idx)
{ {
var settings = _settings[idx]; var settings = _settings[idx];
if (settings != null) if (settings != null)
@ -150,7 +150,7 @@ public partial class ModCollection
} }
// Move all settings to unused settings for rediscovery. // Move all settings to unused settings for rediscovery.
private void PrepareModDiscovery() internal void PrepareModDiscovery()
{ {
foreach (var (mod, setting) in Penumbra.ModManager.Zip(_settings).Where(s => s.Second != null)) foreach (var (mod, setting) in Penumbra.ModManager.Zip(_settings).Where(s => s.Second != null))
_unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod); _unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod);
@ -160,7 +160,7 @@ public partial class ModCollection
// Apply all mod settings from unused settings to the current set of mods. // Apply all mod settings from unused settings to the current set of mods.
// Also fixes invalid settings. // Also fixes invalid settings.
private void ApplyModSettings() internal void ApplyModSettings()
{ {
_settings.Capacity = Math.Max(_settings.Capacity, Penumbra.ModManager.Count); _settings.Capacity = Math.Max(_settings.Capacity, Penumbra.ModManager.Count);
if (Penumbra.ModManager.Aggregate(false, (current, mod) => current | AddMod(mod))) if (Penumbra.ModManager.Aggregate(false, (current, mod) => current | AddMod(mod)))

View file

@ -27,12 +27,12 @@ public class CommandHandler : IDisposable
private readonly Configuration _config; private readonly Configuration _config;
private readonly ConfigWindow _configWindow; private readonly ConfigWindow _configWindow;
private readonly ActorManager _actors; private readonly ActorManager _actors;
private readonly Mod.Manager _modManager; private readonly ModManager _modManager;
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly Penumbra _penumbra; private readonly Penumbra _penumbra;
public CommandHandler(Framework framework, CommandManager commandManager, ChatGui chat, RedrawService redrawService, Configuration config, public CommandHandler(Framework framework, CommandManager commandManager, ChatGui chat, RedrawService redrawService, Configuration config,
ConfigWindow configWindow, Mod.Manager modManager, ModCollection.Manager collectionManager, ActorService actors, Penumbra penumbra) ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorService actors, Penumbra penumbra)
{ {
_commandManager = commandManager; _commandManager = commandManager;
_redrawService = redrawService; _redrawService = redrawService;

View file

@ -36,10 +36,10 @@ public partial class TexToolsImporter : IDisposable
private readonly Configuration _config; private readonly Configuration _config;
private readonly ModEditor _editor; private readonly ModEditor _editor;
private readonly Mod.Manager _modManager; private readonly ModManager _modManager;
public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles, public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles,
Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, Mod.Manager modManager) Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, ModManager modManager)
{ {
_baseDirectory = baseDirectory; _baseDirectory = baseDirectory;
_tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName ); _tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName );

View file

@ -31,12 +31,12 @@ public unsafe class CollectionResolver
private readonly CutsceneService _cutscenes; private readonly CutsceneService _cutscenes;
private readonly Configuration _config; private readonly Configuration _config;
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly TempCollectionManager _tempCollections; private readonly TempCollectionManager _tempCollections;
private readonly DrawObjectState _drawObjectState; private readonly DrawObjectState _drawObjectState;
public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, ClientState clientState, GameGui gameGui, public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, ClientState clientState, GameGui gameGui,
DataManager gameData, ActorService actors, CutsceneService cutscenes, Configuration config, ModCollection.Manager collectionManager, DataManager gameData, ActorService actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager,
TempCollectionManager tempCollections, DrawObjectState drawObjectState) TempCollectionManager tempCollections, DrawObjectState drawObjectState)
{ {
_performance = performance; _performance = performance;

View file

@ -16,7 +16,7 @@ public class PathResolver : IDisposable
{ {
private readonly PerformanceTracker _performance; private readonly PerformanceTracker _performance;
private readonly Configuration _config; private readonly Configuration _config;
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly TempCollectionManager _tempCollections; private readonly TempCollectionManager _tempCollections;
private readonly ResourceLoader _loader; private readonly ResourceLoader _loader;
@ -25,7 +25,7 @@ public class PathResolver : IDisposable
private readonly PathState _pathState; private readonly PathState _pathState;
private readonly MetaState _metaState; private readonly MetaState _metaState;
public unsafe PathResolver(PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager, public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager,
TempCollectionManager tempCollections, ResourceLoader loader, AnimationHookService animationHookService, SubfileHelper subfileHelper, TempCollectionManager tempCollections, ResourceLoader loader, AnimationHookService animationHookService, SubfileHelper subfileHelper,
PathState pathState, MetaState metaState) PathState pathState, MetaState metaState)
{ {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,73 +7,70 @@ using Penumbra.Util;
namespace Penumbra.Mods; 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. using var timer = time.Measure(StartTimeType.Mods);
public static bool MigrateModBackups = false; _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(); // 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 Mod this[int idx] public bool TryGetMod(string modDirectory, string modName, [NotNullWhen(true)] out Mod? mod)
=> _mods[idx]; {
mod = null;
public Mod this[Index idx] foreach (var m in _mods)
=> _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)
{ {
using var timer = time.Measure(StartTimeType.Mods); if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase))
_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)) mod = m;
{ return true;
mod = m;
return true;
}
if (m.Name == modName)
mod ??= m;
} }
return mod != null; if (m.Name == modName)
mod ??= m;
} }
return mod != null;
} }
} }

View file

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

View file

@ -15,10 +15,10 @@ public enum ModPathChangeType
public partial class Mod public partial class Mod
{ {
public DirectoryInfo ModPath { get; private set; } public DirectoryInfo ModPath { get; internal set; }
public string Identifier public string Identifier
=> Index >= 0 ? ModPath.Name : Name; => Index >= 0 ? ModPath.Name : Name;
public int Index { get; private set; } = -1; public int Index { get; internal set; } = -1;
public bool IsTemporary public bool IsTemporary
=> Index < 0; => Index < 0;
@ -33,7 +33,7 @@ public partial class Mod
_default = new SubMod( this ); _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(); modPath.Refresh();
if( !modPath.Exists ) 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; modDataChange = ModDataChangeType.Deletion;
ModPath.Refresh(); ModPath.Refresh();

View file

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

View file

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

View file

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

View file

@ -15,103 +15,105 @@ namespace Penumbra.Mods;
public partial class Mod 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 var ret = new MultiModGroup()
=> 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() 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, if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions)
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) Penumbra.ChatService.NotificationMessage(
{ $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning",
Penumbra.ChatService.NotificationMessage( NotificationType.Warning);
$"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning", break;
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));
} }
ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1)); var subMod = new SubMod(mod);
subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count);
return ret; subMod.Load(mod.ModPath, child, out var priority);
} ret.PrioritizedOptions.Add((subMod, priority));
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);
} }
}
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)) case GroupType.Multi: return this;
return false; case GroupType.Single:
var multi = new SingleModGroup()
DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo); {
UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); Name = Name,
return true; Description = Description,
} Priority = Priority,
DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0),
public void UpdatePositions(int from = 0) };
{ multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod));
foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from)) return multi;
o.SetPosition(o.GroupIdx, i); default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
} }
} }
public bool MoveOption(int optionIdxFrom, int optionIdxTo)
{
if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo))
return false;
DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo);
UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo));
return true;
}
public void UpdatePositions(int from = 0)
{
foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from))
o.SetPosition(o.GroupIdx, i);
}
} }

View file

@ -10,122 +10,119 @@ using Penumbra.Api.Enums;
namespace Penumbra.Mods; 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. public GroupType Type
private sealed class SingleModGroup : IModGroup => 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 var options = json[ "Options" ];
=> GroupType.Single; var ret = new SingleModGroup
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" ]; Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty,
var ret = new SingleModGroup Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty,
{ Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0,
Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0u,
Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, };
Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, if( ret.Name.Length == 0 )
DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0u, {
}; return null;
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 ); Name = Name,
subMod.SetPosition( groupIdx, ret.OptionData.Count ); Description = Description,
subMod.Load( mod.ModPath, child, out _ ); Priority = Priority,
ret.OptionData.Add( subMod ); 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 ) public bool MoveOption( int optionIdxFrom, int optionIdxTo )
ret.DefaultSettings = 0; {
if( !OptionData.Move( optionIdxFrom, optionIdxTo ) )
return ret; {
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; --DefaultSettings;
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 );
} }
} }
else if( DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo )
public bool MoveOption( int optionIdxFrom, int optionIdxTo )
{ {
if( !OptionData.Move( optionIdxFrom, optionIdxTo ) ) ++DefaultSettings;
{
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;
} }
public void UpdatePositions( int from = 0 ) UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) );
return true;
}
public void UpdatePositions( int from = 0 )
{
foreach( var (o, i) in OptionData.WithIndex().Skip( from ) )
{ {
foreach( var (o, i) in OptionData.WithIndex().Skip( from ) ) o.SetPosition( o.GroupIdx, i );
{
o.SetPosition( o.GroupIdx, i );
}
} }
} }
} }

View file

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

View file

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

View file

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

View file

@ -47,8 +47,8 @@ public class Penumbra : IDalamudPlugin
public static CharacterUtility CharacterUtility { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!;
public static GameEventManager GameEvents { get; private set; } = null!; public static GameEventManager GameEvents { get; private set; } = null!;
public static MetaFileManager MetaFileManager { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!;
public static Mod.Manager ModManager { get; private set; } = null!; public static ModManager ModManager { get; private set; } = null!;
public static ModCollection.Manager CollectionManager { get; private set; } = null!; public static CollectionManager CollectionManager { get; private set; } = null!;
public static TempCollectionManager TempCollections { get; private set; } = null!; public static TempCollectionManager TempCollections { get; private set; } = null!;
public static TempModManager TempMods { get; private set; } = null!; public static TempModManager TempMods { get; private set; } = null!;
public static ResourceLoader ResourceLoader { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!;
@ -96,8 +96,8 @@ public class Penumbra : IDalamudPlugin
TempMods = _tmp.Services.GetRequiredService<TempModManager>(); TempMods = _tmp.Services.GetRequiredService<TempModManager>();
ResidentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>(); ResidentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>();
_tmp.Services.GetRequiredService<ResourceManagerService>(); _tmp.Services.GetRequiredService<ResourceManagerService>();
ModManager = _tmp.Services.GetRequiredService<Mod.Manager>(); ModManager = _tmp.Services.GetRequiredService<ModManager>();
CollectionManager = _tmp.Services.GetRequiredService<ModCollection.Manager>(); CollectionManager = _tmp.Services.GetRequiredService<CollectionManager>();
TempCollections = _tmp.Services.GetRequiredService<TempCollectionManager>(); TempCollections = _tmp.Services.GetRequiredService<TempCollectionManager>();
ModFileSystem = _tmp.Services.GetRequiredService<ModFileSystem>(); ModFileSystem = _tmp.Services.GetRequiredService<ModFileSystem>();
RedrawService = _tmp.Services.GetRequiredService<RedrawService>(); RedrawService = _tmp.Services.GetRequiredService<RedrawService>();

View file

@ -88,12 +88,12 @@ public class PenumbraNew
// Add Collection Services // Add Collection Services
services.AddTransient<IndividualCollections>() services.AddTransient<IndividualCollections>()
.AddSingleton<TempCollectionManager>() .AddSingleton<TempCollectionManager>()
.AddSingleton<ModCollection.Manager>(); .AddSingleton<CollectionManager>();
// Add Mod Services // Add Mod Services
services.AddSingleton<TempModManager>() services.AddSingleton<TempModManager>()
.AddSingleton<ModDataEditor>() .AddSingleton<ModDataEditor>()
.AddSingleton<Mod.Manager>() .AddSingleton<ModManager>()
.AddSingleton<ModFileSystem>(); .AddSingleton<ModFileSystem>();
// Add Resource services // Add Resource services

View file

@ -87,7 +87,7 @@ public class ConfigMigrationService
if (_config.Version != 6) if (_config.Version != 6)
return; return;
ModCollection.Manager.MigrateUngenderedCollections(_fileNames); CollectionManager.MigrateUngenderedCollections(_fileNames);
_config.Version = 7; _config.Version = 7;
} }
@ -113,7 +113,7 @@ public class ConfigMigrationService
if (_config.Version != 4) if (_config.Version != 4)
return; return;
Mod.Manager.MigrateModBackups = true; ModManager.MigrateModBackups = true;
_config.Version = 5; _config.Version = 5;
} }
@ -257,11 +257,11 @@ public class ConfigMigrationService
using var j = new JsonTextWriter(writer); using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented; j.Formatting = Formatting.Indented;
j.WriteStartObject(); j.WriteStartObject();
j.WritePropertyName(nameof(ModCollection.Manager.Default)); j.WritePropertyName(nameof(CollectionManager.Default));
j.WriteValue(def); j.WriteValue(def);
j.WritePropertyName(nameof(ModCollection.Manager.Interface)); j.WritePropertyName(nameof(CollectionManager.Interface));
j.WriteValue(ui); j.WriteValue(ui);
j.WritePropertyName(nameof(ModCollection.Manager.Current)); j.WritePropertyName(nameof(CollectionManager.Current));
j.WriteValue(current); j.WriteValue(current);
foreach (var (type, collection) in special) foreach (var (type, collection) in special)
{ {

View file

@ -25,12 +25,12 @@ public class ItemSwapTab : IDisposable, ITab
{ {
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
private readonly ItemService _itemService; private readonly ItemService _itemService;
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly Mod.Manager _modManager; private readonly ModManager _modManager;
private readonly Configuration _config; private readonly Configuration _config;
public ItemSwapTab(CommunicatorService communicator, ItemService itemService, ModCollection.Manager collectionManager, public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager,
Mod.Manager modManager, Configuration config) ModManager modManager, Configuration config)
{ {
_communicator = communicator; _communicator = communicator;
_itemService = itemService; _itemService = itemService;

View file

@ -297,7 +297,7 @@ public partial class ModEditWindow
var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made.";
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes)) if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes))
{ {
var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod)_editor.Option!); var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!);
if (failedFiles > 0) if (failedFiles > 0)
Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.FullName}."); Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.FullName}.");
} }

View file

@ -194,7 +194,7 @@ public partial class ModEditWindow
_editor.FileEditor.Revert(_editor.Mod!, _editor.Option!); _editor.FileEditor.Revert(_editor.Mod!, _editor.Option!);
var fileRegistry = _editor.Files.Available.First(file => file.File.FullName == _targetPath); var fileRegistry = _editor.Files.Available.First(file => file.File.FullName == _targetPath);
_editor.FileEditor.AddPathsToSelected(_editor.Option!, new []{ fileRegistry }, _subDirs); _editor.FileEditor.AddPathsToSelected(_editor.Option!, new []{ fileRegistry }, _subDirs);
_editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod) _editor.Option!); _editor.FileEditor.Apply(_editor.Mod!, (SubMod) _editor.Option!);
return fileRegistry; return fileRegistry;
} }

View file

@ -9,9 +9,9 @@ namespace Penumbra.UI.CollectionTab;
public sealed class CollectionSelector : FilterComboCache<ModCollection> public sealed class CollectionSelector : FilterComboCache<ModCollection>
{ {
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
public CollectionSelector(ModCollection.Manager manager, Func<IReadOnlyList<ModCollection>> items) public CollectionSelector(CollectionManager manager, Func<IReadOnlyList<ModCollection>> items)
: base(items) : base(items)
=> _collectionManager = manager; => _collectionManager = manager;

View file

@ -16,10 +16,10 @@ namespace Penumbra.UI.CollectionTab;
public class IndividualCollectionUi public class IndividualCollectionUi
{ {
private readonly ActorService _actorService; private readonly ActorService _actorService;
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly CollectionSelector _withEmpty; private readonly CollectionSelector _withEmpty;
public IndividualCollectionUi(ActorService actors, ModCollection.Manager collectionManager, CollectionSelector withEmpty) public IndividualCollectionUi(ActorService actors, CollectionManager collectionManager, CollectionSelector withEmpty)
{ {
_actorService = actors; _actorService = actors;
_collectionManager = collectionManager; _collectionManager = collectionManager;

View file

@ -16,9 +16,9 @@ public class InheritanceUi
private const int InheritedCollectionHeight = 9; private const int InheritedCollectionHeight = 9;
private const string InheritanceDragDropLabel = "##InheritanceMove"; private const string InheritanceDragDropLabel = "##InheritanceMove";
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
public InheritanceUi(ModCollection.Manager collectionManager) public InheritanceUi(CollectionManager collectionManager)
=> _collectionManager = collectionManager; => _collectionManager = collectionManager;
/// <summary> Draw the whole inheritance block. </summary> /// <summary> Draw the whole inheritance block. </summary>

View file

@ -7,7 +7,7 @@ namespace Penumbra.UI.CollectionTab;
public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, string)> public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, string)>
{ {
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
public (CollectionType, string, string)? CurrentType public (CollectionType, string, string)? CurrentType
=> CollectionTypeExtensions.Special[CurrentIdx]; => CollectionTypeExtensions.Special[CurrentIdx];
@ -16,7 +16,7 @@ public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, stri
private readonly float _unscaledWidth; private readonly float _unscaledWidth;
private readonly string _label; private readonly string _label;
public SpecialCombo(ModCollection.Manager collectionManager, string label, float unscaledWidth) public SpecialCombo(CollectionManager collectionManager, string label, float unscaledWidth)
: base(CollectionTypeExtensions.Special, false) : base(CollectionTypeExtensions.Special, false)
{ {
_collectionManager = collectionManager; _collectionManager = collectionManager;

View file

@ -14,12 +14,12 @@ namespace Penumbra.UI;
public class FileDialogService : IDisposable public class FileDialogService : IDisposable
{ {
private readonly Mod.Manager _mods; private readonly ModManager _mods;
private readonly FileDialogManager _manager; private readonly FileDialogManager _manager;
private readonly ConcurrentDictionary<string, string> _startPaths = new(); private readonly ConcurrentDictionary<string, string> _startPaths = new();
private bool _isOpen; private bool _isOpen;
public FileDialogService(Mod.Manager mods, Configuration config) public FileDialogService(ModManager mods, Configuration config)
{ {
_mods = mods; _mods = mods;
_manager = SetupFileManager(config.ModDirectory); _manager = SetupFileManager(config.ModDirectory);

View file

@ -29,8 +29,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
private readonly ChatService _chat; private readonly ChatService _chat;
private readonly Configuration _config; private readonly Configuration _config;
private readonly FileDialogService _fileDialog; private readonly FileDialogService _fileDialog;
private readonly Mod.Manager _modManager; private readonly ModManager _modManager;
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly TutorialService _tutorial; private readonly TutorialService _tutorial;
private readonly ModEditor _modEditor; private readonly ModEditor _modEditor;
@ -38,8 +38,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem, Mod.Manager modManager, public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem, ModManager modManager,
ModCollection.Manager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, ChatService chat, CollectionManager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, ChatService chat,
ModEditor modEditor) ModEditor modEditor)
: base(fileSystem, DalamudServices.KeyState, HandleException) : base(fileSystem, DalamudServices.KeyState, HandleException)
{ {

View file

@ -14,9 +14,9 @@ namespace Penumbra.UI.ModsTab;
public class ModPanelConflictsTab : ITab public class ModPanelConflictsTab : ITab
{ {
private readonly ModFileSystemSelector _selector; private readonly ModFileSystemSelector _selector;
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
public ModPanelConflictsTab(ModCollection.Manager collectionManager, ModFileSystemSelector selector) public ModPanelConflictsTab(CollectionManager collectionManager, ModFileSystemSelector selector)
{ {
_collectionManager = collectionManager; _collectionManager = collectionManager;
_selector = selector; _selector = selector;

View file

@ -13,11 +13,11 @@ public class ModPanelDescriptionTab : ITab
{ {
private readonly ModFileSystemSelector _selector; private readonly ModFileSystemSelector _selector;
private readonly TutorialService _tutorial; private readonly TutorialService _tutorial;
private readonly Mod.Manager _modManager; private readonly ModManager _modManager;
private readonly TagButtons _localTags = new(); private readonly TagButtons _localTags = new();
private readonly TagButtons _modTags = new(); private readonly TagButtons _modTags = new();
public ModPanelDescriptionTab(ModFileSystemSelector selector, TutorialService tutorial, Mod.Manager modManager) public ModPanelDescriptionTab(ModFileSystemSelector selector, TutorialService tutorial, ModManager modManager)
{ {
_selector = selector; _selector = selector;
_tutorial = tutorial; _tutorial = tutorial;

View file

@ -20,7 +20,7 @@ namespace Penumbra.UI.ModsTab;
public class ModPanelEditTab : ITab public class ModPanelEditTab : ITab
{ {
private readonly ChatService _chat; private readonly ChatService _chat;
private readonly Mod.Manager _modManager; private readonly ModManager _modManager;
private readonly ModFileSystem _fileSystem; private readonly ModFileSystem _fileSystem;
private readonly ModFileSystemSelector _selector; private readonly ModFileSystemSelector _selector;
private readonly ModEditWindow _editWindow; private readonly ModEditWindow _editWindow;
@ -33,7 +33,7 @@ public class ModPanelEditTab : ITab
private ModFileSystem.Leaf _leaf = null!; private ModFileSystem.Leaf _leaf = null!;
private Mod _mod = null!; private Mod _mod = null!;
public ModPanelEditTab(Mod.Manager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat, public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat,
ModEditWindow editWindow, ModEditor editor) ModEditWindow editWindow, ModEditor editor)
{ {
_modManager = modManager; _modManager = modManager;
@ -219,7 +219,7 @@ public class ModPanelEditTab : ITab
public static void Reset() public static void Reset()
=> _newGroupName = string.Empty; => _newGroupName = string.Empty;
public static void Draw(Mod.Manager modManager, Mod mod) public static void Draw(ModManager modManager, Mod mod)
{ {
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3));
ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3);
@ -250,15 +250,15 @@ public class ModPanelEditTab : ITab
private static class MoveDirectory private static class MoveDirectory
{ {
private static string? _currentModDirectory; private static string? _currentModDirectory;
private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical; private static ModManager.NewDirectoryState _state = ModManager.NewDirectoryState.Identical;
public static void Reset() public static void Reset()
{ {
_currentModDirectory = null; _currentModDirectory = null;
_state = Mod.Manager.NewDirectoryState.Identical; _state = ModManager.NewDirectoryState.Identical;
} }
public static void Draw(Mod.Manager modManager, Mod mod, Vector2 buttonSize) public static void Draw(ModManager modManager, Mod mod, Vector2 buttonSize)
{ {
ImGui.SetNextItemWidth(buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X); ImGui.SetNextItemWidth(buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X);
var tmp = _currentModDirectory ?? mod.ModPath.Name; var tmp = _currentModDirectory ?? mod.ModPath.Name;
@ -270,13 +270,13 @@ public class ModPanelEditTab : ITab
var (disabled, tt) = _state switch var (disabled, tt) = _state switch
{ {
Mod.Manager.NewDirectoryState.Identical => (true, "Current directory name is identical to new one."), ModManager.NewDirectoryState.Identical => (true, "Current directory name is identical to new one."),
Mod.Manager.NewDirectoryState.Empty => (true, "Please enter a new directory name first."), ModManager.NewDirectoryState.Empty => (true, "Please enter a new directory name first."),
Mod.Manager.NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), ModManager.NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."),
Mod.Manager.NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), ModManager.NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."),
Mod.Manager.NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."), ModManager.NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."),
Mod.Manager.NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."), ModManager.NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."),
Mod.Manager.NewDirectoryState.ContainsInvalidSymbols => (true, ModManager.NewDirectoryState.ContainsInvalidSymbols => (true,
$"{_currentModDirectory} contains invalid symbols for FFXIV."), $"{_currentModDirectory} contains invalid symbols for FFXIV."),
_ => (true, "Unknown error."), _ => (true, "Unknown error."),
}; };
@ -317,7 +317,7 @@ public class ModPanelEditTab : ITab
ImGui.OpenPopup(PopupName); ImGui.OpenPopup(PopupName);
} }
public static void DrawPopup(Mod.Manager modManager) public static void DrawPopup(ModManager modManager)
{ {
if (_mod == null) if (_mod == null)
return; return;

View file

@ -18,11 +18,11 @@ namespace Penumbra.UI.ModsTab;
public class ModPanelSettingsTab : ITab public class ModPanelSettingsTab : ITab
{ {
private readonly Configuration _config; private readonly Configuration _config;
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly ModFileSystemSelector _selector; private readonly ModFileSystemSelector _selector;
private readonly TutorialService _tutorial; private readonly TutorialService _tutorial;
private readonly PenumbraApi _api; private readonly PenumbraApi _api;
private readonly Mod.Manager _modManager; private readonly ModManager _modManager;
private bool _inherited; private bool _inherited;
private ModSettings _settings = null!; private ModSettings _settings = null!;
@ -30,7 +30,7 @@ public class ModPanelSettingsTab : ITab
private bool _empty; private bool _empty;
private int? _currentPriority = null; private int? _currentPriority = null;
public ModPanelSettingsTab(ModCollection.Manager collectionManager, Mod.Manager modManager, ModFileSystemSelector selector, public ModPanelSettingsTab(CollectionManager collectionManager, ModManager modManager, ModFileSystemSelector selector,
TutorialService tutorial, PenumbraApi api, Configuration config) TutorialService tutorial, PenumbraApi api, Configuration config)
{ {
_collectionManager = collectionManager; _collectionManager = collectionManager;

View file

@ -27,7 +27,7 @@ public class ModPanelTabBar
public readonly ModPanelChangedItemsTab ChangedItems; public readonly ModPanelChangedItemsTab ChangedItems;
public readonly ModPanelEditTab Edit; public readonly ModPanelEditTab Edit;
private readonly ModEditWindow _modEditWindow; private readonly ModEditWindow _modEditWindow;
private readonly Mod.Manager _modManager; private readonly ModManager _modManager;
private readonly TutorialService _tutorial; private readonly TutorialService _tutorial;
public readonly ITab[] Tabs; public readonly ITab[] Tabs;
@ -35,7 +35,7 @@ public class ModPanelTabBar
private Mod? _lastMod = null; private Mod? _lastMod = null;
public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description, public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description,
ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, Mod.Manager modManager, ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, ModManager modManager,
TutorialService tutorial) TutorialService tutorial)
{ {
_modEditWindow = modEditWindow; _modEditWindow = modEditWindow;
@ -107,7 +107,7 @@ public class ModPanelTabBar
if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip)) if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip))
{ {
_modEditWindow.ChangeMod(mod); _modEditWindow.ChangeMod(mod);
_modEditWindow.ChangeOption((Mod.SubMod) mod.Default); _modEditWindow.ChangeOption((SubMod) mod.Default);
_modEditWindow.IsOpen = true; _modEditWindow.IsOpen = true;
} }

View file

@ -16,10 +16,10 @@ namespace Penumbra.UI.Tabs;
public class ChangedItemsTab : ITab public class ChangedItemsTab : ITab
{ {
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly PenumbraApi _api; private readonly PenumbraApi _api;
public ChangedItemsTab(ModCollection.Manager collectionManager, PenumbraApi api) public ChangedItemsTab(CollectionManager collectionManager, PenumbraApi api)
{ {
_collectionManager = collectionManager; _collectionManager = collectionManager;
_api = api; _api = api;

View file

@ -17,7 +17,7 @@ public class CollectionsTab : IDisposable, ITab
{ {
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
private readonly Configuration _config; private readonly Configuration _config;
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly TutorialService _tutorial; private readonly TutorialService _tutorial;
private readonly SpecialCombo _specialCollectionCombo; private readonly SpecialCombo _specialCollectionCombo;
@ -26,7 +26,7 @@ public class CollectionsTab : IDisposable, ITab
private readonly InheritanceUi _inheritance; private readonly InheritanceUi _inheritance;
private readonly IndividualCollectionUi _individualCollections; private readonly IndividualCollectionUi _individualCollections;
public CollectionsTab(ActorService actorService, CommunicatorService communicator, ModCollection.Manager collectionManager, public CollectionsTab(ActorService actorService, CommunicatorService communicator, CollectionManager collectionManager,
TutorialService tutorial, Configuration config) TutorialService tutorial, Configuration config)
{ {
_communicator = communicator; _communicator = communicator;

View file

@ -34,8 +34,8 @@ public class DebugTab : ITab
private readonly StartTracker _timer; private readonly StartTracker _timer;
private readonly PerformanceTracker _performance; private readonly PerformanceTracker _performance;
private readonly Configuration _config; private readonly Configuration _config;
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly Mod.Manager _modManager; private readonly ModManager _modManager;
private readonly ValidityChecker _validityChecker; private readonly ValidityChecker _validityChecker;
private readonly HttpApi _httpApi; private readonly HttpApi _httpApi;
private readonly ActorService _actorService; private readonly ActorService _actorService;
@ -52,8 +52,8 @@ public class DebugTab : ITab
private readonly IdentifiedCollectionCache _identifiedCollectionCache; private readonly IdentifiedCollectionCache _identifiedCollectionCache;
private readonly CutsceneService _cutsceneService; private readonly CutsceneService _cutsceneService;
public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager, public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager,
ValidityChecker validityChecker, Mod.Manager modManager, HttpApi httpApi, ActorService actorService, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService,
DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources,
ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver,
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,

View file

@ -17,9 +17,9 @@ namespace Penumbra.UI.Tabs;
public class EffectiveTab : ITab public class EffectiveTab : ITab
{ {
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
public EffectiveTab(ModCollection.Manager collectionManager) public EffectiveTab(CollectionManager collectionManager)
=> _collectionManager = collectionManager; => _collectionManager = collectionManager;
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label

View file

@ -23,13 +23,13 @@ public class ModsTab : ITab
private readonly ModFileSystemSelector _selector; private readonly ModFileSystemSelector _selector;
private readonly ModPanel _panel; private readonly ModPanel _panel;
private readonly TutorialService _tutorial; private readonly TutorialService _tutorial;
private readonly Mod.Manager _modManager; private readonly ModManager _modManager;
private readonly ModCollection.Manager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly RedrawService _redrawService; private readonly RedrawService _redrawService;
private readonly Configuration _config; private readonly Configuration _config;
private readonly CollectionsTab _collectionsTab; private readonly CollectionsTab _collectionsTab;
public ModsTab(Mod.Manager modManager, ModCollection.Manager collectionManager, ModFileSystemSelector selector, ModPanel panel, public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel,
TutorialService tutorial, RedrawService redrawService, Configuration config, CollectionsTab collectionsTab) TutorialService tutorial, RedrawService redrawService, Configuration config, CollectionsTab collectionsTab)
{ {
_modManager = modManager; _modManager = modManager;

View file

@ -31,14 +31,14 @@ public class SettingsTab : ITab
private readonly TutorialService _tutorial; private readonly TutorialService _tutorial;
private readonly Penumbra _penumbra; private readonly Penumbra _penumbra;
private readonly FileDialogService _fileDialog; private readonly FileDialogService _fileDialog;
private readonly Mod.Manager _modManager; private readonly ModManager _modManager;
private readonly ModFileSystemSelector _selector; private readonly ModFileSystemSelector _selector;
private readonly CharacterUtility _characterUtility; private readonly CharacterUtility _characterUtility;
private readonly ResidentResourceManager _residentResources; private readonly ResidentResourceManager _residentResources;
private readonly DalamudServices _dalamud; private readonly DalamudServices _dalamud;
public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra,
FileDialogService fileDialog, Mod.Manager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility,
ResidentResourceManager residentResources, DalamudServices dalamud) ResidentResourceManager residentResources, DalamudServices dalamud)
{ {
_config = config; _config = config;