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 Temporary _temporary;
public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, Mod.Manager modManager)
public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, ModManager modManager)
{
_ipcProviders = ipcProviders;
_pluginState = new PluginState(pi);
@ -1139,9 +1139,9 @@ public class IpcTester : IDisposable
private class Temporary
{
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;
_modManager = modManager;

View file

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

View file

@ -112,7 +112,7 @@ public class PenumbraIpcProviders : IDisposable
internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll;
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;

View file

@ -1,422 +1,419 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.Mods;
using Penumbra.UI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Dalamud.Interface.Internal.Notifications;
using Penumbra.GameData.Actors;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Collections;
public partial class ModCollection
{
public sealed partial class Manager : ISavable
{
public const int Version = 1;
// The collection currently selected for changing settings.
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())
return _specialCollections[(int)type];
return type switch
{
CollectionType.Default => Default,
CollectionType.Interface => Interface,
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.Current => Current.Index,
CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count
? -1
: Individuals[individualIndex].Collection.Index,
_ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index,
_ => -1,
};
if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx)
return;
var newCollection = this[newIdx];
if (newIdx > Empty.Index)
newCollection.CreateCache(collectionType is CollectionType.Default);
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)
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)
{
_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);
if (idx < 0)
{
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);
configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this);
// 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;
// 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 {Empty.Name}.", "Load Failure",
NotificationType.Warning);
dict.Add(player, Empty);
}
else
{
dict.Add(player, this[idx]);
}
}
Individuals.Migrate0To1(dict);
return true;
}
// Read the active collection file into a jObject.
// Returns true if this is successful, false if the file does not exist or it is unsuccessful.
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();
return false;
}
// Save if any of the active collections is changed.
private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3)
{
if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary)
Penumbra.SaveService.QueueSave(this);
}
// Cache handling. Usually recreate caches on the next framework tick,
// but at launch create all of them at once.
public void CreateNecessaryCaches()
{
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();
Task.WaitAll(tasks);
}
private void RemoveCache(int idx)
{
if (idx != Empty.Index
&& 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();
}
// Recalculate effective files for active collections on events.
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);
}
private void OnModRemovedActive(Mod mod)
{
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)
{
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
collection._cache!.ReloadMod(mod, true);
}
public string ToFilename(FilenameService fileNames)
=> fileNames.ActiveCollectionsFile;
public string TypeName
=> "Active Collections";
public string LogName(string _)
=> "to file";
public void Save(StreamWriter writer)
{
var jObj = new JObject
{
{ nameof(Version), Version },
{ nameof(Default), Default.Name },
{ nameof(Interface), Interface.Name },
{ nameof(Current), Current.Name },
};
foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null)
.Select(p => ((CollectionType)p.Index, p.Value!)))
jObj.Add(type.ToString(), collection.Name);
jObj.Add(nameof(Individuals), Individuals.ToJObject());
using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
jObj.WriteTo(j);
}
}
}
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.Mods;
using Penumbra.UI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Dalamud.Interface.Internal.Notifications;
using Penumbra.GameData.Actors;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Collections;
public sealed partial class CollectionManager : 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)
{
if (type.IsSpecial())
return _specialCollections[(int)type];
return type switch
{
CollectionType.Default => Default,
CollectionType.Interface => Interface,
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.Current => Current.Index,
CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count
? -1
: Individuals[individualIndex].Collection.Index,
_ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index,
_ => -1,
};
if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx)
return;
var newCollection = this[newIdx];
if (newIdx > ModCollection.Empty.Index)
newCollection.CreateCache(collectionType is CollectionType.Default);
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)
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)
{
_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 ? 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];
}
// 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 {ModCollection.Empty.Name}.",
"Load Failure", NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
}
else
{
Interface = this[interfaceIdx];
}
// Load the current collection.
var currentName = jObject[nameof(Current)]?.ToObject<string>() ?? ModCollection.DefaultCollection;
var currentIdx = GetIndexForCollectionName(currentName);
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(typeName);
if (idx < 0)
{
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);
configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this);
// 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;
// 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]);
}
}
Individuals.Migrate0To1(dict);
return true;
}
// Read the active collection file into a jObject.
// Returns true if this is successful, false if the file does not exist or it is unsuccessful.
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();
return false;
}
// Save if any of the active collections is changed.
private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3)
{
if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary)
Penumbra.SaveService.QueueSave(this);
}
// Cache handling. Usually recreate caches on the next framework tick,
// but at launch create all of them at once.
public void CreateNecessaryCaches()
{
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();
Task.WaitAll(tasks);
}
private void RemoveCache(int idx)
{
if (idx != ModCollection.Empty.Index
&& 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();
}
// Recalculate effective files for active collections on events.
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);
}
private void OnModRemovedActive(Mod mod)
{
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)
{
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
collection._cache!.ReloadMod(mod, true);
}
public string ToFilename(FilenameService fileNames)
=> fileNames.ActiveCollectionsFile;
public string TypeName
=> "Active Collections";
public string LogName(string _)
=> "to file";
public void Save(StreamWriter writer)
{
var jObj = new JObject
{
{ nameof(Version), Version },
{ nameof(Default), Default.Name },
{ nameof(Interface), Interface.Name },
{ nameof(Current), Current.Name },
};
foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null)
.Select(p => ((CollectionType)p.Index, p.Value!)))
jObj.Add(type.ToString(), collection.Name);
jObj.Add(nameof(Individuals), Individuals.ToJObject());
using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
jObj.WriteTo(j);
}
}

View file

@ -17,462 +17,459 @@ using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
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;
private readonly CommunicatorService _communicator;
private readonly CharacterUtility _characterUtility;
private readonly ResidentResourceManager _residentResources;
private readonly Configuration _config;
ModCollection.Empty,
};
public ModCollection this[Index idx]
=> _collections[idx];
// The empty collection is always available and always has index 0.
// It can not be deleted or moved.
private readonly List<ModCollection> _collections = new()
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 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,
};
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();
fixedName = string.Empty;
return false;
}
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;
_communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange;
_modManager.ModDiscoveryStarted -= OnModDiscoveryStarted;
_modManager.ModDiscoveryFinished -= OnModDiscoveryFinished;
_modManager.ModOptionChanged -= OnModOptionsChanged;
_modManager.ModPathChanged -= OnModPathChange;
fixedName = string.Empty;
return false;
}
private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed)
=> TempModManager.OnGlobalModChange(_collections, mod, created, removed);
fixedName = name;
return true;
}
// 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)
// Add a new collection of the given name.
// If duplicate is not-null, the new collection will be a duplicate of it.
// 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))
{
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;
Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists.");
return false;
}
// Add a new collection of the given name.
// If duplicate is not-null, the new collection will be a duplicate of it.
// 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)
var newCollection = duplicate?.Duplicate(name) ?? ModCollection.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 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.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;
Penumbra.Log.Error("Can not remove the empty collection.");
return false;
}
// 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 == DefaultName.Index)
{
if (idx <= Empty.Index || idx >= _collections.Count)
{
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;
Penumbra.Log.Error("Can not remove the default collection.");
return false;
}
public bool RemoveCollection(ModCollection collection)
=> RemoveCollection(collection.Index);
if (idx == Current.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)
collection.PrepareModDiscovery();
if (idx == _specialCollections[i]?.Index)
SetCollection(ModCollection.Empty, (CollectionType)i);
}
private void OnModDiscoveryFinished()
for (var i = 0; i < Individuals.Count; ++i)
{
// 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();
if (Individuals[i].Collection.Index == idx)
SetCollection(ModCollection.Empty, CollectionType.Individual, i);
}
var collection = _collections[idx];
// A changed mod path forces changes for all collections, active and inactive.
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory)
// 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)
{
switch (type)
{
case ModPathChangeType.Added:
foreach (var collection in this)
collection.AddMod(mod);
var inheritedIdx = c._inheritance.IndexOf(collection);
if (inheritedIdx >= 0)
c.RemoveInheritance(inheritedIdx);
OnModAddedActive(mod);
break;
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);
}
if (c.Index > idx)
--c.Index;
}
// 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)
Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}.");
_communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
return true;
}
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.
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)
case ModPathChangeType.Added:
foreach (var collection in this)
{
if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false)
Penumbra.SaveService.QueueSave(collection);
}
collection.AddMod(mod);
// 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);
}
}
}
OnModAddedActive(mod);
break;
case ModPathChangeType.Deleted:
OnModRemovedActive(mod);
foreach (var collection in this)
collection.RemoveMod(mod, mod.Index);
// 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(DefaultCollection);
if (idx >= 0)
{
DefaultName = this[idx];
return;
}
break;
case ModPathChangeType.Moved:
OnModMovedActive(mod);
foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
Penumbra.SaveService.QueueSave(collection);
var defaultCollection = CreateNewEmpty(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 = 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;
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.
// 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;
}
public bool ReadJObject( JArray? obj, ModCollection.Manager manager )
public bool ReadJObject( JArray? obj, CollectionManager manager )
{
if( obj == null )
{

View file

@ -8,7 +8,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Penumbra.Interop;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
@ -20,27 +19,27 @@ namespace Penumbra.Collections;
public partial class ModCollection
{
// Only active collections need to have a cache.
private Cache? _cache;
internal ModCollectionCache? _cache;
public bool HasCache
=> _cache != null;
// Count the number of changes of the effective file list.
// 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.
private void CreateCache(bool isDefault)
internal void CreateCache(bool isDefault)
{
if (_cache == null)
{
CalculateEffectiveFileList(isDefault);
Penumbra.Log.Verbose($"Created new cache for collection {Name}.");
}
if (_cache != null)
return;
CalculateEffectiveFileList(isDefault);
Penumbra.Log.Verbose($"Created new cache for collection {Name}.");
}
// Force an update with metadata for this cache.
private void ForceCacheUpdate()
internal void ForceCacheUpdate()
=> CalculateEffectiveFileList(this == Penumbra.CollectionManager.Default);
// Handle temporary mods for this collection.
@ -83,7 +82,7 @@ public partial class ModCollection
}
[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)
return true;
@ -127,14 +126,14 @@ public partial class ModCollection
=> Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name, () =>
CalculateEffectiveFileListInternal(isDefault));
private void CalculateEffectiveFileListInternal(bool isDefault)
internal void CalculateEffectiveFileListInternal(bool isDefault)
{
// Skip the empty collection.
if (Index == 0)
return;
Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}");
_cache ??= new Cache(this);
_cache ??= new ModCollectionCache(this);
_cache.FullRecalculation(isDefault);
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,
// 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>();
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.
public event Action< bool > InheritanceChanged;
private readonly List< ModCollection > _inheritance = new();
internal readonly List< ModCollection > _inheritance = new();
public IReadOnlyList< ModCollection > Inheritance
=> _inheritance;
@ -98,7 +98,7 @@ public partial class ModCollection
Penumbra.Log.Debug( $"Removed {inheritance.AnonymizedName} from {AnonymizedName} inheritances." );
}
private void ClearSubscriptions( ModCollection other )
internal void ClearSubscriptions( ModCollection other )
{
other.ModSettingChanged -= OnInheritedModSettingChange;
other.InheritanceChanged -= OnInheritedInheritanceChange;

View file

@ -23,18 +23,18 @@ public partial class ModCollection
// The collection name can contain invalid path characters,
// 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).
public string AnonymizedName
=> this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})";
public int Version { get; private set; }
public int Index { get; private set; } = -1;
public int Version { get; internal set; }
public int Index { get; internal set; } = -1;
// 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.
private readonly List<ModSettings?> _settings;
internal readonly List<ModSettings?> _settings;
public IReadOnlyList<ModSettings?> 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.
private bool AddMod(Mod mod)
internal bool AddMod(Mod mod)
{
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.
private void RemoveMod(Mod mod, int idx)
internal void RemoveMod(Mod mod, int idx)
{
var settings = _settings[idx];
if (settings != null)
@ -150,7 +150,7 @@ public partial class ModCollection
}
// 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))
_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.
// Also fixes invalid settings.
private void ApplyModSettings()
internal void ApplyModSettings()
{
_settings.Capacity = Math.Max(_settings.Capacity, Penumbra.ModManager.Count);
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 ConfigWindow _configWindow;
private readonly ActorManager _actors;
private readonly Mod.Manager _modManager;
private readonly ModCollection.Manager _collectionManager;
private readonly ModManager _modManager;
private readonly CollectionManager _collectionManager;
private readonly Penumbra _penumbra;
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;
_redrawService = redrawService;

View file

@ -36,10 +36,10 @@ public partial class TexToolsImporter : IDisposable
private readonly Configuration _config;
private readonly ModEditor _editor;
private readonly Mod.Manager _modManager;
private readonly ModManager _modManager;
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;
_tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName );

View file

@ -31,12 +31,12 @@ public unsafe class CollectionResolver
private readonly CutsceneService _cutscenes;
private readonly Configuration _config;
private readonly ModCollection.Manager _collectionManager;
private readonly CollectionManager _collectionManager;
private readonly TempCollectionManager _tempCollections;
private readonly DrawObjectState _drawObjectState;
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)
{
_performance = performance;

View file

@ -16,7 +16,7 @@ public class PathResolver : IDisposable
{
private readonly PerformanceTracker _performance;
private readonly Configuration _config;
private readonly ModCollection.Manager _collectionManager;
private readonly CollectionManager _collectionManager;
private readonly TempCollectionManager _tempCollections;
private readonly ResourceLoader _loader;
@ -25,7 +25,7 @@ public class PathResolver : IDisposable
private readonly PathState _pathState;
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,
PathState pathState, MetaState metaState)
{

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,202 +4,199 @@ using System.Linq;
namespace Penumbra.Mods;
public partial class Mod
public partial class ModManager
{
public partial class Manager
public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory);
public event ModPathChangeDelegate ModPathChanged;
// Rename/Move a mod directory.
// Updates all collection settings and sort order settings.
public void MoveModDirectory(int idx, string newName)
{
public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory);
var mod = this[idx];
var oldName = mod.Name;
var oldDirectory = mod.ModPath;
public event ModPathChangeDelegate ModPathChanged;
// Rename/Move a mod directory.
// Updates all collection settings and sort order settings.
public void MoveModDirectory(int idx, string newName)
switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir))
{
var mod = this[idx];
var oldName = mod.Name;
var oldDirectory = mod.ModPath;
switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir))
{
case NewDirectoryState.NonExisting:
// Nothing to do
break;
case NewDirectoryState.ExistsEmpty:
try
{
Directory.Delete(dir!.FullName);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}");
return;
}
break;
// Should be caught beforehand.
case NewDirectoryState.ExistsNonEmpty:
case NewDirectoryState.ExistsAsFile:
case NewDirectoryState.ContainsInvalidSymbols:
// Nothing to do at all.
case NewDirectoryState.Identical:
default:
return;
}
try
{
Directory.Move(oldDirectory.FullName, dir!.FullName);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}");
return;
}
DataEditor.MoveDataFile(oldDirectory, dir);
new ModBackup(this, mod).Move(null, dir.Name);
dir.Refresh();
mod.ModPath = dir;
if (!mod.Reload(this, false, out var metaChange))
{
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
return;
}
ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir);
if (metaChange != ModDataChangeType.None)
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
}
/// <summary>
/// Reload a mod without changing its base directory.
/// If the base directory does not exist anymore, the mod will be deleted.
/// </summary>
public void ReloadMod(int idx)
{
var mod = this[idx];
var oldName = mod.Name;
ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
if (!mod.Reload(this, true, out var metaChange))
{
Penumbra.Log.Warning(mod.Name.Length == 0
? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead."
: $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead.");
DeleteMod(idx);
return;
}
ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath);
if (metaChange != ModDataChangeType.None)
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
}
/// <summary>
/// Delete a mod by its index. The event is invoked before the mod is removed from the list.
/// Deletes from filesystem as well as from internal data.
/// Updates indices of later mods.
/// </summary>
public void DeleteMod(int idx)
{
var mod = this[idx];
if (Directory.Exists(mod.ModPath.FullName))
case NewDirectoryState.NonExisting:
// Nothing to do
break;
case NewDirectoryState.ExistsEmpty:
try
{
Directory.Delete(mod.ModPath.FullName, true);
Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}.");
Directory.Delete(dir!.FullName);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}");
Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}");
return;
}
ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null);
_mods.RemoveAt(idx);
foreach (var remainingMod in _mods.Skip(idx))
--remainingMod.Index;
Penumbra.Log.Debug($"Deleted mod {mod.Name}.");
}
/// <summary> Load a new mod and add it to the manager if successful. </summary>
public void AddMod(DirectoryInfo modFolder)
{
if (_mods.Any(m => m.ModPath.Name == modFolder.Name))
break;
// Should be caught beforehand.
case NewDirectoryState.ExistsNonEmpty:
case NewDirectoryState.ExistsAsFile:
case NewDirectoryState.ContainsInvalidSymbols:
// Nothing to do at all.
case NewDirectoryState.Identical:
default:
return;
Creator.SplitMultiGroups(modFolder);
var mod = LoadMod(this, modFolder, true);
if (mod == null)
return;
mod.Index = _mods.Count;
_mods.Add(mod);
ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath);
Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}.");
}
public enum NewDirectoryState
try
{
NonExisting,
ExistsEmpty,
ExistsNonEmpty,
ExistsAsFile,
ContainsInvalidSymbols,
Identical,
Empty,
Directory.Move(oldDirectory.FullName, dir!.FullName);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}");
return;
}
/// <summary> Return the state of the new potential name of a directory. </summary>
public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory)
DataEditor.MoveDataFile(oldDirectory, dir);
new ModBackup(this, mod).Move(null, dir.Name);
dir.Refresh();
mod.ModPath = dir;
if (!mod.Reload(this, false, out var metaChange))
{
directory = null;
if (newName.Length == 0)
return NewDirectoryState.Empty;
if (oldName == newName)
return NewDirectoryState.Identical;
var fixedNewName = Creator.ReplaceBadXivSymbols(newName);
if (fixedNewName != newName)
return NewDirectoryState.ContainsInvalidSymbols;
directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName));
if (File.Exists(directory.FullName))
return NewDirectoryState.ExistsAsFile;
if (!Directory.Exists(directory.FullName))
return NewDirectoryState.NonExisting;
if (directory.EnumerateFileSystemInfos().Any())
return NewDirectoryState.ExistsNonEmpty;
return NewDirectoryState.ExistsEmpty;
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
return;
}
ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir);
if (metaChange != ModDataChangeType.None)
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
}
/// <summary> Add new mods to NewMods and remove deleted mods from NewMods. </summary>
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory)
/// <summary>
/// Reload a mod without changing its base directory.
/// If the base directory does not exist anymore, the mod will be deleted.
/// </summary>
public void ReloadMod(int idx)
{
var mod = this[idx];
var oldName = mod.Name;
ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
if (!mod.Reload(this, true, out var metaChange))
{
switch (type)
Penumbra.Log.Warning(mod.Name.Length == 0
? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead."
: $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead.");
DeleteMod(idx);
return;
}
ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath);
if (metaChange != ModDataChangeType.None)
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
}
/// <summary>
/// Delete a mod by its index. The event is invoked before the mod is removed from the list.
/// Deletes from filesystem as well as from internal data.
/// Updates indices of later mods.
/// </summary>
public void DeleteMod(int idx)
{
var mod = this[idx];
if (Directory.Exists(mod.ModPath.FullName))
try
{
case ModPathChangeType.Added:
NewMods.Add(mod);
break;
case ModPathChangeType.Deleted:
NewMods.Remove(mod);
break;
case ModPathChangeType.Moved:
if (oldDirectory != null && newDirectory != null)
DataEditor.MoveDataFile(oldDirectory, newDirectory);
break;
Directory.Delete(mod.ModPath.FullName, true);
Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}.");
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}");
}
ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null);
_mods.RemoveAt(idx);
foreach (var remainingMod in _mods.Skip(idx))
--remainingMod.Index;
Penumbra.Log.Debug($"Deleted mod {mod.Name}.");
}
/// <summary> Load a new mod and add it to the manager if successful. </summary>
public void AddMod(DirectoryInfo modFolder)
{
if (_mods.Any(m => m.ModPath.Name == modFolder.Name))
return;
Mod.Creator.SplitMultiGroups(modFolder);
var mod = Mod.LoadMod(this, modFolder, true);
if (mod == null)
return;
mod.Index = _mods.Count;
_mods.Add(mod);
ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath);
Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}.");
}
public enum NewDirectoryState
{
NonExisting,
ExistsEmpty,
ExistsNonEmpty,
ExistsAsFile,
ContainsInvalidSymbols,
Identical,
Empty,
}
/// <summary> Return the state of the new potential name of a directory. </summary>
public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory)
{
directory = null;
if (newName.Length == 0)
return NewDirectoryState.Empty;
if (oldName == newName)
return NewDirectoryState.Identical;
var fixedNewName = Mod.Creator.ReplaceBadXivSymbols(newName);
if (fixedNewName != newName)
return NewDirectoryState.ContainsInvalidSymbols;
directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName));
if (File.Exists(directory.FullName))
return NewDirectoryState.ExistsAsFile;
if (!Directory.Exists(directory.FullName))
return NewDirectoryState.NonExisting;
if (directory.EnumerateFileSystemInfos().Any())
return NewDirectoryState.ExistsNonEmpty;
return NewDirectoryState.ExistsEmpty;
}
/// <summary> Add new mods to NewMods and remove deleted mods from NewMods. </summary>
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory)
{
switch (type)
{
case ModPathChangeType.Added:
NewMods.Add(mod);
break;
case ModPathChangeType.Deleted:
NewMods.Remove(mod);
break;
case ModPathChangeType.Moved:
if (oldDirectory != null && newDirectory != null)
DataEditor.MoveDataFile(oldDirectory, newDirectory);
break;
}
}
}

View file

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

View file

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

View file

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

View file

@ -7,73 +7,70 @@ using Penumbra.Util;
namespace Penumbra.Mods;
public sealed partial class Mod
public sealed partial class ModManager : IReadOnlyList<Mod>
{
public sealed partial class Manager : IReadOnlyList<Mod>
// Set when reading Config and migrating from v4 to v5.
public static bool MigrateModBackups = false;
// An easily accessible set of new mods.
// Mods are added when they are created or imported.
// Mods are removed when they are deleted or when they are toggled in any collection.
// Also gets cleared on mod rediscovery.
public readonly HashSet<Mod> NewMods = new();
private readonly List<Mod> _mods = new();
public Mod this[int idx]
=> _mods[idx];
public Mod this[Index idx]
=> _mods[idx];
public int Count
=> _mods.Count;
public IEnumerator<Mod> GetEnumerator()
=> _mods.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
public readonly ModDataEditor DataEditor;
public ModManager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor)
{
// Set when reading Config and migrating from v4 to v5.
public static bool MigrateModBackups = false;
using var timer = time.Measure(StartTimeType.Mods);
_config = config;
_communicator = communicator;
DataEditor = dataEditor;
ModDirectoryChanged += OnModDirectoryChange;
SetBaseDirectory(config.ModDirectory, true);
UpdateExportDirectory(_config.ExportDirectory, false);
ModOptionChanged += OnModOptionChange;
ModPathChanged += OnModPathChange;
DiscoverMods();
}
// An easily accessible set of new mods.
// Mods are added when they are created or imported.
// Mods are removed when they are deleted or when they are toggled in any collection.
// Also gets cleared on mod rediscovery.
public readonly HashSet<Mod> NewMods = new();
private readonly List<Mod> _mods = new();
public Mod this[int idx]
=> _mods[idx];
public Mod this[Index idx]
=> _mods[idx];
public int Count
=> _mods.Count;
public IEnumerator<Mod> GetEnumerator()
=> _mods.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
public readonly ModDataEditor DataEditor;
public Manager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor)
// Try to obtain a mod by its directory name (unique identifier, preferred),
// or the first mod of the given name if no directory fits.
public bool TryGetMod(string modDirectory, string modName, [NotNullWhen(true)] out Mod? mod)
{
mod = null;
foreach (var m in _mods)
{
using var timer = time.Measure(StartTimeType.Mods);
_config = config;
_communicator = communicator;
DataEditor = dataEditor;
ModDirectoryChanged += OnModDirectoryChange;
SetBaseDirectory(config.ModDirectory, true);
UpdateExportDirectory(_config.ExportDirectory, false);
ModOptionChanged += OnModOptionChange;
ModPathChanged += OnModPathChange;
DiscoverMods();
}
// Try to obtain a mod by its directory name (unique identifier, preferred),
// or the first mod of the given name if no directory fits.
public bool TryGetMod(string modDirectory, string modName, [NotNullWhen(true)] out Mod? mod)
{
mod = null;
foreach (var m in _mods)
if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase))
{
if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase))
{
mod = m;
return true;
}
if (m.Name == modName)
mod ??= m;
mod = m;
return true;
}
return mod != null;
if (m.Name == modName)
mod ??= m;
}
return mod != null;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,12 +25,12 @@ public class ItemSwapTab : IDisposable, ITab
{
private readonly CommunicatorService _communicator;
private readonly ItemService _itemService;
private readonly ModCollection.Manager _collectionManager;
private readonly Mod.Manager _modManager;
private readonly CollectionManager _collectionManager;
private readonly ModManager _modManager;
private readonly Configuration _config;
public ItemSwapTab(CommunicatorService communicator, ItemService itemService, ModCollection.Manager collectionManager,
Mod.Manager modManager, Configuration config)
public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager,
ModManager modManager, Configuration config)
{
_communicator = communicator;
_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.";
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)
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!);
var fileRegistry = _editor.Files.Available.First(file => file.File.FullName == _targetPath);
_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;
}

View file

@ -9,9 +9,9 @@ namespace Penumbra.UI.CollectionTab;
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)
=> _collectionManager = manager;

View file

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

View file

@ -16,9 +16,9 @@ public class InheritanceUi
private const int InheritedCollectionHeight = 9;
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;
/// <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)>
{
private readonly ModCollection.Manager _collectionManager;
private readonly CollectionManager _collectionManager;
public (CollectionType, string, string)? CurrentType
=> CollectionTypeExtensions.Special[CurrentIdx];
@ -16,7 +16,7 @@ public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, stri
private readonly float _unscaledWidth;
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)
{
_collectionManager = collectionManager;

View file

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

View file

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

View file

@ -14,9 +14,9 @@ namespace Penumbra.UI.ModsTab;
public class ModPanelConflictsTab : ITab
{
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;
_selector = selector;

View file

@ -13,11 +13,11 @@ public class ModPanelDescriptionTab : ITab
{
private readonly ModFileSystemSelector _selector;
private readonly TutorialService _tutorial;
private readonly Mod.Manager _modManager;
private readonly ModManager _modManager;
private readonly TagButtons _localTags = 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;
_tutorial = tutorial;

View file

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

View file

@ -18,11 +18,11 @@ namespace Penumbra.UI.ModsTab;
public class ModPanelSettingsTab : ITab
{
private readonly Configuration _config;
private readonly ModCollection.Manager _collectionManager;
private readonly CollectionManager _collectionManager;
private readonly ModFileSystemSelector _selector;
private readonly TutorialService _tutorial;
private readonly PenumbraApi _api;
private readonly Mod.Manager _modManager;
private readonly ModManager _modManager;
private bool _inherited;
private ModSettings _settings = null!;
@ -30,7 +30,7 @@ public class ModPanelSettingsTab : ITab
private bool _empty;
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)
{
_collectionManager = collectionManager;

View file

@ -27,7 +27,7 @@ public class ModPanelTabBar
public readonly ModPanelChangedItemsTab ChangedItems;
public readonly ModPanelEditTab Edit;
private readonly ModEditWindow _modEditWindow;
private readonly Mod.Manager _modManager;
private readonly ModManager _modManager;
private readonly TutorialService _tutorial;
public readonly ITab[] Tabs;
@ -35,7 +35,7 @@ public class ModPanelTabBar
private Mod? _lastMod = null;
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)
{
_modEditWindow = modEditWindow;
@ -107,7 +107,7 @@ public class ModPanelTabBar
if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip))
{
_modEditWindow.ChangeMod(mod);
_modEditWindow.ChangeOption((Mod.SubMod) mod.Default);
_modEditWindow.ChangeOption((SubMod) mod.Default);
_modEditWindow.IsOpen = true;
}

View file

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

View file

@ -17,7 +17,7 @@ public class CollectionsTab : IDisposable, ITab
{
private readonly CommunicatorService _communicator;
private readonly Configuration _config;
private readonly ModCollection.Manager _collectionManager;
private readonly CollectionManager _collectionManager;
private readonly TutorialService _tutorial;
private readonly SpecialCombo _specialCollectionCombo;
@ -26,7 +26,7 @@ public class CollectionsTab : IDisposable, ITab
private readonly InheritanceUi _inheritance;
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)
{
_communicator = communicator;

View file

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

View file

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

View file

@ -23,13 +23,13 @@ public class ModsTab : ITab
private readonly ModFileSystemSelector _selector;
private readonly ModPanel _panel;
private readonly TutorialService _tutorial;
private readonly Mod.Manager _modManager;
private readonly ModCollection.Manager _collectionManager;
private readonly ModManager _modManager;
private readonly CollectionManager _collectionManager;
private readonly RedrawService _redrawService;
private readonly Configuration _config;
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)
{
_modManager = modManager;

View file

@ -31,14 +31,14 @@ public class SettingsTab : ITab
private readonly TutorialService _tutorial;
private readonly Penumbra _penumbra;
private readonly FileDialogService _fileDialog;
private readonly Mod.Manager _modManager;
private readonly ModManager _modManager;
private readonly ModFileSystemSelector _selector;
private readonly CharacterUtility _characterUtility;
private readonly ResidentResourceManager _residentResources;
private readonly DalamudServices _dalamud;
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)
{
_config = config;