mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-14 04:34:19 +01:00
Move Mod.Manager and ModCollection.Manager to outer scope and required changes.
This commit is contained in:
parent
ccdafcf85d
commit
1253079968
59 changed files with 2562 additions and 2615 deletions
|
|
@ -38,7 +38,7 @@ public class IpcTester : IDisposable
|
||||||
private readonly ModSettings _modSettings;
|
private readonly ModSettings _modSettings;
|
||||||
private readonly Temporary _temporary;
|
private readonly Temporary _temporary;
|
||||||
|
|
||||||
public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, Mod.Manager modManager)
|
public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, ModManager modManager)
|
||||||
{
|
{
|
||||||
_ipcProviders = ipcProviders;
|
_ipcProviders = ipcProviders;
|
||||||
_pluginState = new PluginState(pi);
|
_pluginState = new PluginState(pi);
|
||||||
|
|
@ -1139,9 +1139,9 @@ public class IpcTester : IDisposable
|
||||||
private class Temporary
|
private class Temporary
|
||||||
{
|
{
|
||||||
private readonly DalamudPluginInterface _pi;
|
private readonly DalamudPluginInterface _pi;
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
|
|
||||||
public Temporary(DalamudPluginInterface pi, Mod.Manager modManager)
|
public Temporary(DalamudPluginInterface pi, ModManager modManager)
|
||||||
{
|
{
|
||||||
_pi = pi;
|
_pi = pi;
|
||||||
_modManager = modManager;
|
_modManager = modManager;
|
||||||
|
|
|
||||||
|
|
@ -93,10 +93,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
||||||
private Penumbra _penumbra;
|
private Penumbra _penumbra;
|
||||||
private Lumina.GameData? _lumina;
|
private Lumina.GameData? _lumina;
|
||||||
|
|
||||||
private Mod.Manager _modManager;
|
private ModManager _modManager;
|
||||||
private ResourceLoader _resourceLoader;
|
private ResourceLoader _resourceLoader;
|
||||||
private Configuration _config;
|
private Configuration _config;
|
||||||
private ModCollection.Manager _collectionManager;
|
private CollectionManager _collectionManager;
|
||||||
private DalamudServices _dalamud;
|
private DalamudServices _dalamud;
|
||||||
private TempCollectionManager _tempCollections;
|
private TempCollectionManager _tempCollections;
|
||||||
private TempModManager _tempMods;
|
private TempModManager _tempMods;
|
||||||
|
|
@ -104,8 +104,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
||||||
private CollectionResolver _collectionResolver;
|
private CollectionResolver _collectionResolver;
|
||||||
private CutsceneService _cutsceneService;
|
private CutsceneService _cutsceneService;
|
||||||
|
|
||||||
public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, Mod.Manager modManager, ResourceLoader resourceLoader,
|
public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, ModManager modManager, ResourceLoader resourceLoader,
|
||||||
Configuration config, ModCollection.Manager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections,
|
Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections,
|
||||||
TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService)
|
TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService)
|
||||||
{
|
{
|
||||||
_communicator = communicator;
|
_communicator = communicator;
|
||||||
|
|
@ -1021,7 +1021,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
||||||
|
|
||||||
// Resolve a path given by string for a specific collection.
|
// Resolve a path given by string for a specific collection.
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
private string ResolvePath(string path, Mod.Manager _, ModCollection collection)
|
private string ResolvePath(string path, ModManager _, ModCollection collection)
|
||||||
{
|
{
|
||||||
if (!_config.EnableMods)
|
if (!_config.EnableMods)
|
||||||
return path;
|
return path;
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ public class PenumbraIpcProviders : IDisposable
|
||||||
internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll;
|
internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll;
|
||||||
internal readonly FuncProvider< string, string, int, PenumbraApiEc > RemoveTemporaryMod;
|
internal readonly FuncProvider< string, string, int, PenumbraApiEc > RemoveTemporaryMod;
|
||||||
|
|
||||||
public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api, Mod.Manager modManager )
|
public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager )
|
||||||
{
|
{
|
||||||
Api = api;
|
Api = api;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,408 +15,405 @@ using Penumbra.Util;
|
||||||
|
|
||||||
namespace Penumbra.Collections;
|
namespace Penumbra.Collections;
|
||||||
|
|
||||||
public partial class ModCollection
|
public sealed partial class CollectionManager : ISavable
|
||||||
{
|
{
|
||||||
public sealed partial class Manager : ISavable
|
public const int Version = 1;
|
||||||
|
|
||||||
|
// The collection currently selected for changing settings.
|
||||||
|
public ModCollection Current { get; private set; } = ModCollection.Empty;
|
||||||
|
|
||||||
|
// The collection currently selected is in use either as an active collection or through inheritance.
|
||||||
|
public bool CurrentCollectionInUse { get; private set; }
|
||||||
|
|
||||||
|
// The collection used for general file redirections and all characters not specifically named.
|
||||||
|
public ModCollection Default { get; private set; } = ModCollection.Empty;
|
||||||
|
|
||||||
|
// The collection used for all files categorized as UI files.
|
||||||
|
public ModCollection Interface { get; private set; } = ModCollection.Empty;
|
||||||
|
|
||||||
|
// A single collection that can not be deleted as a fallback for the current collection.
|
||||||
|
private ModCollection DefaultName { get; set; } = ModCollection.Empty;
|
||||||
|
|
||||||
|
// The list of character collections.
|
||||||
|
public readonly IndividualCollections Individuals;
|
||||||
|
|
||||||
|
public ModCollection Individual(ActorIdentifier identifier)
|
||||||
|
=> Individuals.TryGetCollection(identifier, out var c) ? c : Default;
|
||||||
|
|
||||||
|
// Special Collections
|
||||||
|
private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues<Api.Enums.ApiCollectionType>().Length - 3];
|
||||||
|
|
||||||
|
// Return the configured collection for the given type or null.
|
||||||
|
// Does not handle Inactive, use ByName instead.
|
||||||
|
public ModCollection? ByType(CollectionType type)
|
||||||
|
=> ByType(type, ActorIdentifier.Invalid);
|
||||||
|
|
||||||
|
public ModCollection? ByType(CollectionType type, ActorIdentifier identifier)
|
||||||
{
|
{
|
||||||
public const int Version = 1;
|
if (type.IsSpecial())
|
||||||
|
return _specialCollections[(int)type];
|
||||||
|
|
||||||
// The collection currently selected for changing settings.
|
return type switch
|
||||||
public ModCollection Current { get; private set; } = Empty;
|
|
||||||
|
|
||||||
// The collection currently selected is in use either as an active collection or through inheritance.
|
|
||||||
public bool CurrentCollectionInUse { get; private set; }
|
|
||||||
|
|
||||||
// The collection used for general file redirections and all characters not specifically named.
|
|
||||||
public ModCollection Default { get; private set; } = Empty;
|
|
||||||
|
|
||||||
// The collection used for all files categorized as UI files.
|
|
||||||
public ModCollection Interface { get; private set; } = Empty;
|
|
||||||
|
|
||||||
// A single collection that can not be deleted as a fallback for the current collection.
|
|
||||||
private ModCollection DefaultName { get; set; } = Empty;
|
|
||||||
|
|
||||||
// The list of character collections.
|
|
||||||
public readonly IndividualCollections Individuals;
|
|
||||||
|
|
||||||
public ModCollection Individual(ActorIdentifier identifier)
|
|
||||||
=> Individuals.TryGetCollection(identifier, out var c) ? c : Default;
|
|
||||||
|
|
||||||
// Special Collections
|
|
||||||
private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues<Api.Enums.ApiCollectionType>().Length - 3];
|
|
||||||
|
|
||||||
// Return the configured collection for the given type or null.
|
|
||||||
// Does not handle Inactive, use ByName instead.
|
|
||||||
public ModCollection? ByType(CollectionType type)
|
|
||||||
=> ByType(type, ActorIdentifier.Invalid);
|
|
||||||
|
|
||||||
public ModCollection? ByType(CollectionType type, ActorIdentifier identifier)
|
|
||||||
{
|
{
|
||||||
if (type.IsSpecial())
|
CollectionType.Default => Default,
|
||||||
return _specialCollections[(int)type];
|
CollectionType.Interface => Interface,
|
||||||
|
CollectionType.Current => Current,
|
||||||
|
CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue(identifier, out var c) ? c : null,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return type switch
|
// Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections.
|
||||||
{
|
private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1)
|
||||||
CollectionType.Default => Default,
|
{
|
||||||
CollectionType.Interface => Interface,
|
var oldCollectionIdx = collectionType switch
|
||||||
CollectionType.Current => Current,
|
|
||||||
CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue(identifier, out var c) ? c : null,
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections.
|
|
||||||
private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1)
|
|
||||||
{
|
{
|
||||||
var oldCollectionIdx = collectionType switch
|
CollectionType.Default => Default.Index,
|
||||||
{
|
CollectionType.Interface => Interface.Index,
|
||||||
CollectionType.Default => Default.Index,
|
CollectionType.Current => Current.Index,
|
||||||
CollectionType.Interface => Interface.Index,
|
CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count
|
||||||
CollectionType.Current => Current.Index,
|
? -1
|
||||||
CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count
|
: Individuals[individualIndex].Collection.Index,
|
||||||
? -1
|
_ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index,
|
||||||
: Individuals[individualIndex].Collection.Index,
|
_ => -1,
|
||||||
_ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index,
|
};
|
||||||
_ => -1,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx)
|
if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var newCollection = this[newIdx];
|
var newCollection = this[newIdx];
|
||||||
if (newIdx > Empty.Index)
|
if (newIdx > ModCollection.Empty.Index)
|
||||||
newCollection.CreateCache(collectionType is CollectionType.Default);
|
newCollection.CreateCache(collectionType is CollectionType.Default);
|
||||||
|
|
||||||
switch (collectionType)
|
switch (collectionType)
|
||||||
{
|
|
||||||
case CollectionType.Default:
|
|
||||||
Default = newCollection;
|
|
||||||
break;
|
|
||||||
case CollectionType.Interface:
|
|
||||||
Interface = newCollection;
|
|
||||||
break;
|
|
||||||
case CollectionType.Current:
|
|
||||||
Current = newCollection;
|
|
||||||
break;
|
|
||||||
case CollectionType.Individual:
|
|
||||||
if (!Individuals.ChangeCollection(individualIndex, newCollection))
|
|
||||||
{
|
|
||||||
RemoveCache(newIdx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
_specialCollections[(int)collectionType] = newCollection;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveCache(oldCollectionIdx);
|
|
||||||
|
|
||||||
UpdateCurrentCollectionInUse();
|
|
||||||
_communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection,
|
|
||||||
collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateCurrentCollectionInUse()
|
|
||||||
=> CurrentCollectionInUse = _specialCollections
|
|
||||||
.OfType<ModCollection>()
|
|
||||||
.Prepend(Interface)
|
|
||||||
.Prepend(Default)
|
|
||||||
.Concat(Individuals.Assignments.Select(kvp => kvp.Collection))
|
|
||||||
.SelectMany(c => c.GetFlattenedInheritance()).Contains(Current);
|
|
||||||
|
|
||||||
public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1)
|
|
||||||
=> SetCollection(collection.Index, collectionType, individualIndex);
|
|
||||||
|
|
||||||
// Create a special collection if it does not exist and set it to Empty.
|
|
||||||
public bool CreateSpecialCollection(CollectionType collectionType)
|
|
||||||
{
|
{
|
||||||
if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null)
|
case CollectionType.Default:
|
||||||
return false;
|
Default = newCollection;
|
||||||
|
break;
|
||||||
_specialCollections[(int)collectionType] = Default;
|
case CollectionType.Interface:
|
||||||
_communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty);
|
Interface = newCollection;
|
||||||
return true;
|
break;
|
||||||
}
|
case CollectionType.Current:
|
||||||
|
Current = newCollection;
|
||||||
// Remove a special collection if it exists
|
break;
|
||||||
public void RemoveSpecialCollection(CollectionType collectionType)
|
case CollectionType.Individual:
|
||||||
{
|
if (!Individuals.ChangeCollection(individualIndex, newCollection))
|
||||||
if (!collectionType.IsSpecial())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var old = _specialCollections[(int)collectionType];
|
|
||||||
if (old != null)
|
|
||||||
{
|
|
||||||
_specialCollections[(int)collectionType] = null;
|
|
||||||
_communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrappers around Individual Collection handling.
|
|
||||||
public void CreateIndividualCollection(params ActorIdentifier[] identifiers)
|
|
||||||
{
|
|
||||||
if (Individuals.Add(identifiers, Default))
|
|
||||||
_communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveIndividualCollection(int individualIndex)
|
|
||||||
{
|
|
||||||
if (individualIndex < 0 || individualIndex >= Individuals.Count)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var (name, old) = Individuals[individualIndex];
|
|
||||||
if (Individuals.Delete(individualIndex))
|
|
||||||
_communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MoveIndividualCollection(int from, int to)
|
|
||||||
{
|
|
||||||
if (Individuals.Move(from, to))
|
|
||||||
Penumbra.SaveService.QueueSave(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtain the index of a collection by name.
|
|
||||||
private int GetIndexForCollectionName(string name)
|
|
||||||
=> name.Length == 0 ? Empty.Index : _collections.IndexOf(c => c.Name == name);
|
|
||||||
|
|
||||||
// Load default, current, special, and character collections from config.
|
|
||||||
// Then create caches. If a collection does not exist anymore, reset it to an appropriate default.
|
|
||||||
private void LoadCollections(FilenameService files)
|
|
||||||
{
|
|
||||||
var configChanged = !ReadActiveCollections(files, out var jObject);
|
|
||||||
|
|
||||||
// Load the default collection.
|
|
||||||
var defaultName = jObject[nameof(Default)]?.ToObject<string>() ?? (configChanged ? DefaultCollection : Empty.Name);
|
|
||||||
var defaultIdx = GetIndexForCollectionName(defaultName);
|
|
||||||
if (defaultIdx < 0)
|
|
||||||
{
|
|
||||||
Penumbra.ChatService.NotificationMessage(
|
|
||||||
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure",
|
|
||||||
NotificationType.Warning);
|
|
||||||
Default = Empty;
|
|
||||||
configChanged = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Default = this[defaultIdx];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the interface collection.
|
|
||||||
var interfaceName = jObject[nameof(Interface)]?.ToObject<string>() ?? Default.Name;
|
|
||||||
var interfaceIdx = GetIndexForCollectionName(interfaceName);
|
|
||||||
if (interfaceIdx < 0)
|
|
||||||
{
|
|
||||||
Penumbra.ChatService.NotificationMessage(
|
|
||||||
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.",
|
|
||||||
"Load Failure", NotificationType.Warning);
|
|
||||||
Interface = Empty;
|
|
||||||
configChanged = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Interface = this[interfaceIdx];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the current collection.
|
|
||||||
var currentName = jObject[nameof(Current)]?.ToObject<string>() ?? DefaultCollection;
|
|
||||||
var currentIdx = GetIndexForCollectionName(currentName);
|
|
||||||
if (currentIdx < 0)
|
|
||||||
{
|
|
||||||
Penumbra.ChatService.NotificationMessage(
|
|
||||||
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.",
|
|
||||||
"Load Failure", NotificationType.Warning);
|
|
||||||
Current = DefaultName;
|
|
||||||
configChanged = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Current = this[currentIdx];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load special collections.
|
|
||||||
foreach (var (type, name, _) in CollectionTypeExtensions.Special)
|
|
||||||
{
|
|
||||||
var typeName = jObject[type.ToString()]?.ToObject<string>();
|
|
||||||
if (typeName != null)
|
|
||||||
{
|
{
|
||||||
var idx = GetIndexForCollectionName(typeName);
|
RemoveCache(newIdx);
|
||||||
if (idx < 0)
|
return;
|
||||||
{
|
|
||||||
Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.",
|
|
||||||
"Load Failure",
|
|
||||||
NotificationType.Warning);
|
|
||||||
configChanged = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_specialCollections[(int)type] = this[idx];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
configChanged |= MigrateIndividualCollections(jObject);
|
break;
|
||||||
configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this);
|
default:
|
||||||
|
_specialCollections[(int)collectionType] = newCollection;
|
||||||
// Save any changes and create all required caches.
|
break;
|
||||||
if (configChanged)
|
|
||||||
Penumbra.SaveService.ImmediateSave(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate ungendered collections to Male and Female for 0.5.9.0.
|
RemoveCache(oldCollectionIdx);
|
||||||
public static void MigrateUngenderedCollections(FilenameService fileNames)
|
|
||||||
|
UpdateCurrentCollectionInUse();
|
||||||
|
_communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection,
|
||||||
|
collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCurrentCollectionInUse()
|
||||||
|
=> CurrentCollectionInUse = _specialCollections
|
||||||
|
.OfType<ModCollection>()
|
||||||
|
.Prepend(Interface)
|
||||||
|
.Prepend(Default)
|
||||||
|
.Concat(Individuals.Assignments.Select(kvp => kvp.Collection))
|
||||||
|
.SelectMany(c => c.GetFlattenedInheritance()).Contains(Current);
|
||||||
|
|
||||||
|
public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1)
|
||||||
|
=> SetCollection(collection.Index, collectionType, individualIndex);
|
||||||
|
|
||||||
|
// Create a special collection if it does not exist and set it to Empty.
|
||||||
|
public bool CreateSpecialCollection(CollectionType collectionType)
|
||||||
|
{
|
||||||
|
if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_specialCollections[(int)collectionType] = Default;
|
||||||
|
_communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a special collection if it exists
|
||||||
|
public void RemoveSpecialCollection(CollectionType collectionType)
|
||||||
|
{
|
||||||
|
if (!collectionType.IsSpecial())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var old = _specialCollections[(int)collectionType];
|
||||||
|
if (old != null)
|
||||||
{
|
{
|
||||||
if (!ReadActiveCollections(fileNames, out var jObject))
|
_specialCollections[(int)collectionType] = null;
|
||||||
return;
|
_communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male ")))
|
// Wrappers around Individual Collection handling.
|
||||||
{
|
public void CreateIndividualCollection(params ActorIdentifier[] identifiers)
|
||||||
var oldName = type.ToString()[4..];
|
{
|
||||||
var value = jObject[oldName];
|
if (Individuals.Add(identifiers, Default))
|
||||||
if (value == null)
|
_communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName);
|
||||||
continue;
|
}
|
||||||
|
|
||||||
jObject.Remove(oldName);
|
public void RemoveIndividualCollection(int individualIndex)
|
||||||
jObject.Add("Male" + oldName, value);
|
{
|
||||||
jObject.Add("Female" + oldName, value);
|
if (individualIndex < 0 || individualIndex >= Individuals.Count)
|
||||||
}
|
return;
|
||||||
|
|
||||||
using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate);
|
var (name, old) = Individuals[individualIndex];
|
||||||
using var writer = new StreamWriter(stream);
|
if (Individuals.Delete(individualIndex))
|
||||||
using var j = new JsonTextWriter(writer);
|
_communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name);
|
||||||
j.Formatting = Formatting.Indented;
|
}
|
||||||
jObject.WriteTo(j);
|
|
||||||
|
public void MoveIndividualCollection(int from, int to)
|
||||||
|
{
|
||||||
|
if (Individuals.Move(from, to))
|
||||||
|
Penumbra.SaveService.QueueSave(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain the index of a collection by name.
|
||||||
|
private int GetIndexForCollectionName(string name)
|
||||||
|
=> name.Length == 0 ? ModCollection.Empty.Index : _collections.IndexOf(c => c.Name == name);
|
||||||
|
|
||||||
|
// Load default, current, special, and character collections from config.
|
||||||
|
// Then create caches. If a collection does not exist anymore, reset it to an appropriate default.
|
||||||
|
private void LoadCollections(FilenameService files)
|
||||||
|
{
|
||||||
|
var configChanged = !ReadActiveCollections(files, out var jObject);
|
||||||
|
|
||||||
|
// Load the default collection.
|
||||||
|
var defaultName = jObject[nameof(Default)]?.ToObject<string>() ?? (configChanged ? ModCollection.DefaultCollection : ModCollection.Empty.Name);
|
||||||
|
var defaultIdx = GetIndexForCollectionName(defaultName);
|
||||||
|
if (defaultIdx < 0)
|
||||||
|
{
|
||||||
|
Penumbra.ChatService.NotificationMessage(
|
||||||
|
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure",
|
||||||
|
NotificationType.Warning);
|
||||||
|
Default = ModCollection.Empty;
|
||||||
|
configChanged = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Default = this[defaultIdx];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate individual collections to Identifiers for 0.6.0.
|
// Load the interface collection.
|
||||||
private bool MigrateIndividualCollections(JObject jObject)
|
var interfaceName = jObject[nameof(Interface)]?.ToObject<string>() ?? Default.Name;
|
||||||
|
var interfaceIdx = GetIndexForCollectionName(interfaceName);
|
||||||
|
if (interfaceIdx < 0)
|
||||||
{
|
{
|
||||||
var version = jObject[nameof(Version)]?.Value<int>() ?? 0;
|
Penumbra.ChatService.NotificationMessage(
|
||||||
if (version > 0)
|
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.",
|
||||||
return false;
|
"Load Failure", NotificationType.Warning);
|
||||||
|
Interface = ModCollection.Empty;
|
||||||
|
configChanged = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Interface = this[interfaceIdx];
|
||||||
|
}
|
||||||
|
|
||||||
// Load character collections. If a player name comes up multiple times, the last one is applied.
|
// Load the current collection.
|
||||||
var characters = jObject["Characters"]?.ToObject<Dictionary<string, string>>() ?? new Dictionary<string, string>();
|
var currentName = jObject[nameof(Current)]?.ToObject<string>() ?? ModCollection.DefaultCollection;
|
||||||
var dict = new Dictionary<string, ModCollection>(characters.Count);
|
var currentIdx = GetIndexForCollectionName(currentName);
|
||||||
foreach (var (player, collectionName) in characters)
|
if (currentIdx < 0)
|
||||||
|
{
|
||||||
|
Penumbra.ChatService.NotificationMessage(
|
||||||
|
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollection}.",
|
||||||
|
"Load Failure", NotificationType.Warning);
|
||||||
|
Current = DefaultName;
|
||||||
|
configChanged = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Current = this[currentIdx];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load special collections.
|
||||||
|
foreach (var (type, name, _) in CollectionTypeExtensions.Special)
|
||||||
|
{
|
||||||
|
var typeName = jObject[type.ToString()]?.ToObject<string>();
|
||||||
|
if (typeName != null)
|
||||||
{
|
{
|
||||||
var idx = GetIndexForCollectionName(collectionName);
|
var idx = GetIndexForCollectionName(typeName);
|
||||||
if (idx < 0)
|
if (idx < 0)
|
||||||
{
|
{
|
||||||
Penumbra.ChatService.NotificationMessage(
|
Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.",
|
||||||
$"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure",
|
"Load Failure",
|
||||||
NotificationType.Warning);
|
NotificationType.Warning);
|
||||||
dict.Add(player, Empty);
|
configChanged = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
dict.Add(player, this[idx]);
|
_specialCollections[(int)type] = this[idx];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Individuals.Migrate0To1(dict);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the active collection file into a jObject.
|
configChanged |= MigrateIndividualCollections(jObject);
|
||||||
// Returns true if this is successful, false if the file does not exist or it is unsuccessful.
|
configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this);
|
||||||
private static bool ReadActiveCollections(FilenameService files, out JObject ret)
|
|
||||||
{
|
|
||||||
var file = files.ActiveCollectionsFile;
|
|
||||||
if (File.Exists(file))
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ret = JObject.Parse(File.ReadAllText(file));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = new JObject();
|
// Save any changes and create all required caches.
|
||||||
|
if (configChanged)
|
||||||
|
Penumbra.SaveService.ImmediateSave(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate ungendered collections to Male and Female for 0.5.9.0.
|
||||||
|
public static void MigrateUngenderedCollections(FilenameService fileNames)
|
||||||
|
{
|
||||||
|
if (!ReadActiveCollections(fileNames, out var jObject))
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male ")))
|
||||||
|
{
|
||||||
|
var oldName = type.ToString()[4..];
|
||||||
|
var value = jObject[oldName];
|
||||||
|
if (value == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
jObject.Remove(oldName);
|
||||||
|
jObject.Add("Male" + oldName, value);
|
||||||
|
jObject.Add("Female" + oldName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate);
|
||||||
|
using var writer = new StreamWriter(stream);
|
||||||
|
using var j = new JsonTextWriter(writer);
|
||||||
|
j.Formatting = Formatting.Indented;
|
||||||
|
jObject.WriteTo(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate individual collections to Identifiers for 0.6.0.
|
||||||
|
private bool MigrateIndividualCollections(JObject jObject)
|
||||||
|
{
|
||||||
|
var version = jObject[nameof(Version)]?.Value<int>() ?? 0;
|
||||||
|
if (version > 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Load character collections. If a player name comes up multiple times, the last one is applied.
|
||||||
|
var characters = jObject["Characters"]?.ToObject<Dictionary<string, string>>() ?? new Dictionary<string, string>();
|
||||||
|
var dict = new Dictionary<string, ModCollection>(characters.Count);
|
||||||
|
foreach (var (player, collectionName) in characters)
|
||||||
|
{
|
||||||
|
var idx = GetIndexForCollectionName(collectionName);
|
||||||
|
if (idx < 0)
|
||||||
|
{
|
||||||
|
Penumbra.ChatService.NotificationMessage(
|
||||||
|
$"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure",
|
||||||
|
NotificationType.Warning);
|
||||||
|
dict.Add(player, ModCollection.Empty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dict.Add(player, this[idx]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save if any of the active collections is changed.
|
Individuals.Migrate0To1(dict);
|
||||||
private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3)
|
return true;
|
||||||
{
|
}
|
||||||
if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary)
|
|
||||||
Penumbra.SaveService.QueueSave(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache handling. Usually recreate caches on the next framework tick,
|
// Read the active collection file into a jObject.
|
||||||
// but at launch create all of them at once.
|
// Returns true if this is successful, false if the file does not exist or it is unsuccessful.
|
||||||
public void CreateNecessaryCaches()
|
private static bool ReadActiveCollections(FilenameService files, out JObject ret)
|
||||||
{
|
{
|
||||||
var tasks = _specialCollections.OfType<ModCollection>()
|
var file = files.ActiveCollectionsFile;
|
||||||
.Concat(Individuals.Select(p => p.Collection))
|
if (File.Exists(file))
|
||||||
.Prepend(Current)
|
try
|
||||||
.Prepend(Default)
|
{
|
||||||
.Prepend(Interface)
|
ret = JObject.Parse(File.ReadAllText(file));
|
||||||
.Distinct()
|
return true;
|
||||||
.Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == Default)))
|
}
|
||||||
.ToArray();
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}");
|
||||||
|
}
|
||||||
|
|
||||||
Task.WaitAll(tasks);
|
ret = new JObject();
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void RemoveCache(int idx)
|
// Save if any of the active collections is changed.
|
||||||
{
|
private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3)
|
||||||
if (idx != Empty.Index
|
{
|
||||||
&& idx != Default.Index
|
if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary)
|
||||||
&& idx != Interface.Index
|
Penumbra.SaveService.QueueSave(this);
|
||||||
&& idx != Current.Index
|
}
|
||||||
&& _specialCollections.All(c => c == null || c.Index != idx)
|
|
||||||
&& Individuals.Select(p => p.Collection).All(c => c.Index != idx))
|
|
||||||
_collections[idx].ClearCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recalculate effective files for active collections on events.
|
// Cache handling. Usually recreate caches on the next framework tick,
|
||||||
private void OnModAddedActive(Mod mod)
|
// but at launch create all of them at once.
|
||||||
{
|
public void CreateNecessaryCaches()
|
||||||
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
|
{
|
||||||
collection._cache!.AddMod(mod, true);
|
var tasks = _specialCollections.OfType<ModCollection>()
|
||||||
}
|
.Concat(Individuals.Select(p => p.Collection))
|
||||||
|
.Prepend(Current)
|
||||||
|
.Prepend(Default)
|
||||||
|
.Prepend(Interface)
|
||||||
|
.Distinct()
|
||||||
|
.Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == Default)))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
private void OnModRemovedActive(Mod mod)
|
Task.WaitAll(tasks);
|
||||||
{
|
}
|
||||||
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
|
|
||||||
collection._cache!.RemoveMod(mod, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnModMovedActive(Mod mod)
|
private void RemoveCache(int idx)
|
||||||
{
|
{
|
||||||
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
|
if (idx != ModCollection.Empty.Index
|
||||||
collection._cache!.ReloadMod(mod, true);
|
&& idx != Default.Index
|
||||||
}
|
&& idx != Interface.Index
|
||||||
|
&& idx != Current.Index
|
||||||
|
&& _specialCollections.All(c => c == null || c.Index != idx)
|
||||||
|
&& Individuals.Select(p => p.Collection).All(c => c.Index != idx))
|
||||||
|
_collections[idx].ClearCache();
|
||||||
|
}
|
||||||
|
|
||||||
public string ToFilename(FilenameService fileNames)
|
// Recalculate effective files for active collections on events.
|
||||||
=> fileNames.ActiveCollectionsFile;
|
private void OnModAddedActive(Mod mod)
|
||||||
|
{
|
||||||
|
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
|
||||||
|
collection._cache!.AddMod(mod, true);
|
||||||
|
}
|
||||||
|
|
||||||
public string TypeName
|
private void OnModRemovedActive(Mod mod)
|
||||||
=> "Active Collections";
|
{
|
||||||
|
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
|
||||||
|
collection._cache!.RemoveMod(mod, true);
|
||||||
|
}
|
||||||
|
|
||||||
public string LogName(string _)
|
private void OnModMovedActive(Mod mod)
|
||||||
=> "to file";
|
{
|
||||||
|
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
|
||||||
|
collection._cache!.ReloadMod(mod, true);
|
||||||
|
}
|
||||||
|
|
||||||
public void Save(StreamWriter writer)
|
public string ToFilename(FilenameService fileNames)
|
||||||
{
|
=> fileNames.ActiveCollectionsFile;
|
||||||
var jObj = new JObject
|
|
||||||
|
public string TypeName
|
||||||
|
=> "Active Collections";
|
||||||
|
|
||||||
|
public string LogName(string _)
|
||||||
|
=> "to file";
|
||||||
|
|
||||||
|
public void Save(StreamWriter writer)
|
||||||
|
{
|
||||||
|
var jObj = new JObject
|
||||||
{
|
{
|
||||||
{ nameof(Version), Version },
|
{ nameof(Version), Version },
|
||||||
{ nameof(Default), Default.Name },
|
{ nameof(Default), Default.Name },
|
||||||
{ nameof(Interface), Interface.Name },
|
{ nameof(Interface), Interface.Name },
|
||||||
{ nameof(Current), Current.Name },
|
{ nameof(Current), Current.Name },
|
||||||
};
|
};
|
||||||
foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null)
|
foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null)
|
||||||
.Select(p => ((CollectionType)p.Index, p.Value!)))
|
.Select(p => ((CollectionType)p.Index, p.Value!)))
|
||||||
jObj.Add(type.ToString(), collection.Name);
|
jObj.Add(type.ToString(), collection.Name);
|
||||||
|
|
||||||
jObj.Add(nameof(Individuals), Individuals.ToJObject());
|
jObj.Add(nameof(Individuals), Individuals.ToJObject());
|
||||||
using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
||||||
jObj.WriteTo(j);
|
jObj.WriteTo(j);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -17,462 +17,459 @@ using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
|
||||||
|
|
||||||
namespace Penumbra.Collections;
|
namespace Penumbra.Collections;
|
||||||
|
|
||||||
public partial class ModCollection
|
public sealed partial class CollectionManager : IDisposable, IEnumerable<ModCollection>
|
||||||
{
|
{
|
||||||
public sealed partial class Manager : IDisposable, IEnumerable<ModCollection>
|
private readonly Mods.ModManager _modManager;
|
||||||
|
private readonly CommunicatorService _communicator;
|
||||||
|
private readonly CharacterUtility _characterUtility;
|
||||||
|
private readonly ResidentResourceManager _residentResources;
|
||||||
|
private readonly Configuration _config;
|
||||||
|
|
||||||
|
|
||||||
|
// The empty collection is always available and always has index 0.
|
||||||
|
// It can not be deleted or moved.
|
||||||
|
private readonly List<ModCollection> _collections = new()
|
||||||
{
|
{
|
||||||
private readonly Mod.Manager _modManager;
|
ModCollection.Empty,
|
||||||
private readonly CommunicatorService _communicator;
|
};
|
||||||
private readonly CharacterUtility _characterUtility;
|
|
||||||
private readonly ResidentResourceManager _residentResources;
|
|
||||||
private readonly Configuration _config;
|
|
||||||
|
|
||||||
|
public ModCollection this[Index idx]
|
||||||
|
=> _collections[idx];
|
||||||
|
|
||||||
// The empty collection is always available and always has index 0.
|
public ModCollection? this[string name]
|
||||||
// It can not be deleted or moved.
|
=> ByName(name, out var c) ? c : null;
|
||||||
private readonly List<ModCollection> _collections = new()
|
|
||||||
|
public int Count
|
||||||
|
=> _collections.Count;
|
||||||
|
|
||||||
|
// Obtain a collection case-independently by name.
|
||||||
|
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
|
||||||
|
=> _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
|
||||||
|
|
||||||
|
// Default enumeration skips the empty collection.
|
||||||
|
public IEnumerator<ModCollection> GetEnumerator()
|
||||||
|
=> _collections.Skip(1).GetEnumerator();
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
=> GetEnumerator();
|
||||||
|
|
||||||
|
public IEnumerable<ModCollection> GetEnumeratorWithEmpty()
|
||||||
|
=> _collections;
|
||||||
|
|
||||||
|
public CollectionManager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility,
|
||||||
|
ResidentResourceManager residentResources, Configuration config, Mods.ModManager modManager, IndividualCollections individuals)
|
||||||
|
{
|
||||||
|
using var time = timer.Measure(StartTimeType.Collections);
|
||||||
|
_communicator = communicator;
|
||||||
|
_characterUtility = characterUtility;
|
||||||
|
_residentResources = residentResources;
|
||||||
|
_config = config;
|
||||||
|
_modManager = modManager;
|
||||||
|
Individuals = individuals;
|
||||||
|
|
||||||
|
// The collection manager reacts to changes in mods by itself.
|
||||||
|
_modManager.ModDiscoveryStarted += OnModDiscoveryStarted;
|
||||||
|
_modManager.ModDiscoveryFinished += OnModDiscoveryFinished;
|
||||||
|
_modManager.ModOptionChanged += OnModOptionsChanged;
|
||||||
|
_modManager.ModPathChanged += OnModPathChange;
|
||||||
|
_communicator.CollectionChange.Event += SaveOnChange;
|
||||||
|
_communicator.TemporaryGlobalModChange.Event += OnGlobalModChange;
|
||||||
|
ReadCollections(files);
|
||||||
|
LoadCollections(files);
|
||||||
|
UpdateCurrentCollectionInUse();
|
||||||
|
CreateNecessaryCaches();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_communicator.CollectionChange.Event -= SaveOnChange;
|
||||||
|
_communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange;
|
||||||
|
_modManager.ModDiscoveryStarted -= OnModDiscoveryStarted;
|
||||||
|
_modManager.ModDiscoveryFinished -= OnModDiscoveryFinished;
|
||||||
|
_modManager.ModOptionChanged -= OnModOptionsChanged;
|
||||||
|
_modManager.ModPathChanged -= OnModPathChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed)
|
||||||
|
=> TempModManager.OnGlobalModChange(_collections, mod, created, removed);
|
||||||
|
|
||||||
|
// Returns true if the name is not empty, it is not the name of the empty collection
|
||||||
|
// and no existing collection results in the same filename as name.
|
||||||
|
public bool CanAddCollection(string name, out string fixedName)
|
||||||
|
{
|
||||||
|
if (!ModCollection.IsValidName(name))
|
||||||
{
|
{
|
||||||
Empty,
|
fixedName = string.Empty;
|
||||||
};
|
return false;
|
||||||
|
|
||||||
public ModCollection this[Index idx]
|
|
||||||
=> _collections[idx];
|
|
||||||
|
|
||||||
public ModCollection? this[string name]
|
|
||||||
=> ByName(name, out var c) ? c : null;
|
|
||||||
|
|
||||||
public int Count
|
|
||||||
=> _collections.Count;
|
|
||||||
|
|
||||||
// Obtain a collection case-independently by name.
|
|
||||||
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
|
|
||||||
=> _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
|
|
||||||
|
|
||||||
// Default enumeration skips the empty collection.
|
|
||||||
public IEnumerator<ModCollection> GetEnumerator()
|
|
||||||
=> _collections.Skip(1).GetEnumerator();
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
=> GetEnumerator();
|
|
||||||
|
|
||||||
public IEnumerable<ModCollection> GetEnumeratorWithEmpty()
|
|
||||||
=> _collections;
|
|
||||||
|
|
||||||
public Manager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility,
|
|
||||||
ResidentResourceManager residentResources, Configuration config, Mod.Manager manager, IndividualCollections individuals)
|
|
||||||
{
|
|
||||||
using var time = timer.Measure(StartTimeType.Collections);
|
|
||||||
_communicator = communicator;
|
|
||||||
_characterUtility = characterUtility;
|
|
||||||
_residentResources = residentResources;
|
|
||||||
_config = config;
|
|
||||||
_modManager = manager;
|
|
||||||
Individuals = individuals;
|
|
||||||
|
|
||||||
// The collection manager reacts to changes in mods by itself.
|
|
||||||
_modManager.ModDiscoveryStarted += OnModDiscoveryStarted;
|
|
||||||
_modManager.ModDiscoveryFinished += OnModDiscoveryFinished;
|
|
||||||
_modManager.ModOptionChanged += OnModOptionsChanged;
|
|
||||||
_modManager.ModPathChanged += OnModPathChange;
|
|
||||||
_communicator.CollectionChange.Event += SaveOnChange;
|
|
||||||
_communicator.TemporaryGlobalModChange.Event += OnGlobalModChange;
|
|
||||||
ReadCollections(files);
|
|
||||||
LoadCollections(files);
|
|
||||||
UpdateCurrentCollectionInUse();
|
|
||||||
CreateNecessaryCaches();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
name = name.RemoveInvalidPathSymbols().ToLowerInvariant();
|
||||||
|
if (name.Length == 0
|
||||||
|
|| name == ModCollection.Empty.Name.ToLowerInvariant()
|
||||||
|
|| _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name))
|
||||||
{
|
{
|
||||||
_communicator.CollectionChange.Event -= SaveOnChange;
|
fixedName = string.Empty;
|
||||||
_communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange;
|
return false;
|
||||||
_modManager.ModDiscoveryStarted -= OnModDiscoveryStarted;
|
|
||||||
_modManager.ModDiscoveryFinished -= OnModDiscoveryFinished;
|
|
||||||
_modManager.ModOptionChanged -= OnModOptionsChanged;
|
|
||||||
_modManager.ModPathChanged -= OnModPathChange;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed)
|
fixedName = name;
|
||||||
=> TempModManager.OnGlobalModChange(_collections, mod, created, removed);
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Returns true if the name is not empty, it is not the name of the empty collection
|
// Add a new collection of the given name.
|
||||||
// and no existing collection results in the same filename as name.
|
// If duplicate is not-null, the new collection will be a duplicate of it.
|
||||||
public bool CanAddCollection(string name, out string fixedName)
|
// If the name of the collection would result in an already existing filename, skip it.
|
||||||
|
// Returns true if the collection was successfully created and fires a Inactive event.
|
||||||
|
// Also sets the current collection to the new collection afterwards.
|
||||||
|
public bool AddCollection(string name, ModCollection? duplicate)
|
||||||
|
{
|
||||||
|
if (!CanAddCollection(name, out var fixedName))
|
||||||
{
|
{
|
||||||
if (!IsValidName(name))
|
Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists.");
|
||||||
{
|
return false;
|
||||||
fixedName = string.Empty;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
name = name.RemoveInvalidPathSymbols().ToLowerInvariant();
|
|
||||||
if (name.Length == 0
|
|
||||||
|| name == Empty.Name.ToLowerInvariant()
|
|
||||||
|| _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name))
|
|
||||||
{
|
|
||||||
fixedName = string.Empty;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fixedName = name;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a new collection of the given name.
|
var newCollection = duplicate?.Duplicate(name) ?? ModCollection.CreateNewEmpty(name);
|
||||||
// If duplicate is not-null, the new collection will be a duplicate of it.
|
newCollection.Index = _collections.Count;
|
||||||
// If the name of the collection would result in an already existing filename, skip it.
|
_collections.Add(newCollection);
|
||||||
// Returns true if the collection was successfully created and fires a Inactive event.
|
|
||||||
// Also sets the current collection to the new collection afterwards.
|
Penumbra.SaveService.ImmediateSave(newCollection);
|
||||||
public bool AddCollection(string name, ModCollection? duplicate)
|
Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}.");
|
||||||
|
_communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
|
||||||
|
SetCollection(newCollection.Index, CollectionType.Current);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the given collection if it exists and is neither the empty nor the default-named collection.
|
||||||
|
// If the removed collection was active, it also sets the corresponding collection to the appropriate default.
|
||||||
|
// Also removes the collection from inheritances of all other collections.
|
||||||
|
public bool RemoveCollection(int idx)
|
||||||
|
{
|
||||||
|
if (idx <= ModCollection.Empty.Index || idx >= _collections.Count)
|
||||||
{
|
{
|
||||||
if (!CanAddCollection(name, out var fixedName))
|
Penumbra.Log.Error("Can not remove the empty collection.");
|
||||||
{
|
return false;
|
||||||
Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newCollection = duplicate?.Duplicate(name) ?? CreateNewEmpty(name);
|
|
||||||
newCollection.Index = _collections.Count;
|
|
||||||
_collections.Add(newCollection);
|
|
||||||
|
|
||||||
Penumbra.SaveService.ImmediateSave(newCollection);
|
|
||||||
Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}.");
|
|
||||||
_communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
|
|
||||||
SetCollection(newCollection.Index, CollectionType.Current);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the given collection if it exists and is neither the empty nor the default-named collection.
|
if (idx == DefaultName.Index)
|
||||||
// If the removed collection was active, it also sets the corresponding collection to the appropriate default.
|
|
||||||
// Also removes the collection from inheritances of all other collections.
|
|
||||||
public bool RemoveCollection(int idx)
|
|
||||||
{
|
{
|
||||||
if (idx <= Empty.Index || idx >= _collections.Count)
|
Penumbra.Log.Error("Can not remove the default collection.");
|
||||||
{
|
return false;
|
||||||
Penumbra.Log.Error("Can not remove the empty collection.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (idx == DefaultName.Index)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error("Can not remove the default collection.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (idx == Current.Index)
|
|
||||||
SetCollection(DefaultName.Index, CollectionType.Current);
|
|
||||||
|
|
||||||
if (idx == Default.Index)
|
|
||||||
SetCollection(Empty.Index, CollectionType.Default);
|
|
||||||
|
|
||||||
for (var i = 0; i < _specialCollections.Length; ++i)
|
|
||||||
{
|
|
||||||
if (idx == _specialCollections[i]?.Index)
|
|
||||||
SetCollection(Empty, (CollectionType)i);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < Individuals.Count; ++i)
|
|
||||||
{
|
|
||||||
if (Individuals[i].Collection.Index == idx)
|
|
||||||
SetCollection(Empty, CollectionType.Individual, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
var collection = _collections[idx];
|
|
||||||
|
|
||||||
// Clear own inheritances.
|
|
||||||
foreach (var inheritance in collection.Inheritance)
|
|
||||||
collection.ClearSubscriptions(inheritance);
|
|
||||||
|
|
||||||
Penumbra.SaveService.ImmediateDelete(collection);
|
|
||||||
_collections.RemoveAt(idx);
|
|
||||||
|
|
||||||
// Clear external inheritances.
|
|
||||||
foreach (var c in _collections)
|
|
||||||
{
|
|
||||||
var inheritedIdx = c._inheritance.IndexOf(collection);
|
|
||||||
if (inheritedIdx >= 0)
|
|
||||||
c.RemoveInheritance(inheritedIdx);
|
|
||||||
|
|
||||||
if (c.Index > idx)
|
|
||||||
--c.Index;
|
|
||||||
}
|
|
||||||
|
|
||||||
Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}.");
|
|
||||||
_communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool RemoveCollection(ModCollection collection)
|
if (idx == Current.Index)
|
||||||
=> RemoveCollection(collection.Index);
|
SetCollection(DefaultName.Index, CollectionType.Current);
|
||||||
|
|
||||||
private void OnModDiscoveryStarted()
|
if (idx == Default.Index)
|
||||||
|
SetCollection(ModCollection.Empty.Index, CollectionType.Default);
|
||||||
|
|
||||||
|
for (var i = 0; i < _specialCollections.Length; ++i)
|
||||||
{
|
{
|
||||||
foreach (var collection in this)
|
if (idx == _specialCollections[i]?.Index)
|
||||||
collection.PrepareModDiscovery();
|
SetCollection(ModCollection.Empty, (CollectionType)i);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnModDiscoveryFinished()
|
for (var i = 0; i < Individuals.Count; ++i)
|
||||||
{
|
{
|
||||||
// First, re-apply all mod settings.
|
if (Individuals[i].Collection.Index == idx)
|
||||||
foreach (var collection in this)
|
SetCollection(ModCollection.Empty, CollectionType.Individual, i);
|
||||||
collection.ApplyModSettings();
|
|
||||||
|
|
||||||
// Afterwards, we update the caches. This can not happen in the same loop due to inheritance.
|
|
||||||
foreach (var collection in this.Where(c => c.HasCache))
|
|
||||||
collection.ForceCacheUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var collection = _collections[idx];
|
||||||
|
|
||||||
// A changed mod path forces changes for all collections, active and inactive.
|
// Clear own inheritances.
|
||||||
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
foreach (var inheritance in collection.Inheritance)
|
||||||
DirectoryInfo? newDirectory)
|
collection.ClearSubscriptions(inheritance);
|
||||||
|
|
||||||
|
Penumbra.SaveService.ImmediateDelete(collection);
|
||||||
|
_collections.RemoveAt(idx);
|
||||||
|
|
||||||
|
// Clear external inheritances.
|
||||||
|
foreach (var c in _collections)
|
||||||
{
|
{
|
||||||
switch (type)
|
var inheritedIdx = c._inheritance.IndexOf(collection);
|
||||||
{
|
if (inheritedIdx >= 0)
|
||||||
case ModPathChangeType.Added:
|
c.RemoveInheritance(inheritedIdx);
|
||||||
foreach (var collection in this)
|
|
||||||
collection.AddMod(mod);
|
|
||||||
|
|
||||||
OnModAddedActive(mod);
|
if (c.Index > idx)
|
||||||
break;
|
--c.Index;
|
||||||
case ModPathChangeType.Deleted:
|
|
||||||
OnModRemovedActive(mod);
|
|
||||||
foreach (var collection in this)
|
|
||||||
collection.RemoveMod(mod, mod.Index);
|
|
||||||
|
|
||||||
break;
|
|
||||||
case ModPathChangeType.Moved:
|
|
||||||
OnModMovedActive(mod);
|
|
||||||
foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
|
|
||||||
Penumbra.SaveService.QueueSave(collection);
|
|
||||||
|
|
||||||
break;
|
|
||||||
case ModPathChangeType.StartingReload:
|
|
||||||
OnModRemovedActive(mod);
|
|
||||||
break;
|
|
||||||
case ModPathChangeType.Reloaded:
|
|
||||||
OnModAddedActive(mod);
|
|
||||||
break;
|
|
||||||
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically update all relevant collections when a mod is changed.
|
Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}.");
|
||||||
// This means saving if options change in a way where the settings may change and the collection has settings for this mod.
|
_communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
|
||||||
// And also updating effective file and meta manipulation lists if necessary.
|
return true;
|
||||||
private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
|
}
|
||||||
|
|
||||||
|
public bool RemoveCollection(ModCollection collection)
|
||||||
|
=> RemoveCollection(collection.Index);
|
||||||
|
|
||||||
|
private void OnModDiscoveryStarted()
|
||||||
|
{
|
||||||
|
foreach (var collection in this)
|
||||||
|
collection.PrepareModDiscovery();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnModDiscoveryFinished()
|
||||||
|
{
|
||||||
|
// First, re-apply all mod settings.
|
||||||
|
foreach (var collection in this)
|
||||||
|
collection.ApplyModSettings();
|
||||||
|
|
||||||
|
// Afterwards, we update the caches. This can not happen in the same loop due to inheritance.
|
||||||
|
foreach (var collection in this.Where(c => c.HasCache))
|
||||||
|
collection.ForceCacheUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A changed mod path forces changes for all collections, active and inactive.
|
||||||
|
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
||||||
|
DirectoryInfo? newDirectory)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
{
|
{
|
||||||
// Handle changes that break revertability.
|
case ModPathChangeType.Added:
|
||||||
if (type == ModOptionChangeType.PrepareChange)
|
|
||||||
{
|
|
||||||
foreach (var collection in this.Where(c => c.HasCache))
|
|
||||||
{
|
|
||||||
if (collection[mod.Index].Settings is { Enabled: true })
|
|
||||||
collection._cache!.RemoveMod(mod, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload);
|
|
||||||
|
|
||||||
// Handle changes that require overwriting the collection.
|
|
||||||
if (requiresSaving)
|
|
||||||
foreach (var collection in this)
|
foreach (var collection in this)
|
||||||
{
|
collection.AddMod(mod);
|
||||||
if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false)
|
|
||||||
Penumbra.SaveService.QueueSave(collection);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle changes that reload the mod if the changes did not need to be prepared,
|
OnModAddedActive(mod);
|
||||||
// or re-add the mod if they were prepared.
|
break;
|
||||||
if (recomputeList)
|
case ModPathChangeType.Deleted:
|
||||||
foreach (var collection in this.Where(c => c.HasCache))
|
OnModRemovedActive(mod);
|
||||||
{
|
foreach (var collection in this)
|
||||||
if (collection[mod.Index].Settings is { Enabled: true })
|
collection.RemoveMod(mod, mod.Index);
|
||||||
{
|
|
||||||
if (reload)
|
|
||||||
collection._cache!.ReloadMod(mod, true);
|
|
||||||
else
|
|
||||||
collection._cache!.AddMod(mod, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the collection with the default name if it does not exist.
|
break;
|
||||||
// It should always be ensured that it exists, otherwise it will be created.
|
case ModPathChangeType.Moved:
|
||||||
// This can also not be deleted, so there are always at least the empty and a collection with default name.
|
OnModMovedActive(mod);
|
||||||
private void AddDefaultCollection()
|
foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
|
||||||
{
|
Penumbra.SaveService.QueueSave(collection);
|
||||||
var idx = GetIndexForCollectionName(DefaultCollection);
|
|
||||||
if (idx >= 0)
|
|
||||||
{
|
|
||||||
DefaultName = this[idx];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultCollection = CreateNewEmpty(DefaultCollection);
|
break;
|
||||||
Penumbra.SaveService.ImmediateSave(defaultCollection);
|
case ModPathChangeType.StartingReload:
|
||||||
defaultCollection.Index = _collections.Count;
|
OnModRemovedActive(mod);
|
||||||
_collections.Add(defaultCollection);
|
break;
|
||||||
}
|
case ModPathChangeType.Reloaded:
|
||||||
|
OnModAddedActive(mod);
|
||||||
// Inheritances can not be setup before all collections are read,
|
break;
|
||||||
// so this happens after reading the collections.
|
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
||||||
private void ApplyInheritances(IEnumerable<IReadOnlyList<string>> inheritances)
|
|
||||||
{
|
|
||||||
foreach (var (collection, inheritance) in this.Zip(inheritances))
|
|
||||||
{
|
|
||||||
var changes = false;
|
|
||||||
foreach (var subCollectionName in inheritance)
|
|
||||||
{
|
|
||||||
if (!ByName(subCollectionName, out var subCollection))
|
|
||||||
{
|
|
||||||
changes = true;
|
|
||||||
Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed.");
|
|
||||||
}
|
|
||||||
else if (!collection.AddInheritance(subCollection, false))
|
|
||||||
{
|
|
||||||
changes = true;
|
|
||||||
Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changes)
|
|
||||||
Penumbra.SaveService.ImmediateSave(collection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read all collection files in the Collection Directory.
|
|
||||||
// Ensure that the default named collection exists, and apply inheritances afterwards.
|
|
||||||
// Duplicate collection files are not deleted, just not added here.
|
|
||||||
private void ReadCollections(FilenameService files)
|
|
||||||
{
|
|
||||||
var inheritances = new List<IReadOnlyList<string>>();
|
|
||||||
foreach (var file in files.CollectionFiles)
|
|
||||||
{
|
|
||||||
var collection = LoadFromFile(file, out var inheritance);
|
|
||||||
if (collection == null || collection.Name.Length == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json")
|
|
||||||
Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}.");
|
|
||||||
|
|
||||||
if (this[collection.Name] != null)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
inheritances.Add(inheritance);
|
|
||||||
collection.Index = _collections.Count;
|
|
||||||
_collections.Add(collection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddDefaultCollection();
|
|
||||||
ApplyInheritances(inheritances);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string RedundancyCheck(CollectionType type, ActorIdentifier id)
|
|
||||||
{
|
|
||||||
var checkAssignment = ByType(type, id);
|
|
||||||
if (checkAssignment == null)
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
// Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap.
|
|
||||||
case CollectionType.Individual:
|
|
||||||
switch (id.Type)
|
|
||||||
{
|
|
||||||
case IdentifierType.Player when id.HomeWorld != ushort.MaxValue:
|
|
||||||
{
|
|
||||||
var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue));
|
|
||||||
return global?.Index == checkAssignment.Index
|
|
||||||
? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."
|
|
||||||
: string.Empty;
|
|
||||||
}
|
|
||||||
case IdentifierType.Owned:
|
|
||||||
if (id.HomeWorld != ushort.MaxValue)
|
|
||||||
{
|
|
||||||
var global = ByType(CollectionType.Individual,
|
|
||||||
Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId));
|
|
||||||
if (global?.Index == checkAssignment.Index)
|
|
||||||
return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it.";
|
|
||||||
}
|
|
||||||
|
|
||||||
var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId));
|
|
||||||
return unowned?.Index == checkAssignment.Index
|
|
||||||
? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it."
|
|
||||||
: string.Empty;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
// The group of all Characters is redundant if they are all equal to Default or unassigned.
|
|
||||||
case CollectionType.MalePlayerCharacter:
|
|
||||||
case CollectionType.MaleNonPlayerCharacter:
|
|
||||||
case CollectionType.FemalePlayerCharacter:
|
|
||||||
case CollectionType.FemaleNonPlayerCharacter:
|
|
||||||
var first = ByType(CollectionType.MalePlayerCharacter) ?? Default;
|
|
||||||
var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default;
|
|
||||||
var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default;
|
|
||||||
var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default;
|
|
||||||
if (first.Index == second.Index
|
|
||||||
&& first.Index == third.Index
|
|
||||||
&& first.Index == fourth.Index
|
|
||||||
&& first.Index == Default.Index)
|
|
||||||
return
|
|
||||||
"Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n"
|
|
||||||
+ "You can keep just the Default Assignment.";
|
|
||||||
|
|
||||||
break;
|
|
||||||
// Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default.
|
|
||||||
case CollectionType.NonPlayerChild:
|
|
||||||
case CollectionType.NonPlayerElderly:
|
|
||||||
var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter);
|
|
||||||
var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter);
|
|
||||||
var collection1 = CollectionType.MaleNonPlayerCharacter;
|
|
||||||
var collection2 = CollectionType.FemaleNonPlayerCharacter;
|
|
||||||
if (maleNpc == null)
|
|
||||||
{
|
|
||||||
maleNpc = Default;
|
|
||||||
if (maleNpc.Index != checkAssignment.Index)
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
collection1 = CollectionType.Default;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (femaleNpc == null)
|
|
||||||
{
|
|
||||||
femaleNpc = Default;
|
|
||||||
if (femaleNpc.Index != checkAssignment.Index)
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
collection2 = CollectionType.Default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return collection1 == collection2
|
|
||||||
? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them."
|
|
||||||
: $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them.";
|
|
||||||
|
|
||||||
// For other assignments, check the inheritance order, unassigned means fall-through,
|
|
||||||
// assigned needs identical assignments to be redundant.
|
|
||||||
default:
|
|
||||||
var group = type.InheritanceOrder();
|
|
||||||
foreach (var parentType in group)
|
|
||||||
{
|
|
||||||
var assignment = ByType(parentType);
|
|
||||||
if (assignment == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (assignment.Index == checkAssignment.Index)
|
|
||||||
return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it.";
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Empty;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automatically update all relevant collections when a mod is changed.
|
||||||
|
// This means saving if options change in a way where the settings may change and the collection has settings for this mod.
|
||||||
|
// And also updating effective file and meta manipulation lists if necessary.
|
||||||
|
private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
|
||||||
|
{
|
||||||
|
// Handle changes that break revertability.
|
||||||
|
if (type == ModOptionChangeType.PrepareChange)
|
||||||
|
{
|
||||||
|
foreach (var collection in this.Where(c => c.HasCache))
|
||||||
|
{
|
||||||
|
if (collection[mod.Index].Settings is { Enabled: true })
|
||||||
|
collection._cache!.RemoveMod(mod, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload);
|
||||||
|
|
||||||
|
// Handle changes that require overwriting the collection.
|
||||||
|
if (requiresSaving)
|
||||||
|
foreach (var collection in this)
|
||||||
|
{
|
||||||
|
if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false)
|
||||||
|
Penumbra.SaveService.QueueSave(collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle changes that reload the mod if the changes did not need to be prepared,
|
||||||
|
// or re-add the mod if they were prepared.
|
||||||
|
if (recomputeList)
|
||||||
|
foreach (var collection in this.Where(c => c.HasCache))
|
||||||
|
{
|
||||||
|
if (collection[mod.Index].Settings is { Enabled: true })
|
||||||
|
{
|
||||||
|
if (reload)
|
||||||
|
collection._cache!.ReloadMod(mod, true);
|
||||||
|
else
|
||||||
|
collection._cache!.AddMod(mod, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the collection with the default name if it does not exist.
|
||||||
|
// It should always be ensured that it exists, otherwise it will be created.
|
||||||
|
// This can also not be deleted, so there are always at least the empty and a collection with default name.
|
||||||
|
private void AddDefaultCollection()
|
||||||
|
{
|
||||||
|
var idx = GetIndexForCollectionName(ModCollection.DefaultCollection);
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
DefaultName = this[idx];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultCollection = ModCollection.CreateNewEmpty((string)ModCollection.DefaultCollection);
|
||||||
|
Penumbra.SaveService.ImmediateSave(defaultCollection);
|
||||||
|
defaultCollection.Index = _collections.Count;
|
||||||
|
_collections.Add(defaultCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inheritances can not be setup before all collections are read,
|
||||||
|
// so this happens after reading the collections.
|
||||||
|
private void ApplyInheritances(IEnumerable<IReadOnlyList<string>> inheritances)
|
||||||
|
{
|
||||||
|
foreach (var (collection, inheritance) in this.Zip(inheritances))
|
||||||
|
{
|
||||||
|
var changes = false;
|
||||||
|
foreach (var subCollectionName in inheritance)
|
||||||
|
{
|
||||||
|
if (!ByName(subCollectionName, out var subCollection))
|
||||||
|
{
|
||||||
|
changes = true;
|
||||||
|
Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed.");
|
||||||
|
}
|
||||||
|
else if (!collection.AddInheritance(subCollection, false))
|
||||||
|
{
|
||||||
|
changes = true;
|
||||||
|
Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes)
|
||||||
|
Penumbra.SaveService.ImmediateSave(collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all collection files in the Collection Directory.
|
||||||
|
// Ensure that the default named collection exists, and apply inheritances afterwards.
|
||||||
|
// Duplicate collection files are not deleted, just not added here.
|
||||||
|
private void ReadCollections(FilenameService files)
|
||||||
|
{
|
||||||
|
var inheritances = new List<IReadOnlyList<string>>();
|
||||||
|
foreach (var file in files.CollectionFiles)
|
||||||
|
{
|
||||||
|
var collection = ModCollection.LoadFromFile(file, out var inheritance);
|
||||||
|
if (collection == null || collection.Name.Length == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json")
|
||||||
|
Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}.");
|
||||||
|
|
||||||
|
if (this[collection.Name] != null)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inheritances.Add(inheritance);
|
||||||
|
collection.Index = _collections.Count;
|
||||||
|
_collections.Add(collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddDefaultCollection();
|
||||||
|
ApplyInheritances(inheritances);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string RedundancyCheck(CollectionType type, ActorIdentifier id)
|
||||||
|
{
|
||||||
|
var checkAssignment = ByType(type, id);
|
||||||
|
if (checkAssignment == null)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
// Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap.
|
||||||
|
case CollectionType.Individual:
|
||||||
|
switch (id.Type)
|
||||||
|
{
|
||||||
|
case IdentifierType.Player when id.HomeWorld != ushort.MaxValue:
|
||||||
|
{
|
||||||
|
var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue));
|
||||||
|
return global?.Index == checkAssignment.Index
|
||||||
|
? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."
|
||||||
|
: string.Empty;
|
||||||
|
}
|
||||||
|
case IdentifierType.Owned:
|
||||||
|
if (id.HomeWorld != ushort.MaxValue)
|
||||||
|
{
|
||||||
|
var global = ByType(CollectionType.Individual,
|
||||||
|
Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId));
|
||||||
|
if (global?.Index == checkAssignment.Index)
|
||||||
|
return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it.";
|
||||||
|
}
|
||||||
|
|
||||||
|
var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId));
|
||||||
|
return unowned?.Index == checkAssignment.Index
|
||||||
|
? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it."
|
||||||
|
: string.Empty;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// The group of all Characters is redundant if they are all equal to Default or unassigned.
|
||||||
|
case CollectionType.MalePlayerCharacter:
|
||||||
|
case CollectionType.MaleNonPlayerCharacter:
|
||||||
|
case CollectionType.FemalePlayerCharacter:
|
||||||
|
case CollectionType.FemaleNonPlayerCharacter:
|
||||||
|
var first = ByType(CollectionType.MalePlayerCharacter) ?? Default;
|
||||||
|
var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default;
|
||||||
|
var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default;
|
||||||
|
var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default;
|
||||||
|
if (first.Index == second.Index
|
||||||
|
&& first.Index == third.Index
|
||||||
|
&& first.Index == fourth.Index
|
||||||
|
&& first.Index == Default.Index)
|
||||||
|
return
|
||||||
|
"Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n"
|
||||||
|
+ "You can keep just the Default Assignment.";
|
||||||
|
|
||||||
|
break;
|
||||||
|
// Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default.
|
||||||
|
case CollectionType.NonPlayerChild:
|
||||||
|
case CollectionType.NonPlayerElderly:
|
||||||
|
var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter);
|
||||||
|
var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter);
|
||||||
|
var collection1 = CollectionType.MaleNonPlayerCharacter;
|
||||||
|
var collection2 = CollectionType.FemaleNonPlayerCharacter;
|
||||||
|
if (maleNpc == null)
|
||||||
|
{
|
||||||
|
maleNpc = Default;
|
||||||
|
if (maleNpc.Index != checkAssignment.Index)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
collection1 = CollectionType.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (femaleNpc == null)
|
||||||
|
{
|
||||||
|
femaleNpc = Default;
|
||||||
|
if (femaleNpc.Index != checkAssignment.Index)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
collection2 = CollectionType.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection1 == collection2
|
||||||
|
? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them."
|
||||||
|
: $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them.";
|
||||||
|
|
||||||
|
// For other assignments, check the inheritance order, unassigned means fall-through,
|
||||||
|
// assigned needs identical assignments to be redundant.
|
||||||
|
default:
|
||||||
|
var group = type.InheritanceOrder();
|
||||||
|
foreach (var parentType in group)
|
||||||
|
{
|
||||||
|
var assignment = ByType(parentType);
|
||||||
|
if (assignment == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (assignment.Index == checkAssignment.Index)
|
||||||
|
return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it.";
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ public partial class IndividualCollections
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ReadJObject( JArray? obj, ModCollection.Manager manager )
|
public bool ReadJObject( JArray? obj, CollectionManager manager )
|
||||||
{
|
{
|
||||||
if( obj == null )
|
if( obj == null )
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Penumbra.Interop;
|
|
||||||
using Penumbra.Interop.Structs;
|
using Penumbra.Interop.Structs;
|
||||||
using Penumbra.Meta.Files;
|
using Penumbra.Meta.Files;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
|
|
@ -20,27 +19,27 @@ namespace Penumbra.Collections;
|
||||||
public partial class ModCollection
|
public partial class ModCollection
|
||||||
{
|
{
|
||||||
// Only active collections need to have a cache.
|
// Only active collections need to have a cache.
|
||||||
private Cache? _cache;
|
internal ModCollectionCache? _cache;
|
||||||
|
|
||||||
public bool HasCache
|
public bool HasCache
|
||||||
=> _cache != null;
|
=> _cache != null;
|
||||||
|
|
||||||
// Count the number of changes of the effective file list.
|
// Count the number of changes of the effective file list.
|
||||||
// This is used for material and imc changes.
|
// This is used for material and imc changes.
|
||||||
public int ChangeCounter { get; private set; }
|
public int ChangeCounter { get; internal set; }
|
||||||
|
|
||||||
// Only create, do not update.
|
// Only create, do not update.
|
||||||
private void CreateCache(bool isDefault)
|
internal void CreateCache(bool isDefault)
|
||||||
{
|
{
|
||||||
if (_cache == null)
|
if (_cache != null)
|
||||||
{
|
return;
|
||||||
CalculateEffectiveFileList(isDefault);
|
|
||||||
Penumbra.Log.Verbose($"Created new cache for collection {Name}.");
|
CalculateEffectiveFileList(isDefault);
|
||||||
}
|
Penumbra.Log.Verbose($"Created new cache for collection {Name}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force an update with metadata for this cache.
|
// Force an update with metadata for this cache.
|
||||||
private void ForceCacheUpdate()
|
internal void ForceCacheUpdate()
|
||||||
=> CalculateEffectiveFileList(this == Penumbra.CollectionManager.Default);
|
=> CalculateEffectiveFileList(this == Penumbra.CollectionManager.Default);
|
||||||
|
|
||||||
// Handle temporary mods for this collection.
|
// Handle temporary mods for this collection.
|
||||||
|
|
@ -83,7 +82,7 @@ public partial class ModCollection
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static bool CheckFullPath(Utf8GamePath path, FullPath fullPath)
|
internal static bool CheckFullPath(Utf8GamePath path, FullPath fullPath)
|
||||||
{
|
{
|
||||||
if (fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength)
|
if (fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength)
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -127,14 +126,14 @@ public partial class ModCollection
|
||||||
=> Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name, () =>
|
=> Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name, () =>
|
||||||
CalculateEffectiveFileListInternal(isDefault));
|
CalculateEffectiveFileListInternal(isDefault));
|
||||||
|
|
||||||
private void CalculateEffectiveFileListInternal(bool isDefault)
|
internal void CalculateEffectiveFileListInternal(bool isDefault)
|
||||||
{
|
{
|
||||||
// Skip the empty collection.
|
// Skip the empty collection.
|
||||||
if (Index == 0)
|
if (Index == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}");
|
Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}");
|
||||||
_cache ??= new Cache(this);
|
_cache ??= new ModCollectionCache(this);
|
||||||
_cache.FullRecalculation(isDefault);
|
_cache.FullRecalculation(isDefault);
|
||||||
|
|
||||||
Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished.");
|
Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished.");
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -15,7 +15,7 @@ public partial class ModCollection : ISavable
|
||||||
{
|
{
|
||||||
// Since inheritances depend on other collections existing,
|
// Since inheritances depend on other collections existing,
|
||||||
// we return them as a list to be applied after reading all collections.
|
// we return them as a list to be applied after reading all collections.
|
||||||
private static ModCollection? LoadFromFile(FileInfo file, out IReadOnlyList<string> inheritance)
|
internal static ModCollection? LoadFromFile(FileInfo file, out IReadOnlyList<string> inheritance)
|
||||||
{
|
{
|
||||||
inheritance = Array.Empty<string>();
|
inheritance = Array.Empty<string>();
|
||||||
if (!file.Exists)
|
if (!file.Exists)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ public partial class ModCollection
|
||||||
// The bool signifies whether the change was in an already inherited collection.
|
// The bool signifies whether the change was in an already inherited collection.
|
||||||
public event Action< bool > InheritanceChanged;
|
public event Action< bool > InheritanceChanged;
|
||||||
|
|
||||||
private readonly List< ModCollection > _inheritance = new();
|
internal readonly List< ModCollection > _inheritance = new();
|
||||||
|
|
||||||
public IReadOnlyList< ModCollection > Inheritance
|
public IReadOnlyList< ModCollection > Inheritance
|
||||||
=> _inheritance;
|
=> _inheritance;
|
||||||
|
|
@ -98,7 +98,7 @@ public partial class ModCollection
|
||||||
Penumbra.Log.Debug( $"Removed {inheritance.AnonymizedName} from {AnonymizedName} inheritances." );
|
Penumbra.Log.Debug( $"Removed {inheritance.AnonymizedName} from {AnonymizedName} inheritances." );
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearSubscriptions( ModCollection other )
|
internal void ClearSubscriptions( ModCollection other )
|
||||||
{
|
{
|
||||||
other.ModSettingChanged -= OnInheritedModSettingChange;
|
other.ModSettingChanged -= OnInheritedModSettingChange;
|
||||||
other.InheritanceChanged -= OnInheritedInheritanceChange;
|
other.InheritanceChanged -= OnInheritedInheritanceChange;
|
||||||
|
|
|
||||||
|
|
@ -23,18 +23,18 @@ public partial class ModCollection
|
||||||
|
|
||||||
// The collection name can contain invalid path characters,
|
// The collection name can contain invalid path characters,
|
||||||
// but after removing those and going to lower case it has to be unique.
|
// but after removing those and going to lower case it has to be unique.
|
||||||
public string Name { get; private init; }
|
public string Name { get; internal init; }
|
||||||
|
|
||||||
// Get the first two letters of a collection name and its Index (or None if it is the empty collection).
|
// Get the first two letters of a collection name and its Index (or None if it is the empty collection).
|
||||||
public string AnonymizedName
|
public string AnonymizedName
|
||||||
=> this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})";
|
=> this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})";
|
||||||
|
|
||||||
public int Version { get; private set; }
|
public int Version { get; internal set; }
|
||||||
public int Index { get; private set; } = -1;
|
public int Index { get; internal set; } = -1;
|
||||||
|
|
||||||
// If a ModSetting is null, it can be inherited from other collections.
|
// If a ModSetting is null, it can be inherited from other collections.
|
||||||
// If no collection provides a setting for the mod, it is just disabled.
|
// If no collection provides a setting for the mod, it is just disabled.
|
||||||
private readonly List<ModSettings?> _settings;
|
internal readonly List<ModSettings?> _settings;
|
||||||
|
|
||||||
public IReadOnlyList<ModSettings?> Settings
|
public IReadOnlyList<ModSettings?> Settings
|
||||||
=> _settings;
|
=> _settings;
|
||||||
|
|
@ -115,7 +115,7 @@ public partial class ModCollection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion.
|
// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion.
|
||||||
private bool AddMod(Mod mod)
|
internal bool AddMod(Mod mod)
|
||||||
{
|
{
|
||||||
if (_unusedSettings.TryGetValue(mod.ModPath.Name, out var save))
|
if (_unusedSettings.TryGetValue(mod.ModPath.Name, out var save))
|
||||||
{
|
{
|
||||||
|
|
@ -130,7 +130,7 @@ public partial class ModCollection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move settings from the current mod list to the unused mod settings.
|
// Move settings from the current mod list to the unused mod settings.
|
||||||
private void RemoveMod(Mod mod, int idx)
|
internal void RemoveMod(Mod mod, int idx)
|
||||||
{
|
{
|
||||||
var settings = _settings[idx];
|
var settings = _settings[idx];
|
||||||
if (settings != null)
|
if (settings != null)
|
||||||
|
|
@ -150,7 +150,7 @@ public partial class ModCollection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move all settings to unused settings for rediscovery.
|
// Move all settings to unused settings for rediscovery.
|
||||||
private void PrepareModDiscovery()
|
internal void PrepareModDiscovery()
|
||||||
{
|
{
|
||||||
foreach (var (mod, setting) in Penumbra.ModManager.Zip(_settings).Where(s => s.Second != null))
|
foreach (var (mod, setting) in Penumbra.ModManager.Zip(_settings).Where(s => s.Second != null))
|
||||||
_unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod);
|
_unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod);
|
||||||
|
|
@ -160,7 +160,7 @@ public partial class ModCollection
|
||||||
|
|
||||||
// Apply all mod settings from unused settings to the current set of mods.
|
// Apply all mod settings from unused settings to the current set of mods.
|
||||||
// Also fixes invalid settings.
|
// Also fixes invalid settings.
|
||||||
private void ApplyModSettings()
|
internal void ApplyModSettings()
|
||||||
{
|
{
|
||||||
_settings.Capacity = Math.Max(_settings.Capacity, Penumbra.ModManager.Count);
|
_settings.Capacity = Math.Max(_settings.Capacity, Penumbra.ModManager.Count);
|
||||||
if (Penumbra.ModManager.Aggregate(false, (current, mod) => current | AddMod(mod)))
|
if (Penumbra.ModManager.Aggregate(false, (current, mod) => current | AddMod(mod)))
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,12 @@ public class CommandHandler : IDisposable
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly ConfigWindow _configWindow;
|
private readonly ConfigWindow _configWindow;
|
||||||
private readonly ActorManager _actors;
|
private readonly ActorManager _actors;
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
private readonly Penumbra _penumbra;
|
private readonly Penumbra _penumbra;
|
||||||
|
|
||||||
public CommandHandler(Framework framework, CommandManager commandManager, ChatGui chat, RedrawService redrawService, Configuration config,
|
public CommandHandler(Framework framework, CommandManager commandManager, ChatGui chat, RedrawService redrawService, Configuration config,
|
||||||
ConfigWindow configWindow, Mod.Manager modManager, ModCollection.Manager collectionManager, ActorService actors, Penumbra penumbra)
|
ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorService actors, Penumbra penumbra)
|
||||||
{
|
{
|
||||||
_commandManager = commandManager;
|
_commandManager = commandManager;
|
||||||
_redrawService = redrawService;
|
_redrawService = redrawService;
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,10 @@ public partial class TexToolsImporter : IDisposable
|
||||||
|
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly ModEditor _editor;
|
private readonly ModEditor _editor;
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
|
|
||||||
public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles,
|
public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles,
|
||||||
Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, Mod.Manager modManager)
|
Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, ModManager modManager)
|
||||||
{
|
{
|
||||||
_baseDirectory = baseDirectory;
|
_baseDirectory = baseDirectory;
|
||||||
_tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName );
|
_tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName );
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,12 @@ public unsafe class CollectionResolver
|
||||||
private readonly CutsceneService _cutscenes;
|
private readonly CutsceneService _cutscenes;
|
||||||
|
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
private readonly TempCollectionManager _tempCollections;
|
private readonly TempCollectionManager _tempCollections;
|
||||||
private readonly DrawObjectState _drawObjectState;
|
private readonly DrawObjectState _drawObjectState;
|
||||||
|
|
||||||
public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, ClientState clientState, GameGui gameGui,
|
public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, ClientState clientState, GameGui gameGui,
|
||||||
DataManager gameData, ActorService actors, CutsceneService cutscenes, Configuration config, ModCollection.Manager collectionManager,
|
DataManager gameData, ActorService actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager,
|
||||||
TempCollectionManager tempCollections, DrawObjectState drawObjectState)
|
TempCollectionManager tempCollections, DrawObjectState drawObjectState)
|
||||||
{
|
{
|
||||||
_performance = performance;
|
_performance = performance;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ public class PathResolver : IDisposable
|
||||||
{
|
{
|
||||||
private readonly PerformanceTracker _performance;
|
private readonly PerformanceTracker _performance;
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
private readonly TempCollectionManager _tempCollections;
|
private readonly TempCollectionManager _tempCollections;
|
||||||
private readonly ResourceLoader _loader;
|
private readonly ResourceLoader _loader;
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ public class PathResolver : IDisposable
|
||||||
private readonly PathState _pathState;
|
private readonly PathState _pathState;
|
||||||
private readonly MetaState _metaState;
|
private readonly MetaState _metaState;
|
||||||
|
|
||||||
public unsafe PathResolver(PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager,
|
public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager,
|
||||||
TempCollectionManager tempCollections, ResourceLoader loader, AnimationHookService animationHookService, SubfileHelper subfileHelper,
|
TempCollectionManager tempCollections, ResourceLoader loader, AnimationHookService animationHookService, SubfileHelper subfileHelper,
|
||||||
PathState pathState, MetaState metaState)
|
PathState pathState, MetaState metaState)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@ namespace Penumbra.Mods;
|
||||||
|
|
||||||
public class DuplicateManager
|
public class DuplicateManager
|
||||||
{
|
{
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly SHA256 _hasher = SHA256.Create();
|
private readonly SHA256 _hasher = SHA256.Create();
|
||||||
private readonly ModFileCollection _files;
|
private readonly ModFileCollection _files;
|
||||||
private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new();
|
private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new();
|
||||||
|
|
||||||
public DuplicateManager(ModFileCollection files, Mod.Manager modManager)
|
public DuplicateManager(ModFileCollection files, ModManager modManager)
|
||||||
{
|
{
|
||||||
_files = files;
|
_files = files;
|
||||||
_modManager = modManager;
|
_modManager = modManager;
|
||||||
|
|
@ -80,7 +80,7 @@ public class DuplicateManager
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var sub = (Mod.SubMod)subMod;
|
var sub = (SubMod)subMod;
|
||||||
sub.FileData = dict;
|
sub.FileData = dict;
|
||||||
if (groupIdx == -1)
|
if (groupIdx == -1)
|
||||||
mod.SaveDefaultMod();
|
mod.SaveDefaultMod();
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ public class ModBackup
|
||||||
{
|
{
|
||||||
public static bool CreatingBackup { get; private set; }
|
public static bool CreatingBackup { get; private set; }
|
||||||
|
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly Mod _mod;
|
private readonly Mod _mod;
|
||||||
public readonly string Name;
|
public readonly string Name;
|
||||||
public readonly bool Exists;
|
public readonly bool Exists;
|
||||||
|
|
||||||
public ModBackup(Mod.Manager modManager, Mod mod)
|
public ModBackup(ModManager modManager, Mod mod)
|
||||||
{
|
{
|
||||||
_modManager = modManager;
|
_modManager = modManager;
|
||||||
_mod = mod;
|
_mod = mod;
|
||||||
|
|
@ -24,9 +24,9 @@ public class ModBackup
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Migrate file extensions. </summary>
|
/// <summary> Migrate file extensions. </summary>
|
||||||
public static void MigrateZipToPmp(Mod.Manager manager)
|
public static void MigrateZipToPmp(ModManager modManager)
|
||||||
{
|
{
|
||||||
foreach (var mod in manager)
|
foreach (var mod in modManager)
|
||||||
{
|
{
|
||||||
var pmpName = mod.ModPath + ".pmp";
|
var pmpName = mod.ModPath + ".pmp";
|
||||||
var zipName = mod.ModPath + ".zip";
|
var zipName = mod.ModPath + ".zip";
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@ namespace Penumbra.Mods;
|
||||||
public class ModFileEditor
|
public class ModFileEditor
|
||||||
{
|
{
|
||||||
private readonly ModFileCollection _files;
|
private readonly ModFileCollection _files;
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
|
|
||||||
public bool Changes { get; private set; }
|
public bool Changes { get; private set; }
|
||||||
|
|
||||||
public ModFileEditor(ModFileCollection files, Mod.Manager modManager)
|
public ModFileEditor(ModFileCollection files, ModManager modManager)
|
||||||
{
|
{
|
||||||
_files = files;
|
_files = files;
|
||||||
_modManager = modManager;
|
_modManager = modManager;
|
||||||
|
|
@ -24,7 +24,7 @@ public class ModFileEditor
|
||||||
Changes = false;
|
Changes = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Apply(Mod mod, Mod.SubMod option)
|
public int Apply(Mod mod, SubMod option)
|
||||||
{
|
{
|
||||||
var dict = new Dictionary<Utf8GamePath, FullPath>();
|
var dict = new Dictionary<Utf8GamePath, FullPath>();
|
||||||
var num = 0;
|
var num = 0;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ namespace Penumbra.Mods;
|
||||||
|
|
||||||
public class ModMetaEditor
|
public class ModMetaEditor
|
||||||
{
|
{
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
|
|
||||||
private readonly HashSet<ImcManipulation> _imc = new();
|
private readonly HashSet<ImcManipulation> _imc = new();
|
||||||
private readonly HashSet<EqpManipulation> _eqp = new();
|
private readonly HashSet<EqpManipulation> _eqp = new();
|
||||||
|
|
@ -15,7 +15,7 @@ public class ModMetaEditor
|
||||||
private readonly HashSet<EstManipulation> _est = new();
|
private readonly HashSet<EstManipulation> _est = new();
|
||||||
private readonly HashSet<RspManipulation> _rsp = new();
|
private readonly HashSet<RspManipulation> _rsp = new();
|
||||||
|
|
||||||
public ModMetaEditor(Mod.Manager modManager)
|
public ModMetaEditor(ModManager modManager)
|
||||||
=> _modManager = modManager;
|
=> _modManager = modManager;
|
||||||
|
|
||||||
public bool Changes { get; private set; } = false;
|
public bool Changes { get; private set; } = false;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ namespace Penumbra.Mods;
|
||||||
|
|
||||||
public class ModNormalizer
|
public class ModNormalizer
|
||||||
{
|
{
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly List<List<Dictionary<Utf8GamePath, FullPath>>> _redirections = new();
|
private readonly List<List<Dictionary<Utf8GamePath, FullPath>>> _redirections = new();
|
||||||
|
|
||||||
public Mod Mod { get; private set; } = null!;
|
public Mod Mod { get; private set; } = null!;
|
||||||
|
|
@ -24,7 +24,7 @@ public class ModNormalizer
|
||||||
public bool Running
|
public bool Running
|
||||||
=> Step < TotalSteps;
|
=> Step < TotalSteps;
|
||||||
|
|
||||||
public ModNormalizer(Mod.Manager modManager)
|
public ModNormalizer(ModManager modManager)
|
||||||
=> _modManager = modManager;
|
=> _modManager = modManager;
|
||||||
|
|
||||||
public void Normalize(Mod mod)
|
public void Normalize(Mod mod)
|
||||||
|
|
@ -177,7 +177,7 @@ public class ModNormalizer
|
||||||
_redirections[groupIdx + 1].Add(new Dictionary<Utf8GamePath, FullPath>());
|
_redirections[groupIdx + 1].Add(new Dictionary<Utf8GamePath, FullPath>());
|
||||||
|
|
||||||
var groupDir = Mod.Creator.CreateModFolder(directory, group.Name);
|
var groupDir = Mod.Creator.CreateModFolder(directory, group.Name);
|
||||||
foreach (var option in group.OfType<Mod.SubMod>())
|
foreach (var option in group.OfType<SubMod>())
|
||||||
{
|
{
|
||||||
var optionDir = Mod.Creator.CreateModFolder(groupDir, option.Name);
|
var optionDir = Mod.Creator.CreateModFolder(groupDir, option.Name);
|
||||||
|
|
||||||
|
|
@ -279,7 +279,7 @@ public class ModNormalizer
|
||||||
|
|
||||||
private void ApplyRedirections()
|
private void ApplyRedirections()
|
||||||
{
|
{
|
||||||
foreach (var option in Mod.AllSubMods.OfType<Mod.SubMod>())
|
foreach (var option in Mod.AllSubMods.OfType<SubMod>())
|
||||||
{
|
{
|
||||||
_modManager.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, _redirections[option.GroupIdx + 1][option.OptionIdx]);
|
_modManager.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, _redirections[option.GroupIdx + 1][option.OptionIdx]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ using Penumbra.Util;
|
||||||
|
|
||||||
public class ModSwapEditor
|
public class ModSwapEditor
|
||||||
{
|
{
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly Dictionary<Utf8GamePath, FullPath> _swaps = new();
|
private readonly Dictionary<Utf8GamePath, FullPath> _swaps = new();
|
||||||
|
|
||||||
public IReadOnlyDictionary<Utf8GamePath, FullPath> Swaps
|
public IReadOnlyDictionary<Utf8GamePath, FullPath> Swaps
|
||||||
=> _swaps;
|
=> _swaps;
|
||||||
|
|
||||||
public ModSwapEditor(Mod.Manager modManager)
|
public ModSwapEditor(ModManager modManager)
|
||||||
=> _modManager = modManager;
|
=> _modManager = modManager;
|
||||||
|
|
||||||
public void Revert(ISubMod option)
|
public void Revert(ISubMod option)
|
||||||
|
|
|
||||||
|
|
@ -4,202 +4,199 @@ using System.Linq;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public partial class Mod
|
public partial class ModManager
|
||||||
{
|
{
|
||||||
public partial class Manager
|
public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
||||||
|
DirectoryInfo? newDirectory);
|
||||||
|
|
||||||
|
public event ModPathChangeDelegate ModPathChanged;
|
||||||
|
|
||||||
|
// Rename/Move a mod directory.
|
||||||
|
// Updates all collection settings and sort order settings.
|
||||||
|
public void MoveModDirectory(int idx, string newName)
|
||||||
{
|
{
|
||||||
public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
var mod = this[idx];
|
||||||
DirectoryInfo? newDirectory);
|
var oldName = mod.Name;
|
||||||
|
var oldDirectory = mod.ModPath;
|
||||||
|
|
||||||
public event ModPathChangeDelegate ModPathChanged;
|
switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir))
|
||||||
|
|
||||||
// Rename/Move a mod directory.
|
|
||||||
// Updates all collection settings and sort order settings.
|
|
||||||
public void MoveModDirectory(int idx, string newName)
|
|
||||||
{
|
{
|
||||||
var mod = this[idx];
|
case NewDirectoryState.NonExisting:
|
||||||
var oldName = mod.Name;
|
// Nothing to do
|
||||||
var oldDirectory = mod.ModPath;
|
break;
|
||||||
|
case NewDirectoryState.ExistsEmpty:
|
||||||
switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir))
|
|
||||||
{
|
|
||||||
case NewDirectoryState.NonExisting:
|
|
||||||
// Nothing to do
|
|
||||||
break;
|
|
||||||
case NewDirectoryState.ExistsEmpty:
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(dir!.FullName);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
// Should be caught beforehand.
|
|
||||||
case NewDirectoryState.ExistsNonEmpty:
|
|
||||||
case NewDirectoryState.ExistsAsFile:
|
|
||||||
case NewDirectoryState.ContainsInvalidSymbols:
|
|
||||||
// Nothing to do at all.
|
|
||||||
case NewDirectoryState.Identical:
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Move(oldDirectory.FullName, dir!.FullName);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DataEditor.MoveDataFile(oldDirectory, dir);
|
|
||||||
new ModBackup(this, mod).Move(null, dir.Name);
|
|
||||||
|
|
||||||
dir.Refresh();
|
|
||||||
mod.ModPath = dir;
|
|
||||||
if (!mod.Reload(this, false, out var metaChange))
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir);
|
|
||||||
if (metaChange != ModDataChangeType.None)
|
|
||||||
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reload a mod without changing its base directory.
|
|
||||||
/// If the base directory does not exist anymore, the mod will be deleted.
|
|
||||||
/// </summary>
|
|
||||||
public void ReloadMod(int idx)
|
|
||||||
{
|
|
||||||
var mod = this[idx];
|
|
||||||
var oldName = mod.Name;
|
|
||||||
|
|
||||||
ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
|
|
||||||
if (!mod.Reload(this, true, out var metaChange))
|
|
||||||
{
|
|
||||||
Penumbra.Log.Warning(mod.Name.Length == 0
|
|
||||||
? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead."
|
|
||||||
: $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead.");
|
|
||||||
|
|
||||||
DeleteMod(idx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath);
|
|
||||||
if (metaChange != ModDataChangeType.None)
|
|
||||||
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Delete a mod by its index. The event is invoked before the mod is removed from the list.
|
|
||||||
/// Deletes from filesystem as well as from internal data.
|
|
||||||
/// Updates indices of later mods.
|
|
||||||
/// </summary>
|
|
||||||
public void DeleteMod(int idx)
|
|
||||||
{
|
|
||||||
var mod = this[idx];
|
|
||||||
if (Directory.Exists(mod.ModPath.FullName))
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.Delete(mod.ModPath.FullName, true);
|
Directory.Delete(dir!.FullName);
|
||||||
Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}.");
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}");
|
Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null);
|
break;
|
||||||
_mods.RemoveAt(idx);
|
// Should be caught beforehand.
|
||||||
foreach (var remainingMod in _mods.Skip(idx))
|
case NewDirectoryState.ExistsNonEmpty:
|
||||||
--remainingMod.Index;
|
case NewDirectoryState.ExistsAsFile:
|
||||||
|
case NewDirectoryState.ContainsInvalidSymbols:
|
||||||
Penumbra.Log.Debug($"Deleted mod {mod.Name}.");
|
// Nothing to do at all.
|
||||||
}
|
case NewDirectoryState.Identical:
|
||||||
|
default:
|
||||||
/// <summary> Load a new mod and add it to the manager if successful. </summary>
|
|
||||||
public void AddMod(DirectoryInfo modFolder)
|
|
||||||
{
|
|
||||||
if (_mods.Any(m => m.ModPath.Name == modFolder.Name))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Creator.SplitMultiGroups(modFolder);
|
|
||||||
var mod = LoadMod(this, modFolder, true);
|
|
||||||
if (mod == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
mod.Index = _mods.Count;
|
|
||||||
_mods.Add(mod);
|
|
||||||
ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath);
|
|
||||||
Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum NewDirectoryState
|
try
|
||||||
{
|
{
|
||||||
NonExisting,
|
Directory.Move(oldDirectory.FullName, dir!.FullName);
|
||||||
ExistsEmpty,
|
}
|
||||||
ExistsNonEmpty,
|
catch (Exception e)
|
||||||
ExistsAsFile,
|
{
|
||||||
ContainsInvalidSymbols,
|
Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}");
|
||||||
Identical,
|
return;
|
||||||
Empty,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Return the state of the new potential name of a directory. </summary>
|
DataEditor.MoveDataFile(oldDirectory, dir);
|
||||||
public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory)
|
new ModBackup(this, mod).Move(null, dir.Name);
|
||||||
|
|
||||||
|
dir.Refresh();
|
||||||
|
mod.ModPath = dir;
|
||||||
|
if (!mod.Reload(this, false, out var metaChange))
|
||||||
{
|
{
|
||||||
directory = null;
|
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
|
||||||
if (newName.Length == 0)
|
return;
|
||||||
return NewDirectoryState.Empty;
|
|
||||||
|
|
||||||
if (oldName == newName)
|
|
||||||
return NewDirectoryState.Identical;
|
|
||||||
|
|
||||||
var fixedNewName = Creator.ReplaceBadXivSymbols(newName);
|
|
||||||
if (fixedNewName != newName)
|
|
||||||
return NewDirectoryState.ContainsInvalidSymbols;
|
|
||||||
|
|
||||||
directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName));
|
|
||||||
if (File.Exists(directory.FullName))
|
|
||||||
return NewDirectoryState.ExistsAsFile;
|
|
||||||
|
|
||||||
if (!Directory.Exists(directory.FullName))
|
|
||||||
return NewDirectoryState.NonExisting;
|
|
||||||
|
|
||||||
if (directory.EnumerateFileSystemInfos().Any())
|
|
||||||
return NewDirectoryState.ExistsNonEmpty;
|
|
||||||
|
|
||||||
return NewDirectoryState.ExistsEmpty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir);
|
||||||
|
if (metaChange != ModDataChangeType.None)
|
||||||
|
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary> Add new mods to NewMods and remove deleted mods from NewMods. </summary>
|
/// <summary>
|
||||||
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
/// Reload a mod without changing its base directory.
|
||||||
DirectoryInfo? newDirectory)
|
/// If the base directory does not exist anymore, the mod will be deleted.
|
||||||
|
/// </summary>
|
||||||
|
public void ReloadMod(int idx)
|
||||||
|
{
|
||||||
|
var mod = this[idx];
|
||||||
|
var oldName = mod.Name;
|
||||||
|
|
||||||
|
ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
|
||||||
|
if (!mod.Reload(this, true, out var metaChange))
|
||||||
{
|
{
|
||||||
switch (type)
|
Penumbra.Log.Warning(mod.Name.Length == 0
|
||||||
|
? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead."
|
||||||
|
: $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead.");
|
||||||
|
|
||||||
|
DeleteMod(idx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath);
|
||||||
|
if (metaChange != ModDataChangeType.None)
|
||||||
|
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete a mod by its index. The event is invoked before the mod is removed from the list.
|
||||||
|
/// Deletes from filesystem as well as from internal data.
|
||||||
|
/// Updates indices of later mods.
|
||||||
|
/// </summary>
|
||||||
|
public void DeleteMod(int idx)
|
||||||
|
{
|
||||||
|
var mod = this[idx];
|
||||||
|
if (Directory.Exists(mod.ModPath.FullName))
|
||||||
|
try
|
||||||
{
|
{
|
||||||
case ModPathChangeType.Added:
|
Directory.Delete(mod.ModPath.FullName, true);
|
||||||
NewMods.Add(mod);
|
Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}.");
|
||||||
break;
|
|
||||||
case ModPathChangeType.Deleted:
|
|
||||||
NewMods.Remove(mod);
|
|
||||||
break;
|
|
||||||
case ModPathChangeType.Moved:
|
|
||||||
if (oldDirectory != null && newDirectory != null)
|
|
||||||
DataEditor.MoveDataFile(oldDirectory, newDirectory);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null);
|
||||||
|
_mods.RemoveAt(idx);
|
||||||
|
foreach (var remainingMod in _mods.Skip(idx))
|
||||||
|
--remainingMod.Index;
|
||||||
|
|
||||||
|
Penumbra.Log.Debug($"Deleted mod {mod.Name}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Load a new mod and add it to the manager if successful. </summary>
|
||||||
|
public void AddMod(DirectoryInfo modFolder)
|
||||||
|
{
|
||||||
|
if (_mods.Any(m => m.ModPath.Name == modFolder.Name))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Mod.Creator.SplitMultiGroups(modFolder);
|
||||||
|
var mod = Mod.LoadMod(this, modFolder, true);
|
||||||
|
if (mod == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
mod.Index = _mods.Count;
|
||||||
|
_mods.Add(mod);
|
||||||
|
ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath);
|
||||||
|
Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum NewDirectoryState
|
||||||
|
{
|
||||||
|
NonExisting,
|
||||||
|
ExistsEmpty,
|
||||||
|
ExistsNonEmpty,
|
||||||
|
ExistsAsFile,
|
||||||
|
ContainsInvalidSymbols,
|
||||||
|
Identical,
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Return the state of the new potential name of a directory. </summary>
|
||||||
|
public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory)
|
||||||
|
{
|
||||||
|
directory = null;
|
||||||
|
if (newName.Length == 0)
|
||||||
|
return NewDirectoryState.Empty;
|
||||||
|
|
||||||
|
if (oldName == newName)
|
||||||
|
return NewDirectoryState.Identical;
|
||||||
|
|
||||||
|
var fixedNewName = Mod.Creator.ReplaceBadXivSymbols(newName);
|
||||||
|
if (fixedNewName != newName)
|
||||||
|
return NewDirectoryState.ContainsInvalidSymbols;
|
||||||
|
|
||||||
|
directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName));
|
||||||
|
if (File.Exists(directory.FullName))
|
||||||
|
return NewDirectoryState.ExistsAsFile;
|
||||||
|
|
||||||
|
if (!Directory.Exists(directory.FullName))
|
||||||
|
return NewDirectoryState.NonExisting;
|
||||||
|
|
||||||
|
if (directory.EnumerateFileSystemInfos().Any())
|
||||||
|
return NewDirectoryState.ExistsNonEmpty;
|
||||||
|
|
||||||
|
return NewDirectoryState.ExistsEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary> Add new mods to NewMods and remove deleted mods from NewMods. </summary>
|
||||||
|
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
||||||
|
DirectoryInfo? newDirectory)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case ModPathChangeType.Added:
|
||||||
|
NewMods.Add(mod);
|
||||||
|
break;
|
||||||
|
case ModPathChangeType.Deleted:
|
||||||
|
NewMods.Remove(mod);
|
||||||
|
break;
|
||||||
|
case ModPathChangeType.Moved:
|
||||||
|
if (oldDirectory != null && newDirectory != null)
|
||||||
|
DataEditor.MoveDataFile(oldDirectory, newDirectory);
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
|
||||||
|
|
||||||
public sealed partial class Mod
|
|
||||||
{
|
|
||||||
public partial class Manager
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,371 +11,367 @@ using Penumbra.Util;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public sealed partial class Mod
|
public sealed partial class ModManager
|
||||||
{
|
{
|
||||||
public sealed partial class Manager
|
public delegate void ModOptionChangeDelegate(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx);
|
||||||
|
public event ModOptionChangeDelegate ModOptionChanged;
|
||||||
|
|
||||||
|
public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type)
|
||||||
{
|
{
|
||||||
public delegate void ModOptionChangeDelegate(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx);
|
var group = mod._groups[groupIdx];
|
||||||
public event ModOptionChangeDelegate ModOptionChanged;
|
if (group.Type == type)
|
||||||
|
return;
|
||||||
|
|
||||||
public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type)
|
mod._groups[groupIdx] = group.Convert(type);
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption)
|
||||||
|
{
|
||||||
|
var group = mod._groups[groupIdx];
|
||||||
|
if (group.DefaultSettings == defaultOption)
|
||||||
|
return;
|
||||||
|
|
||||||
|
group.DefaultSettings = defaultOption;
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RenameModGroup(Mod mod, int groupIdx, string newName)
|
||||||
|
{
|
||||||
|
var group = mod._groups[groupIdx];
|
||||||
|
var oldName = group.Name;
|
||||||
|
if (oldName == newName || !VerifyFileName(mod, group, newName, true))
|
||||||
|
return;
|
||||||
|
|
||||||
|
group.DeleteFile(mod.ModPath, groupIdx);
|
||||||
|
|
||||||
|
var _ = group switch
|
||||||
{
|
{
|
||||||
var group = mod._groups[groupIdx];
|
SingleModGroup s => s.Name = newName,
|
||||||
if (group.Type == type)
|
MultiModGroup m => m.Name = newName,
|
||||||
return;
|
_ => newName,
|
||||||
|
};
|
||||||
|
|
||||||
mod._groups[groupIdx] = group.Convert(type);
|
ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1);
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption)
|
public void AddModGroup(Mod mod, GroupType type, string newName)
|
||||||
{
|
{
|
||||||
var group = mod._groups[groupIdx];
|
if (!VerifyFileName(mod, null, newName, true))
|
||||||
if (group.DefaultSettings == defaultOption)
|
return;
|
||||||
return;
|
|
||||||
|
|
||||||
group.DefaultSettings = defaultOption;
|
var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1;
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RenameModGroup(Mod mod, int groupIdx, string newName)
|
mod._groups.Add(type == GroupType.Multi
|
||||||
{
|
? new MultiModGroup
|
||||||
var group = mod._groups[groupIdx];
|
|
||||||
var oldName = group.Name;
|
|
||||||
if (oldName == newName || !VerifyFileName(mod, group, newName, true))
|
|
||||||
return;
|
|
||||||
|
|
||||||
group.DeleteFile(mod.ModPath, groupIdx);
|
|
||||||
|
|
||||||
var _ = group switch
|
|
||||||
{
|
{
|
||||||
SingleModGroup s => s.Name = newName,
|
Name = newName,
|
||||||
MultiModGroup m => m.Name = newName,
|
Priority = maxPriority,
|
||||||
_ => newName,
|
|
||||||
};
|
|
||||||
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddModGroup(Mod mod, GroupType type, string newName)
|
|
||||||
{
|
|
||||||
if (!VerifyFileName(mod, null, newName, true))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1;
|
|
||||||
|
|
||||||
mod._groups.Add(type == GroupType.Multi
|
|
||||||
? new MultiModGroup
|
|
||||||
{
|
|
||||||
Name = newName,
|
|
||||||
Priority = maxPriority,
|
|
||||||
}
|
|
||||||
: new SingleModGroup
|
|
||||||
{
|
|
||||||
Name = newName,
|
|
||||||
Priority = maxPriority,
|
|
||||||
});
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DeleteModGroup(Mod mod, int groupIdx)
|
|
||||||
{
|
|
||||||
var group = mod._groups[groupIdx];
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1);
|
|
||||||
mod._groups.RemoveAt(groupIdx);
|
|
||||||
UpdateSubModPositions(mod, groupIdx);
|
|
||||||
group.DeleteFile(mod.ModPath, groupIdx);
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo)
|
|
||||||
{
|
|
||||||
if (mod._groups.Move(groupIdxFrom, groupIdxTo))
|
|
||||||
{
|
|
||||||
UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo));
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo);
|
|
||||||
}
|
}
|
||||||
}
|
: new SingleModGroup
|
||||||
|
|
||||||
private static void UpdateSubModPositions(Mod mod, int fromGroup)
|
|
||||||
{
|
|
||||||
foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup))
|
|
||||||
{
|
{
|
||||||
foreach (var (o, optionIdx) in group.OfType<SubMod>().WithIndex())
|
Name = newName,
|
||||||
o.SetPosition(groupIdx, optionIdx);
|
Priority = maxPriority,
|
||||||
}
|
});
|
||||||
}
|
ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription)
|
public void DeleteModGroup(Mod mod, int groupIdx)
|
||||||
|
{
|
||||||
|
var group = mod._groups[groupIdx];
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1);
|
||||||
|
mod._groups.RemoveAt(groupIdx);
|
||||||
|
UpdateSubModPositions(mod, groupIdx);
|
||||||
|
group.DeleteFile(mod.ModPath, groupIdx);
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo)
|
||||||
|
{
|
||||||
|
if (mod._groups.Move(groupIdxFrom, groupIdxTo))
|
||||||
{
|
{
|
||||||
var group = mod._groups[groupIdx];
|
UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo));
|
||||||
if (group.Description == newDescription)
|
ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo);
|
||||||
return;
|
|
||||||
|
|
||||||
var _ = group switch
|
|
||||||
{
|
|
||||||
SingleModGroup s => s.Description = newDescription,
|
|
||||||
MultiModGroup m => m.Description = newDescription,
|
|
||||||
_ => newDescription,
|
|
||||||
};
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription)
|
|
||||||
{
|
|
||||||
var group = mod._groups[groupIdx];
|
|
||||||
var option = group[optionIdx];
|
|
||||||
if (option.Description == newDescription || option is not SubMod s)
|
|
||||||
return;
|
|
||||||
|
|
||||||
s.Description = newDescription;
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority)
|
|
||||||
{
|
|
||||||
var group = mod._groups[groupIdx];
|
|
||||||
if (group.Priority == newPriority)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var _ = group switch
|
|
||||||
{
|
|
||||||
SingleModGroup s => s.Priority = newPriority,
|
|
||||||
MultiModGroup m => m.Priority = newPriority,
|
|
||||||
_ => newPriority,
|
|
||||||
};
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority)
|
|
||||||
{
|
|
||||||
switch (mod._groups[groupIdx])
|
|
||||||
{
|
|
||||||
case SingleModGroup:
|
|
||||||
ChangeGroupPriority(mod, groupIdx, newPriority);
|
|
||||||
break;
|
|
||||||
case MultiModGroup m:
|
|
||||||
if (m.PrioritizedOptions[optionIdx].Priority == newPriority)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority);
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName)
|
|
||||||
{
|
|
||||||
switch (mod._groups[groupIdx])
|
|
||||||
{
|
|
||||||
case SingleModGroup s:
|
|
||||||
if (s.OptionData[optionIdx].Name == newName)
|
|
||||||
return;
|
|
||||||
|
|
||||||
s.OptionData[optionIdx].Name = newName;
|
|
||||||
break;
|
|
||||||
case MultiModGroup m:
|
|
||||||
var option = m.PrioritizedOptions[optionIdx].Mod;
|
|
||||||
if (option.Name == newName)
|
|
||||||
return;
|
|
||||||
|
|
||||||
option.Name = newName;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddOption(Mod mod, int groupIdx, string newName)
|
|
||||||
{
|
|
||||||
var group = mod._groups[groupIdx];
|
|
||||||
var subMod = new SubMod(mod) { Name = newName };
|
|
||||||
subMod.SetPosition(groupIdx, group.Count);
|
|
||||||
switch (group)
|
|
||||||
{
|
|
||||||
case SingleModGroup s:
|
|
||||||
s.OptionData.Add(subMod);
|
|
||||||
break;
|
|
||||||
case MultiModGroup m:
|
|
||||||
m.PrioritizedOptions.Add((subMod, 0));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0)
|
|
||||||
{
|
|
||||||
if (option is not SubMod o)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var group = mod._groups[groupIdx];
|
|
||||||
if (group.Count > 63)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error(
|
|
||||||
$"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, "
|
|
||||||
+ "since only up to 64 options are supported in one group.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
o.SetPosition(groupIdx, group.Count);
|
|
||||||
|
|
||||||
switch (group)
|
|
||||||
{
|
|
||||||
case SingleModGroup s:
|
|
||||||
s.OptionData.Add(o);
|
|
||||||
break;
|
|
||||||
case MultiModGroup m:
|
|
||||||
m.PrioritizedOptions.Add((o, priority));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DeleteOption(Mod mod, int groupIdx, int optionIdx)
|
|
||||||
{
|
|
||||||
var group = mod._groups[groupIdx];
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
|
|
||||||
switch (group)
|
|
||||||
{
|
|
||||||
case SingleModGroup s:
|
|
||||||
s.OptionData.RemoveAt(optionIdx);
|
|
||||||
|
|
||||||
break;
|
|
||||||
case MultiModGroup m:
|
|
||||||
m.PrioritizedOptions.RemoveAt(optionIdx);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
group.UpdatePositions(optionIdx);
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo)
|
|
||||||
{
|
|
||||||
var group = mod._groups[groupIdx];
|
|
||||||
if (group.MoveOption(optionIdxFrom, optionIdxTo))
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet<MetaManipulation> manipulations)
|
|
||||||
{
|
|
||||||
var subMod = GetSubMod(mod, groupIdx, optionIdx);
|
|
||||||
if (subMod.Manipulations.Count == manipulations.Count
|
|
||||||
&& subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m)))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
|
|
||||||
subMod.ManipulationData = manipulations;
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> replacements)
|
|
||||||
{
|
|
||||||
var subMod = GetSubMod(mod, groupIdx, optionIdx);
|
|
||||||
if (subMod.FileData.SetEquals(replacements))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
|
|
||||||
subMod.FileData = replacements;
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> additions)
|
|
||||||
{
|
|
||||||
var subMod = GetSubMod(mod, groupIdx, optionIdx);
|
|
||||||
var oldCount = subMod.FileData.Count;
|
|
||||||
subMod.FileData.AddFrom(additions);
|
|
||||||
if (oldCount != subMod.FileData.Count)
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> swaps)
|
|
||||||
{
|
|
||||||
var subMod = GetSubMod(mod, groupIdx, optionIdx);
|
|
||||||
if (subMod.FileSwapData.SetEquals(swaps))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
|
|
||||||
subMod.FileSwapData = swaps;
|
|
||||||
ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message)
|
|
||||||
{
|
|
||||||
var path = newName.RemoveInvalidPathSymbols();
|
|
||||||
if (path.Length != 0
|
|
||||||
&& !mod.Groups.Any(o => !ReferenceEquals(o, group)
|
|
||||||
&& string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (message)
|
|
||||||
Penumbra.ChatService.NotificationMessage(
|
|
||||||
$"Could not name option {newName} because option with same filename {path} already exists.",
|
|
||||||
"Warning", NotificationType.Warning);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx)
|
|
||||||
{
|
|
||||||
if (groupIdx == -1 && optionIdx == 0)
|
|
||||||
return mod._default;
|
|
||||||
|
|
||||||
return mod._groups[groupIdx] switch
|
|
||||||
{
|
|
||||||
SingleModGroup s => s.OptionData[optionIdx],
|
|
||||||
MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod,
|
|
||||||
_ => throw new InvalidOperationException(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2)
|
|
||||||
{
|
|
||||||
if (type == ModOptionChangeType.PrepareChange)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// File deletion is handled in the actual function.
|
|
||||||
if (type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved)
|
|
||||||
{
|
|
||||||
mod.SaveAllGroups();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (groupIdx == -1)
|
|
||||||
mod.SaveDefaultModDelayed();
|
|
||||||
else
|
|
||||||
IModGroup.SaveDelayed(mod._groups[groupIdx], mod.ModPath, groupIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ComputeChangedItems()
|
|
||||||
{
|
|
||||||
mod.ComputeChangedItems();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// State can not change on adding groups, as they have no immediate options.
|
|
||||||
var unused = type switch
|
|
||||||
{
|
|
||||||
ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(),
|
|
||||||
ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(),
|
|
||||||
ModOptionChangeType.GroupMoved => false,
|
|
||||||
ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption),
|
|
||||||
ModOptionChangeType.PriorityChanged => false,
|
|
||||||
ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(),
|
|
||||||
ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(),
|
|
||||||
ModOptionChangeType.OptionMoved => false,
|
|
||||||
ModOptionChangeType.OptionFilesChanged => ComputeChangedItems()
|
|
||||||
& (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))),
|
|
||||||
ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems()
|
|
||||||
& (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))),
|
|
||||||
ModOptionChangeType.OptionMetaChanged => ComputeChangedItems()
|
|
||||||
& (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))),
|
|
||||||
ModOptionChangeType.DisplayChange => false,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void UpdateSubModPositions(Mod mod, int fromGroup)
|
||||||
|
{
|
||||||
|
foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup))
|
||||||
|
{
|
||||||
|
foreach (var (o, optionIdx) in group.OfType<SubMod>().WithIndex())
|
||||||
|
o.SetPosition(groupIdx, optionIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription)
|
||||||
|
{
|
||||||
|
var group = mod._groups[groupIdx];
|
||||||
|
if (group.Description == newDescription)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var _ = group switch
|
||||||
|
{
|
||||||
|
SingleModGroup s => s.Description = newDescription,
|
||||||
|
MultiModGroup m => m.Description = newDescription,
|
||||||
|
_ => newDescription,
|
||||||
|
};
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription)
|
||||||
|
{
|
||||||
|
var group = mod._groups[groupIdx];
|
||||||
|
var option = group[optionIdx];
|
||||||
|
if (option.Description == newDescription || option is not SubMod s)
|
||||||
|
return;
|
||||||
|
|
||||||
|
s.Description = newDescription;
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority)
|
||||||
|
{
|
||||||
|
var group = mod._groups[groupIdx];
|
||||||
|
if (group.Priority == newPriority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var _ = group switch
|
||||||
|
{
|
||||||
|
SingleModGroup s => s.Priority = newPriority,
|
||||||
|
MultiModGroup m => m.Priority = newPriority,
|
||||||
|
_ => newPriority,
|
||||||
|
};
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority)
|
||||||
|
{
|
||||||
|
switch (mod._groups[groupIdx])
|
||||||
|
{
|
||||||
|
case SingleModGroup:
|
||||||
|
ChangeGroupPriority(mod, groupIdx, newPriority);
|
||||||
|
break;
|
||||||
|
case MultiModGroup m:
|
||||||
|
if (m.PrioritizedOptions[optionIdx].Priority == newPriority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority);
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName)
|
||||||
|
{
|
||||||
|
switch (mod._groups[groupIdx])
|
||||||
|
{
|
||||||
|
case SingleModGroup s:
|
||||||
|
if (s.OptionData[optionIdx].Name == newName)
|
||||||
|
return;
|
||||||
|
|
||||||
|
s.OptionData[optionIdx].Name = newName;
|
||||||
|
break;
|
||||||
|
case MultiModGroup m:
|
||||||
|
var option = m.PrioritizedOptions[optionIdx].Mod;
|
||||||
|
if (option.Name == newName)
|
||||||
|
return;
|
||||||
|
|
||||||
|
option.Name = newName;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddOption(Mod mod, int groupIdx, string newName)
|
||||||
|
{
|
||||||
|
var group = mod._groups[groupIdx];
|
||||||
|
var subMod = new SubMod(mod) { Name = newName };
|
||||||
|
subMod.SetPosition(groupIdx, group.Count);
|
||||||
|
switch (group)
|
||||||
|
{
|
||||||
|
case SingleModGroup s:
|
||||||
|
s.OptionData.Add(subMod);
|
||||||
|
break;
|
||||||
|
case MultiModGroup m:
|
||||||
|
m.PrioritizedOptions.Add((subMod, 0));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0)
|
||||||
|
{
|
||||||
|
if (option is not SubMod o)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var group = mod._groups[groupIdx];
|
||||||
|
if (group.Count > 63)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error(
|
||||||
|
$"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, "
|
||||||
|
+ "since only up to 64 options are supported in one group.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
o.SetPosition(groupIdx, group.Count);
|
||||||
|
|
||||||
|
switch (group)
|
||||||
|
{
|
||||||
|
case SingleModGroup s:
|
||||||
|
s.OptionData.Add(o);
|
||||||
|
break;
|
||||||
|
case MultiModGroup m:
|
||||||
|
m.PrioritizedOptions.Add((o, priority));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteOption(Mod mod, int groupIdx, int optionIdx)
|
||||||
|
{
|
||||||
|
var group = mod._groups[groupIdx];
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
|
||||||
|
switch (group)
|
||||||
|
{
|
||||||
|
case SingleModGroup s:
|
||||||
|
s.OptionData.RemoveAt(optionIdx);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case MultiModGroup m:
|
||||||
|
m.PrioritizedOptions.RemoveAt(optionIdx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
group.UpdatePositions(optionIdx);
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo)
|
||||||
|
{
|
||||||
|
var group = mod._groups[groupIdx];
|
||||||
|
if (group.MoveOption(optionIdxFrom, optionIdxTo))
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet<MetaManipulation> manipulations)
|
||||||
|
{
|
||||||
|
var subMod = GetSubMod(mod, groupIdx, optionIdx);
|
||||||
|
if (subMod.Manipulations.Count == manipulations.Count
|
||||||
|
&& subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m)))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
|
||||||
|
subMod.ManipulationData = manipulations;
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> replacements)
|
||||||
|
{
|
||||||
|
var subMod = GetSubMod(mod, groupIdx, optionIdx);
|
||||||
|
if (subMod.FileData.SetEquals(replacements))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
|
||||||
|
subMod.FileData = replacements;
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> additions)
|
||||||
|
{
|
||||||
|
var subMod = GetSubMod(mod, groupIdx, optionIdx);
|
||||||
|
var oldCount = subMod.FileData.Count;
|
||||||
|
subMod.FileData.AddFrom(additions);
|
||||||
|
if (oldCount != subMod.FileData.Count)
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> swaps)
|
||||||
|
{
|
||||||
|
var subMod = GetSubMod(mod, groupIdx, optionIdx);
|
||||||
|
if (subMod.FileSwapData.SetEquals(swaps))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
|
||||||
|
subMod.FileSwapData = swaps;
|
||||||
|
ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message)
|
||||||
|
{
|
||||||
|
var path = newName.RemoveInvalidPathSymbols();
|
||||||
|
if (path.Length != 0
|
||||||
|
&& !mod.Groups.Any(o => !ReferenceEquals(o, group)
|
||||||
|
&& string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (message)
|
||||||
|
Penumbra.ChatService.NotificationMessage(
|
||||||
|
$"Could not name option {newName} because option with same filename {path} already exists.",
|
||||||
|
"Warning", NotificationType.Warning);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx)
|
||||||
|
{
|
||||||
|
if (groupIdx == -1 && optionIdx == 0)
|
||||||
|
return mod._default;
|
||||||
|
|
||||||
|
return mod._groups[groupIdx] switch
|
||||||
|
{
|
||||||
|
SingleModGroup s => s.OptionData[optionIdx],
|
||||||
|
MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod,
|
||||||
|
_ => throw new InvalidOperationException(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2)
|
||||||
|
{
|
||||||
|
if (type == ModOptionChangeType.PrepareChange)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// File deletion is handled in the actual function.
|
||||||
|
if (type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved)
|
||||||
|
{
|
||||||
|
mod.SaveAllGroups();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (groupIdx == -1)
|
||||||
|
mod.SaveDefaultModDelayed();
|
||||||
|
else
|
||||||
|
IModGroup.SaveDelayed(mod._groups[groupIdx], mod.ModPath, groupIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ComputeChangedItems()
|
||||||
|
{
|
||||||
|
mod.ComputeChangedItems();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// State can not change on adding groups, as they have no immediate options.
|
||||||
|
var unused = type switch
|
||||||
|
{
|
||||||
|
ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(),
|
||||||
|
ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(),
|
||||||
|
ModOptionChangeType.GroupMoved => false,
|
||||||
|
ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption),
|
||||||
|
ModOptionChangeType.PriorityChanged => false,
|
||||||
|
ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(),
|
||||||
|
ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(),
|
||||||
|
ModOptionChangeType.OptionMoved => false,
|
||||||
|
ModOptionChangeType.OptionFilesChanged => ComputeChangedItems()
|
||||||
|
& (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))),
|
||||||
|
ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems()
|
||||||
|
& (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))),
|
||||||
|
ModOptionChangeType.OptionMetaChanged => ComputeChangedItems()
|
||||||
|
& (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))),
|
||||||
|
ModOptionChangeType.DisplayChange => false,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,169 +6,144 @@ using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public sealed partial class Mod
|
public sealed partial class ModManager
|
||||||
{
|
{
|
||||||
public sealed partial class Manager
|
public DirectoryInfo BasePath { get; private set; } = null!;
|
||||||
|
private DirectoryInfo? _exportDirectory;
|
||||||
|
|
||||||
|
public DirectoryInfo ExportDirectory
|
||||||
|
=> _exportDirectory ?? BasePath;
|
||||||
|
|
||||||
|
public bool Valid { get; private set; }
|
||||||
|
|
||||||
|
public event Action? ModDiscoveryStarted;
|
||||||
|
public event Action? ModDiscoveryFinished;
|
||||||
|
public event Action<string, bool> ModDirectoryChanged;
|
||||||
|
|
||||||
|
// Change the mod base directory and discover available mods.
|
||||||
|
public void DiscoverMods(string newDir)
|
||||||
{
|
{
|
||||||
public DirectoryInfo BasePath { get; private set; } = null!;
|
SetBaseDirectory(newDir, false);
|
||||||
private DirectoryInfo? _exportDirectory;
|
DiscoverMods();
|
||||||
|
}
|
||||||
|
|
||||||
public DirectoryInfo ExportDirectory
|
// Set the mod base directory.
|
||||||
=> _exportDirectory ?? BasePath;
|
// If its not the first time, check if it is the same directory as before.
|
||||||
|
// Also checks if the directory is available and tries to create it if it is not.
|
||||||
|
private void SetBaseDirectory(string newPath, bool firstTime)
|
||||||
|
{
|
||||||
|
if (!firstTime && string.Equals(newPath, Penumbra.Config.ModDirectory, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
public bool Valid { get; private set; }
|
if (newPath.Length == 0)
|
||||||
|
|
||||||
public event Action? ModDiscoveryStarted;
|
|
||||||
public event Action? ModDiscoveryFinished;
|
|
||||||
public event Action< string, bool > ModDirectoryChanged;
|
|
||||||
|
|
||||||
// Change the mod base directory and discover available mods.
|
|
||||||
public void DiscoverMods( string newDir )
|
|
||||||
{
|
{
|
||||||
SetBaseDirectory( newDir, false );
|
Valid = false;
|
||||||
DiscoverMods();
|
BasePath = new DirectoryInfo(".");
|
||||||
|
if (Penumbra.Config.ModDirectory != BasePath.FullName)
|
||||||
|
ModDirectoryChanged.Invoke(string.Empty, false);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
// Set the mod base directory.
|
|
||||||
// If its not the first time, check if it is the same directory as before.
|
|
||||||
// Also checks if the directory is available and tries to create it if it is not.
|
|
||||||
private void SetBaseDirectory( string newPath, bool firstTime )
|
|
||||||
{
|
{
|
||||||
if( !firstTime && string.Equals( newPath, Penumbra.Config.ModDirectory, StringComparison.OrdinalIgnoreCase ) )
|
var newDir = new DirectoryInfo(newPath);
|
||||||
{
|
if (!newDir.Exists)
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( newPath.Length == 0 )
|
|
||||||
{
|
|
||||||
Valid = false;
|
|
||||||
BasePath = new DirectoryInfo( "." );
|
|
||||||
if( Penumbra.Config.ModDirectory != BasePath.FullName )
|
|
||||||
{
|
|
||||||
ModDirectoryChanged.Invoke( string.Empty, false );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var newDir = new DirectoryInfo( newPath );
|
|
||||||
if( !newDir.Exists )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory( newDir.FullName );
|
|
||||||
newDir.Refresh();
|
|
||||||
}
|
|
||||||
catch( Exception e )
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BasePath = newDir;
|
|
||||||
Valid = Directory.Exists( newDir.FullName );
|
|
||||||
if( Penumbra.Config.ModDirectory != BasePath.FullName )
|
|
||||||
{
|
|
||||||
ModDirectoryChanged.Invoke( BasePath.FullName, Valid );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OnModDirectoryChange( string newPath, bool _ )
|
|
||||||
{
|
|
||||||
Penumbra.Log.Information( $"Set new mod base directory from {Penumbra.Config.ModDirectory} to {newPath}." );
|
|
||||||
Penumbra.Config.ModDirectory = newPath;
|
|
||||||
Penumbra.Config.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discover new mods.
|
|
||||||
public void DiscoverMods()
|
|
||||||
{
|
|
||||||
NewMods.Clear();
|
|
||||||
ModDiscoveryStarted?.Invoke();
|
|
||||||
_mods.Clear();
|
|
||||||
BasePath.Refresh();
|
|
||||||
|
|
||||||
if( Valid && BasePath.Exists )
|
|
||||||
{
|
|
||||||
var options = new ParallelOptions()
|
|
||||||
{
|
|
||||||
MaxDegreeOfParallelism = Environment.ProcessorCount / 2,
|
|
||||||
};
|
|
||||||
var queue = new ConcurrentQueue< Mod >();
|
|
||||||
Parallel.ForEach( BasePath.EnumerateDirectories(), options, dir =>
|
|
||||||
{
|
|
||||||
var mod = LoadMod( this, dir, false );
|
|
||||||
if( mod != null )
|
|
||||||
{
|
|
||||||
queue.Enqueue( mod );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
foreach( var mod in queue )
|
|
||||||
{
|
|
||||||
mod.Index = _mods.Count;
|
|
||||||
_mods.Add( mod );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ModDiscoveryFinished?.Invoke();
|
|
||||||
Penumbra.Log.Information( "Rediscovered mods." );
|
|
||||||
|
|
||||||
if( MigrateModBackups )
|
|
||||||
{
|
|
||||||
ModBackup.MigrateZipToPmp( this );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateExportDirectory( string newDirectory, bool change )
|
|
||||||
{
|
|
||||||
if( newDirectory.Length == 0 )
|
|
||||||
{
|
|
||||||
if( _exportDirectory == null )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_exportDirectory = null;
|
|
||||||
_config.ExportDirectory = string.Empty;
|
|
||||||
_config.Save();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dir = new DirectoryInfo( newDirectory );
|
|
||||||
if( dir.FullName.Equals( _exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase ) )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( !dir.Exists )
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory( dir.FullName );
|
Directory.CreateDirectory(newDir.FullName);
|
||||||
|
newDir.Refresh();
|
||||||
}
|
}
|
||||||
catch( Exception e )
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Penumbra.Log.Error( $"Could not create Export Directory:\n{e}" );
|
Penumbra.Log.Error($"Could not create specified mod directory {newDir.FullName}:\n{e}");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if( change )
|
BasePath = newDir;
|
||||||
|
Valid = Directory.Exists(newDir.FullName);
|
||||||
|
if (Penumbra.Config.ModDirectory != BasePath.FullName)
|
||||||
|
ModDirectoryChanged.Invoke(BasePath.FullName, Valid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnModDirectoryChange(string newPath, bool _)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Information($"Set new mod base directory from {Penumbra.Config.ModDirectory} to {newPath}.");
|
||||||
|
Penumbra.Config.ModDirectory = newPath;
|
||||||
|
Penumbra.Config.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover new mods.
|
||||||
|
public void DiscoverMods()
|
||||||
|
{
|
||||||
|
NewMods.Clear();
|
||||||
|
ModDiscoveryStarted?.Invoke();
|
||||||
|
_mods.Clear();
|
||||||
|
BasePath.Refresh();
|
||||||
|
|
||||||
|
if (Valid && BasePath.Exists)
|
||||||
|
{
|
||||||
|
var options = new ParallelOptions()
|
||||||
{
|
{
|
||||||
foreach( var mod in _mods )
|
MaxDegreeOfParallelism = Environment.ProcessorCount / 2,
|
||||||
{
|
};
|
||||||
new ModBackup( this, mod ).Move( dir.FullName );
|
var queue = new ConcurrentQueue<Mod>();
|
||||||
}
|
Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir =>
|
||||||
}
|
|
||||||
|
|
||||||
_exportDirectory = dir;
|
|
||||||
|
|
||||||
if( change )
|
|
||||||
{
|
{
|
||||||
_config.ExportDirectory = dir.FullName;
|
var mod = Mod.LoadMod(this, dir, false);
|
||||||
_config.Save();
|
if (mod != null)
|
||||||
|
queue.Enqueue(mod);
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var mod in queue)
|
||||||
|
{
|
||||||
|
mod.Index = _mods.Count;
|
||||||
|
_mods.Add(mod);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ModDiscoveryFinished?.Invoke();
|
||||||
|
Penumbra.Log.Information("Rediscovered mods.");
|
||||||
|
|
||||||
|
if (MigrateModBackups)
|
||||||
|
ModBackup.MigrateZipToPmp(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateExportDirectory(string newDirectory, bool change)
|
||||||
|
{
|
||||||
|
if (newDirectory.Length == 0)
|
||||||
|
{
|
||||||
|
if (_exportDirectory == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_exportDirectory = null;
|
||||||
|
_config.ExportDirectory = string.Empty;
|
||||||
|
_config.Save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dir = new DirectoryInfo(newDirectory);
|
||||||
|
if (dir.FullName.Equals(_exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!dir.Exists)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(dir.FullName);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error($"Could not create Export Directory:\n{e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change)
|
||||||
|
foreach (var mod in _mods)
|
||||||
|
new ModBackup(this, mod).Move(dir.FullName);
|
||||||
|
|
||||||
|
_exportDirectory = dir;
|
||||||
|
|
||||||
|
if (change)
|
||||||
|
{
|
||||||
|
_config.ExportDirectory = dir.FullName;
|
||||||
|
_config.Save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,73 +7,70 @@ using Penumbra.Util;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public sealed partial class Mod
|
public sealed partial class ModManager : IReadOnlyList<Mod>
|
||||||
{
|
{
|
||||||
public sealed partial class Manager : IReadOnlyList<Mod>
|
// Set when reading Config and migrating from v4 to v5.
|
||||||
|
public static bool MigrateModBackups = false;
|
||||||
|
|
||||||
|
// An easily accessible set of new mods.
|
||||||
|
// Mods are added when they are created or imported.
|
||||||
|
// Mods are removed when they are deleted or when they are toggled in any collection.
|
||||||
|
// Also gets cleared on mod rediscovery.
|
||||||
|
public readonly HashSet<Mod> NewMods = new();
|
||||||
|
|
||||||
|
private readonly List<Mod> _mods = new();
|
||||||
|
|
||||||
|
public Mod this[int idx]
|
||||||
|
=> _mods[idx];
|
||||||
|
|
||||||
|
public Mod this[Index idx]
|
||||||
|
=> _mods[idx];
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
=> _mods.Count;
|
||||||
|
|
||||||
|
public IEnumerator<Mod> GetEnumerator()
|
||||||
|
=> _mods.GetEnumerator();
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
=> GetEnumerator();
|
||||||
|
|
||||||
|
private readonly Configuration _config;
|
||||||
|
private readonly CommunicatorService _communicator;
|
||||||
|
public readonly ModDataEditor DataEditor;
|
||||||
|
|
||||||
|
public ModManager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor)
|
||||||
{
|
{
|
||||||
// Set when reading Config and migrating from v4 to v5.
|
using var timer = time.Measure(StartTimeType.Mods);
|
||||||
public static bool MigrateModBackups = false;
|
_config = config;
|
||||||
|
_communicator = communicator;
|
||||||
|
DataEditor = dataEditor;
|
||||||
|
ModDirectoryChanged += OnModDirectoryChange;
|
||||||
|
SetBaseDirectory(config.ModDirectory, true);
|
||||||
|
UpdateExportDirectory(_config.ExportDirectory, false);
|
||||||
|
ModOptionChanged += OnModOptionChange;
|
||||||
|
ModPathChanged += OnModPathChange;
|
||||||
|
DiscoverMods();
|
||||||
|
}
|
||||||
|
|
||||||
// An easily accessible set of new mods.
|
|
||||||
// Mods are added when they are created or imported.
|
|
||||||
// Mods are removed when they are deleted or when they are toggled in any collection.
|
|
||||||
// Also gets cleared on mod rediscovery.
|
|
||||||
public readonly HashSet<Mod> NewMods = new();
|
|
||||||
|
|
||||||
private readonly List<Mod> _mods = new();
|
// Try to obtain a mod by its directory name (unique identifier, preferred),
|
||||||
|
// or the first mod of the given name if no directory fits.
|
||||||
public Mod this[int idx]
|
public bool TryGetMod(string modDirectory, string modName, [NotNullWhen(true)] out Mod? mod)
|
||||||
=> _mods[idx];
|
{
|
||||||
|
mod = null;
|
||||||
public Mod this[Index idx]
|
foreach (var m in _mods)
|
||||||
=> _mods[idx];
|
|
||||||
|
|
||||||
public int Count
|
|
||||||
=> _mods.Count;
|
|
||||||
|
|
||||||
public IEnumerator<Mod> GetEnumerator()
|
|
||||||
=> _mods.GetEnumerator();
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
=> GetEnumerator();
|
|
||||||
|
|
||||||
private readonly Configuration _config;
|
|
||||||
private readonly CommunicatorService _communicator;
|
|
||||||
public readonly ModDataEditor DataEditor;
|
|
||||||
|
|
||||||
public Manager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor)
|
|
||||||
{
|
{
|
||||||
using var timer = time.Measure(StartTimeType.Mods);
|
if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase))
|
||||||
_config = config;
|
|
||||||
_communicator = communicator;
|
|
||||||
DataEditor = dataEditor;
|
|
||||||
ModDirectoryChanged += OnModDirectoryChange;
|
|
||||||
SetBaseDirectory(config.ModDirectory, true);
|
|
||||||
UpdateExportDirectory(_config.ExportDirectory, false);
|
|
||||||
ModOptionChanged += OnModOptionChange;
|
|
||||||
ModPathChanged += OnModPathChange;
|
|
||||||
DiscoverMods();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Try to obtain a mod by its directory name (unique identifier, preferred),
|
|
||||||
// or the first mod of the given name if no directory fits.
|
|
||||||
public bool TryGetMod(string modDirectory, string modName, [NotNullWhen(true)] out Mod? mod)
|
|
||||||
{
|
|
||||||
mod = null;
|
|
||||||
foreach (var m in _mods)
|
|
||||||
{
|
{
|
||||||
if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase))
|
mod = m;
|
||||||
{
|
return true;
|
||||||
mod = m;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m.Name == modName)
|
|
||||||
mod ??= m;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mod != null;
|
if (m.Name == modName)
|
||||||
|
mod ??= m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return mod != null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,10 @@ public enum ModPathChangeType
|
||||||
|
|
||||||
public partial class Mod
|
public partial class Mod
|
||||||
{
|
{
|
||||||
public DirectoryInfo ModPath { get; private set; }
|
public DirectoryInfo ModPath { get; internal set; }
|
||||||
public string Identifier
|
public string Identifier
|
||||||
=> Index >= 0 ? ModPath.Name : Name;
|
=> Index >= 0 ? ModPath.Name : Name;
|
||||||
public int Index { get; private set; } = -1;
|
public int Index { get; internal set; } = -1;
|
||||||
|
|
||||||
public bool IsTemporary
|
public bool IsTemporary
|
||||||
=> Index < 0;
|
=> Index < 0;
|
||||||
|
|
@ -33,7 +33,7 @@ public partial class Mod
|
||||||
_default = new SubMod( this );
|
_default = new SubMod( this );
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Mod? LoadMod( Manager modManager, DirectoryInfo modPath, bool incorporateMetaChanges )
|
public static Mod? LoadMod( ModManager modManager, DirectoryInfo modPath, bool incorporateMetaChanges )
|
||||||
{
|
{
|
||||||
modPath.Refresh();
|
modPath.Refresh();
|
||||||
if( !modPath.Exists )
|
if( !modPath.Exists )
|
||||||
|
|
@ -52,7 +52,7 @@ public partial class Mod
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool Reload(Manager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange )
|
internal bool Reload(ModManager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange )
|
||||||
{
|
{
|
||||||
modDataChange = ModDataChangeType.Deletion;
|
modDataChange = ModDataChangeType.Deletion;
|
||||||
ModPath.Refresh();
|
ModPath.Refresh();
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ public sealed partial class Mod
|
||||||
public SortedList< string, object? > ChangedItems { get; } = new();
|
public SortedList< string, object? > ChangedItems { get; } = new();
|
||||||
public string LowerChangedItemsString { get; private set; } = string.Empty;
|
public string LowerChangedItemsString { get; private set; } = string.Empty;
|
||||||
|
|
||||||
private void ComputeChangedItems()
|
internal void ComputeChangedItems()
|
||||||
{
|
{
|
||||||
ChangedItems.Clear();
|
ChangedItems.Clear();
|
||||||
foreach( var gamePath in AllRedirects )
|
foreach( var gamePath in AllRedirects )
|
||||||
|
|
|
||||||
|
|
@ -18,15 +18,15 @@ public partial class Mod
|
||||||
public IReadOnlyList< IModGroup > Groups
|
public IReadOnlyList< IModGroup > Groups
|
||||||
=> _groups;
|
=> _groups;
|
||||||
|
|
||||||
private readonly SubMod _default;
|
internal readonly SubMod _default;
|
||||||
private readonly List< IModGroup > _groups = new();
|
internal readonly List< IModGroup > _groups = new();
|
||||||
|
|
||||||
public int TotalFileCount { get; private set; }
|
public int TotalFileCount { get; internal set; }
|
||||||
public int TotalSwapCount { get; private set; }
|
public int TotalSwapCount { get; internal set; }
|
||||||
public int TotalManipulations { get; private set; }
|
public int TotalManipulations { get; internal set; }
|
||||||
public bool HasOptions { get; private set; }
|
public bool HasOptions { get; internal set; }
|
||||||
|
|
||||||
private bool SetCounts()
|
internal bool SetCounts()
|
||||||
{
|
{
|
||||||
TotalFileCount = 0;
|
TotalFileCount = 0;
|
||||||
TotalSwapCount = 0;
|
TotalSwapCount = 0;
|
||||||
|
|
@ -120,7 +120,7 @@ public partial class Mod
|
||||||
|
|
||||||
// Delete all existing group files and save them anew.
|
// Delete all existing group files and save them anew.
|
||||||
// Used when indices change in complex ways.
|
// Used when indices change in complex ways.
|
||||||
private void SaveAllGroups()
|
internal void SaveAllGroups()
|
||||||
{
|
{
|
||||||
foreach( var file in GroupFiles )
|
foreach( var file in GroupFiles )
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,12 @@ namespace Penumbra.Mods;
|
||||||
|
|
||||||
public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
|
public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
|
||||||
{
|
{
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly CommunicatorService _communicator;
|
||||||
private readonly FilenameService _files;
|
private readonly FilenameService _files;
|
||||||
|
|
||||||
// Create a new ModFileSystem from the currently loaded mods and the current sort order file.
|
// Create a new ModFileSystem from the currently loaded mods and the current sort order file.
|
||||||
public ModFileSystem(Mod.Manager modManager, CommunicatorService communicator, FilenameService files)
|
public ModFileSystem(ModManager modManager, CommunicatorService communicator, FilenameService files)
|
||||||
{
|
{
|
||||||
_modManager = modManager;
|
_modManager = modManager;
|
||||||
_communicator = communicator;
|
_communicator = communicator;
|
||||||
|
|
|
||||||
|
|
@ -15,103 +15,105 @@ namespace Penumbra.Mods;
|
||||||
|
|
||||||
public partial class Mod
|
public partial class Mod
|
||||||
{
|
{
|
||||||
// Groups that allow all available options to be selected at once.
|
|
||||||
private sealed class MultiModGroup : IModGroup
|
}
|
||||||
|
|
||||||
|
/// <summary> Groups that allow all available options to be selected at once. </summary>
|
||||||
|
public sealed class MultiModGroup : IModGroup
|
||||||
|
{
|
||||||
|
public GroupType Type
|
||||||
|
=> GroupType.Multi;
|
||||||
|
|
||||||
|
public string Name { get; set; } = "Group";
|
||||||
|
public string Description { get; set; } = "A non-exclusive group of settings.";
|
||||||
|
public int Priority { get; set; }
|
||||||
|
public uint DefaultSettings { get; set; }
|
||||||
|
|
||||||
|
public int OptionPriority(Index idx)
|
||||||
|
=> PrioritizedOptions[idx].Priority;
|
||||||
|
|
||||||
|
public ISubMod this[Index idx]
|
||||||
|
=> PrioritizedOptions[idx].Mod;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public int Count
|
||||||
|
=> PrioritizedOptions.Count;
|
||||||
|
|
||||||
|
public readonly List<(SubMod Mod, int Priority)> PrioritizedOptions = new();
|
||||||
|
|
||||||
|
public IEnumerator<ISubMod> GetEnumerator()
|
||||||
|
=> PrioritizedOptions.Select(o => o.Mod).GetEnumerator();
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
=> GetEnumerator();
|
||||||
|
|
||||||
|
public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx)
|
||||||
{
|
{
|
||||||
public GroupType Type
|
var ret = new MultiModGroup()
|
||||||
=> GroupType.Multi;
|
|
||||||
|
|
||||||
public string Name { get; set; } = "Group";
|
|
||||||
public string Description { get; set; } = "A non-exclusive group of settings.";
|
|
||||||
public int Priority { get; set; }
|
|
||||||
public uint DefaultSettings { get; set; }
|
|
||||||
|
|
||||||
public int OptionPriority(Index idx)
|
|
||||||
=> PrioritizedOptions[idx].Priority;
|
|
||||||
|
|
||||||
public ISubMod this[Index idx]
|
|
||||||
=> PrioritizedOptions[idx].Mod;
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public int Count
|
|
||||||
=> PrioritizedOptions.Count;
|
|
||||||
|
|
||||||
public readonly List<(SubMod Mod, int Priority)> PrioritizedOptions = new();
|
|
||||||
|
|
||||||
public IEnumerator<ISubMod> GetEnumerator()
|
|
||||||
=> PrioritizedOptions.Select(o => o.Mod).GetEnumerator();
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
=> GetEnumerator();
|
|
||||||
|
|
||||||
public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx)
|
|
||||||
{
|
{
|
||||||
var ret = new MultiModGroup()
|
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
|
||||||
|
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
|
||||||
|
Priority = json[nameof(Priority)]?.ToObject<int>() ?? 0,
|
||||||
|
DefaultSettings = json[nameof(DefaultSettings)]?.ToObject<uint>() ?? 0,
|
||||||
|
};
|
||||||
|
if (ret.Name.Length == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var options = json["Options"];
|
||||||
|
if (options != null)
|
||||||
|
foreach (var child in options.Children())
|
||||||
{
|
{
|
||||||
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
|
if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions)
|
||||||
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
|
|
||||||
Priority = json[nameof(Priority)]?.ToObject<int>() ?? 0,
|
|
||||||
DefaultSettings = json[nameof(DefaultSettings)]?.ToObject<uint>() ?? 0,
|
|
||||||
};
|
|
||||||
if (ret.Name.Length == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var options = json["Options"];
|
|
||||||
if (options != null)
|
|
||||||
foreach (var child in options.Children())
|
|
||||||
{
|
{
|
||||||
if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions)
|
Penumbra.ChatService.NotificationMessage(
|
||||||
{
|
$"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning",
|
||||||
Penumbra.ChatService.NotificationMessage(
|
NotificationType.Warning);
|
||||||
$"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning",
|
break;
|
||||||
NotificationType.Warning);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var subMod = new SubMod(mod);
|
|
||||||
subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count);
|
|
||||||
subMod.Load(mod.ModPath, child, out var priority);
|
|
||||||
ret.PrioritizedOptions.Add((subMod, priority));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1));
|
var subMod = new SubMod(mod);
|
||||||
|
subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count);
|
||||||
return ret;
|
subMod.Load(mod.ModPath, child, out var priority);
|
||||||
}
|
ret.PrioritizedOptions.Add((subMod, priority));
|
||||||
|
|
||||||
public IModGroup Convert(GroupType type)
|
|
||||||
{
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case GroupType.Multi: return this;
|
|
||||||
case GroupType.Single:
|
|
||||||
var multi = new SingleModGroup()
|
|
||||||
{
|
|
||||||
Name = Name,
|
|
||||||
Description = Description,
|
|
||||||
Priority = Priority,
|
|
||||||
DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0),
|
|
||||||
};
|
|
||||||
multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod));
|
|
||||||
return multi;
|
|
||||||
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public bool MoveOption(int optionIdxFrom, int optionIdxTo)
|
ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1));
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IModGroup Convert(GroupType type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
{
|
{
|
||||||
if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo))
|
case GroupType.Multi: return this;
|
||||||
return false;
|
case GroupType.Single:
|
||||||
|
var multi = new SingleModGroup()
|
||||||
DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo);
|
{
|
||||||
UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo));
|
Name = Name,
|
||||||
return true;
|
Description = Description,
|
||||||
}
|
Priority = Priority,
|
||||||
|
DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0),
|
||||||
public void UpdatePositions(int from = 0)
|
};
|
||||||
{
|
multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod));
|
||||||
foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from))
|
return multi;
|
||||||
o.SetPosition(o.GroupIdx, i);
|
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool MoveOption(int optionIdxFrom, int optionIdxTo)
|
||||||
|
{
|
||||||
|
if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo);
|
||||||
|
UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdatePositions(int from = 0)
|
||||||
|
{
|
||||||
|
foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from))
|
||||||
|
o.SetPosition(o.GroupIdx, i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,122 +10,119 @@ using Penumbra.Api.Enums;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public partial class Mod
|
/// <summary> Groups that allow only one of their available options to be selected. </summary>
|
||||||
|
public sealed class SingleModGroup : IModGroup
|
||||||
{
|
{
|
||||||
// Groups that allow only one of their available options to be selected.
|
public GroupType Type
|
||||||
private sealed class SingleModGroup : IModGroup
|
=> GroupType.Single;
|
||||||
|
|
||||||
|
public string Name { get; set; } = "Option";
|
||||||
|
public string Description { get; set; } = "A mutually exclusive group of settings.";
|
||||||
|
public int Priority { get; set; }
|
||||||
|
public uint DefaultSettings { get; set; }
|
||||||
|
|
||||||
|
public readonly List< SubMod > OptionData = new();
|
||||||
|
|
||||||
|
public int OptionPriority( Index _ )
|
||||||
|
=> Priority;
|
||||||
|
|
||||||
|
public ISubMod this[ Index idx ]
|
||||||
|
=> OptionData[ idx ];
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public int Count
|
||||||
|
=> OptionData.Count;
|
||||||
|
|
||||||
|
public IEnumerator< ISubMod > GetEnumerator()
|
||||||
|
=> OptionData.GetEnumerator();
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
=> GetEnumerator();
|
||||||
|
|
||||||
|
public static SingleModGroup? Load( Mod mod, JObject json, int groupIdx )
|
||||||
{
|
{
|
||||||
public GroupType Type
|
var options = json[ "Options" ];
|
||||||
=> GroupType.Single;
|
var ret = new SingleModGroup
|
||||||
|
|
||||||
public string Name { get; set; } = "Option";
|
|
||||||
public string Description { get; set; } = "A mutually exclusive group of settings.";
|
|
||||||
public int Priority { get; set; }
|
|
||||||
public uint DefaultSettings { get; set; }
|
|
||||||
|
|
||||||
public readonly List< SubMod > OptionData = new();
|
|
||||||
|
|
||||||
public int OptionPriority( Index _ )
|
|
||||||
=> Priority;
|
|
||||||
|
|
||||||
public ISubMod this[ Index idx ]
|
|
||||||
=> OptionData[ idx ];
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public int Count
|
|
||||||
=> OptionData.Count;
|
|
||||||
|
|
||||||
public IEnumerator< ISubMod > GetEnumerator()
|
|
||||||
=> OptionData.GetEnumerator();
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
=> GetEnumerator();
|
|
||||||
|
|
||||||
public static SingleModGroup? Load( Mod mod, JObject json, int groupIdx )
|
|
||||||
{
|
{
|
||||||
var options = json[ "Options" ];
|
Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty,
|
||||||
var ret = new SingleModGroup
|
Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty,
|
||||||
{
|
Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0,
|
||||||
Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty,
|
DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0u,
|
||||||
Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty,
|
};
|
||||||
Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0,
|
if( ret.Name.Length == 0 )
|
||||||
DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0u,
|
{
|
||||||
};
|
return null;
|
||||||
if( ret.Name.Length == 0 )
|
}
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( options != null )
|
if( options != null )
|
||||||
|
{
|
||||||
|
foreach( var child in options.Children() )
|
||||||
{
|
{
|
||||||
foreach( var child in options.Children() )
|
var subMod = new SubMod( mod );
|
||||||
|
subMod.SetPosition( groupIdx, ret.OptionData.Count );
|
||||||
|
subMod.Load( mod.ModPath, child, out _ );
|
||||||
|
ret.OptionData.Add( subMod );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( ( int )ret.DefaultSettings >= ret.Count )
|
||||||
|
ret.DefaultSettings = 0;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IModGroup Convert( GroupType type )
|
||||||
|
{
|
||||||
|
switch( type )
|
||||||
|
{
|
||||||
|
case GroupType.Single: return this;
|
||||||
|
case GroupType.Multi:
|
||||||
|
var multi = new MultiModGroup()
|
||||||
{
|
{
|
||||||
var subMod = new SubMod( mod );
|
Name = Name,
|
||||||
subMod.SetPosition( groupIdx, ret.OptionData.Count );
|
Description = Description,
|
||||||
subMod.Load( mod.ModPath, child, out _ );
|
Priority = Priority,
|
||||||
ret.OptionData.Add( subMod );
|
DefaultSettings = 1u << ( int )DefaultSettings,
|
||||||
}
|
};
|
||||||
}
|
multi.PrioritizedOptions.AddRange( OptionData.Select( ( o, i ) => ( o, i ) ) );
|
||||||
|
return multi;
|
||||||
|
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if( ( int )ret.DefaultSettings >= ret.Count )
|
public bool MoveOption( int optionIdxFrom, int optionIdxTo )
|
||||||
ret.DefaultSettings = 0;
|
{
|
||||||
|
if( !OptionData.Move( optionIdxFrom, optionIdxTo ) )
|
||||||
return ret;
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IModGroup Convert( GroupType type )
|
// Update default settings with the move.
|
||||||
|
if( DefaultSettings == optionIdxFrom )
|
||||||
{
|
{
|
||||||
switch( type )
|
DefaultSettings = ( uint )optionIdxTo;
|
||||||
|
}
|
||||||
|
else if( optionIdxFrom < optionIdxTo )
|
||||||
|
{
|
||||||
|
if( DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo )
|
||||||
{
|
{
|
||||||
case GroupType.Single: return this;
|
--DefaultSettings;
|
||||||
case GroupType.Multi:
|
|
||||||
var multi = new MultiModGroup()
|
|
||||||
{
|
|
||||||
Name = Name,
|
|
||||||
Description = Description,
|
|
||||||
Priority = Priority,
|
|
||||||
DefaultSettings = 1u << ( int )DefaultSettings,
|
|
||||||
};
|
|
||||||
multi.PrioritizedOptions.AddRange( OptionData.Select( ( o, i ) => ( o, i ) ) );
|
|
||||||
return multi;
|
|
||||||
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if( DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo )
|
||||||
public bool MoveOption( int optionIdxFrom, int optionIdxTo )
|
|
||||||
{
|
{
|
||||||
if( !OptionData.Move( optionIdxFrom, optionIdxTo ) )
|
++DefaultSettings;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update default settings with the move.
|
|
||||||
if( DefaultSettings == optionIdxFrom )
|
|
||||||
{
|
|
||||||
DefaultSettings = ( uint )optionIdxTo;
|
|
||||||
}
|
|
||||||
else if( optionIdxFrom < optionIdxTo )
|
|
||||||
{
|
|
||||||
if( DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo )
|
|
||||||
{
|
|
||||||
--DefaultSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if( DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo )
|
|
||||||
{
|
|
||||||
++DefaultSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) );
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdatePositions( int from = 0 )
|
UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdatePositions( int from = 0 )
|
||||||
|
{
|
||||||
|
foreach( var (o, i) in OptionData.WithIndex().Skip( from ) )
|
||||||
{
|
{
|
||||||
foreach( var (o, i) in OptionData.WithIndex().Skip( from ) )
|
o.SetPosition( o.GroupIdx, i );
|
||||||
{
|
|
||||||
o.SetPosition( o.GroupIdx, i );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ public partial class Mod
|
||||||
ISubMod.WriteSubMod( j, serializer, _default, ModPath, 0 );
|
ISubMod.WriteSubMod( j, serializer, _default, ModPath, 0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveDefaultModDelayed()
|
internal void SaveDefaultModDelayed()
|
||||||
=> Penumbra.Framework.RegisterDelayed( nameof( SaveDefaultMod ) + ModPath.Name, SaveDefaultMod );
|
=> Penumbra.Framework.RegisterDelayed( nameof( SaveDefaultMod ) + ModPath.Name, SaveDefaultMod );
|
||||||
|
|
||||||
private void LoadDefaultOption()
|
private void LoadDefaultOption()
|
||||||
|
|
@ -92,233 +92,237 @@ public partial class Mod
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// A sub mod is a collection of
|
|
||||||
// - file replacements
|
}
|
||||||
// - file swaps
|
|
||||||
// - meta manipulations
|
/// <summary>
|
||||||
// that can be used either as an option or as the default data for a mod.
|
/// A sub mod is a collection of
|
||||||
// It can be loaded and reloaded from Json.
|
/// - file replacements
|
||||||
// Nothing is checked for existence or validity when loading.
|
/// - file swaps
|
||||||
// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides.
|
/// - meta manipulations
|
||||||
public sealed class SubMod : ISubMod
|
/// that can be used either as an option or as the default data for a mod.
|
||||||
|
/// It can be loaded and reloaded from Json.
|
||||||
|
/// Nothing is checked for existence or validity when loading.
|
||||||
|
/// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SubMod : ISubMod
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "Default";
|
||||||
|
|
||||||
|
public string FullName
|
||||||
|
=> GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}";
|
||||||
|
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
internal IMod ParentMod { get; private init; }
|
||||||
|
internal int GroupIdx { get; private set; }
|
||||||
|
internal int OptionIdx { get; private set; }
|
||||||
|
|
||||||
|
public bool IsDefault
|
||||||
|
=> GroupIdx < 0;
|
||||||
|
|
||||||
|
public Dictionary< Utf8GamePath, FullPath > FileData = new();
|
||||||
|
public Dictionary< Utf8GamePath, FullPath > FileSwapData = new();
|
||||||
|
public HashSet< MetaManipulation > ManipulationData = new();
|
||||||
|
|
||||||
|
public SubMod( IMod parentMod )
|
||||||
|
=> ParentMod = parentMod;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary< Utf8GamePath, FullPath > Files
|
||||||
|
=> FileData;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps
|
||||||
|
=> FileSwapData;
|
||||||
|
|
||||||
|
public IReadOnlySet< MetaManipulation > Manipulations
|
||||||
|
=> ManipulationData;
|
||||||
|
|
||||||
|
public void SetPosition( int groupIdx, int optionIdx )
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = "Default";
|
GroupIdx = groupIdx;
|
||||||
|
OptionIdx = optionIdx;
|
||||||
|
}
|
||||||
|
|
||||||
public string FullName
|
public void Load( DirectoryInfo basePath, JToken json, out int priority )
|
||||||
=> GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}";
|
{
|
||||||
|
FileData.Clear();
|
||||||
|
FileSwapData.Clear();
|
||||||
|
ManipulationData.Clear();
|
||||||
|
|
||||||
public string Description { get; set; } = string.Empty;
|
// Every option has a name, but priorities are only relevant for multi group options.
|
||||||
|
Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty;
|
||||||
|
Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty;
|
||||||
|
priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0;
|
||||||
|
|
||||||
internal IMod ParentMod { get; private init; }
|
var files = ( JObject? )json[ nameof( Files ) ];
|
||||||
internal int GroupIdx { get; private set; }
|
if( files != null )
|
||||||
internal int OptionIdx { get; private set; }
|
|
||||||
|
|
||||||
public bool IsDefault
|
|
||||||
=> GroupIdx < 0;
|
|
||||||
|
|
||||||
public Dictionary< Utf8GamePath, FullPath > FileData = new();
|
|
||||||
public Dictionary< Utf8GamePath, FullPath > FileSwapData = new();
|
|
||||||
public HashSet< MetaManipulation > ManipulationData = new();
|
|
||||||
|
|
||||||
public SubMod( IMod parentMod )
|
|
||||||
=> ParentMod = parentMod;
|
|
||||||
|
|
||||||
public IReadOnlyDictionary< Utf8GamePath, FullPath > Files
|
|
||||||
=> FileData;
|
|
||||||
|
|
||||||
public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps
|
|
||||||
=> FileSwapData;
|
|
||||||
|
|
||||||
public IReadOnlySet< MetaManipulation > Manipulations
|
|
||||||
=> ManipulationData;
|
|
||||||
|
|
||||||
public void SetPosition( int groupIdx, int optionIdx )
|
|
||||||
{
|
{
|
||||||
GroupIdx = groupIdx;
|
foreach( var property in files.Properties() )
|
||||||
OptionIdx = optionIdx;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Load( DirectoryInfo basePath, JToken json, out int priority )
|
|
||||||
{
|
|
||||||
FileData.Clear();
|
|
||||||
FileSwapData.Clear();
|
|
||||||
ManipulationData.Clear();
|
|
||||||
|
|
||||||
// Every option has a name, but priorities are only relevant for multi group options.
|
|
||||||
Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty;
|
|
||||||
Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty;
|
|
||||||
priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0;
|
|
||||||
|
|
||||||
var files = ( JObject? )json[ nameof( Files ) ];
|
|
||||||
if( files != null )
|
|
||||||
{
|
{
|
||||||
foreach( var property in files.Properties() )
|
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
|
||||||
{
|
{
|
||||||
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
|
FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) );
|
||||||
{
|
|
||||||
FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var swaps = ( JObject? )json[ nameof( FileSwaps ) ];
|
|
||||||
if( swaps != null )
|
|
||||||
{
|
|
||||||
foreach( var property in swaps.Properties() )
|
|
||||||
{
|
|
||||||
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
|
|
||||||
{
|
|
||||||
FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var manips = json[ nameof( Manipulations ) ];
|
|
||||||
if( manips != null )
|
|
||||||
{
|
|
||||||
foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) )
|
|
||||||
{
|
|
||||||
ManipulationData.Add( s );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod.
|
var swaps = ( JObject? )json[ nameof( FileSwaps ) ];
|
||||||
// If delete is true, the files are deleted afterwards.
|
if( swaps != null )
|
||||||
public (bool Changes, List< string > DeleteList) IncorporateMetaChanges( DirectoryInfo basePath, bool delete )
|
|
||||||
{
|
{
|
||||||
var deleteList = new List< string >();
|
foreach( var property in swaps.Properties() )
|
||||||
var oldSize = ManipulationData.Count;
|
|
||||||
var deleteString = delete ? "with deletion." : "without deletion.";
|
|
||||||
foreach( var (key, file) in Files.ToList() )
|
|
||||||
{
|
{
|
||||||
var ext1 = key.Extension().AsciiToLower().ToString();
|
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
|
||||||
var ext2 = file.Extension.ToLowerInvariant();
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if( ext1 == ".meta" || ext2 == ".meta" )
|
FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) );
|
||||||
{
|
|
||||||
FileData.Remove( key );
|
|
||||||
if( !file.Exists )
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
|
|
||||||
Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" );
|
|
||||||
deleteList.Add( file.FullName );
|
|
||||||
ManipulationData.UnionWith( meta.MetaManipulations );
|
|
||||||
}
|
|
||||||
else if( ext1 == ".rgsp" || ext2 == ".rgsp" )
|
|
||||||
{
|
|
||||||
FileData.Remove( key );
|
|
||||||
if( !file.Exists )
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
|
|
||||||
Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" );
|
|
||||||
deleteList.Add( file.FullName );
|
|
||||||
|
|
||||||
ManipulationData.UnionWith( rgsp.MetaManipulations );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch( Exception e )
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteDeleteList( deleteList, delete );
|
|
||||||
return ( oldSize < ManipulationData.Count, deleteList );
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void DeleteDeleteList( IEnumerable< string > deleteList, bool delete )
|
|
||||||
{
|
|
||||||
if( !delete )
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach( var file in deleteList )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Delete( file );
|
|
||||||
}
|
|
||||||
catch( Exception e )
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false )
|
var manips = json[ nameof( Manipulations ) ];
|
||||||
|
if( manips != null )
|
||||||
{
|
{
|
||||||
var files = TexToolsMeta.ConvertToTexTools( Manipulations );
|
foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) )
|
||||||
|
|
||||||
foreach( var (file, data) in files )
|
|
||||||
{
|
{
|
||||||
var path = Path.Combine( basePath.FullName, file );
|
ManipulationData.Add( s );
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory( Path.GetDirectoryName( path )! );
|
|
||||||
File.WriteAllBytes( path, data );
|
|
||||||
}
|
|
||||||
catch( Exception e )
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( test )
|
|
||||||
{
|
|
||||||
TestMetaWriting( files );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Conditional( "DEBUG" )]
|
|
||||||
private void TestMetaWriting( Dictionary< string, byte[] > files )
|
|
||||||
{
|
|
||||||
var meta = new HashSet< MetaManipulation >( Manipulations.Count );
|
|
||||||
foreach( var (file, data) in files )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var x = file.EndsWith( "rgsp" )
|
|
||||||
? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges )
|
|
||||||
: new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges );
|
|
||||||
meta.UnionWith( x.MetaManipulations );
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( !Manipulations.SetEquals( meta ) )
|
|
||||||
{
|
|
||||||
Penumbra.Log.Information( "Meta Sets do not equal." );
|
|
||||||
foreach( var (m1, m2) in Manipulations.Zip( meta ) )
|
|
||||||
{
|
|
||||||
Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" );
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach( var m in Manipulations.Skip( meta.Count ) )
|
|
||||||
{
|
|
||||||
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach( var m in meta.Skip( Manipulations.Count ) )
|
|
||||||
{
|
|
||||||
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Penumbra.Log.Information( "Meta Sets are equal." );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod.
|
||||||
|
// If delete is true, the files are deleted afterwards.
|
||||||
|
public (bool Changes, List< string > DeleteList) IncorporateMetaChanges( DirectoryInfo basePath, bool delete )
|
||||||
|
{
|
||||||
|
var deleteList = new List< string >();
|
||||||
|
var oldSize = ManipulationData.Count;
|
||||||
|
var deleteString = delete ? "with deletion." : "without deletion.";
|
||||||
|
foreach( var (key, file) in Files.ToList() )
|
||||||
|
{
|
||||||
|
var ext1 = key.Extension().AsciiToLower().ToString();
|
||||||
|
var ext2 = file.Extension.ToLowerInvariant();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if( ext1 == ".meta" || ext2 == ".meta" )
|
||||||
|
{
|
||||||
|
FileData.Remove( key );
|
||||||
|
if( !file.Exists )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
|
||||||
|
Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" );
|
||||||
|
deleteList.Add( file.FullName );
|
||||||
|
ManipulationData.UnionWith( meta.MetaManipulations );
|
||||||
|
}
|
||||||
|
else if( ext1 == ".rgsp" || ext2 == ".rgsp" )
|
||||||
|
{
|
||||||
|
FileData.Remove( key );
|
||||||
|
if( !file.Exists )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
|
||||||
|
Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" );
|
||||||
|
deleteList.Add( file.FullName );
|
||||||
|
|
||||||
|
ManipulationData.UnionWith( rgsp.MetaManipulations );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteDeleteList( deleteList, delete );
|
||||||
|
return ( oldSize < ManipulationData.Count, deleteList );
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void DeleteDeleteList( IEnumerable< string > deleteList, bool delete )
|
||||||
|
{
|
||||||
|
if( !delete )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach( var file in deleteList )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete( file );
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false )
|
||||||
|
{
|
||||||
|
var files = TexToolsMeta.ConvertToTexTools( Manipulations );
|
||||||
|
|
||||||
|
foreach( var (file, data) in files )
|
||||||
|
{
|
||||||
|
var path = Path.Combine( basePath.FullName, file );
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory( Path.GetDirectoryName( path )! );
|
||||||
|
File.WriteAllBytes( path, data );
|
||||||
|
}
|
||||||
|
catch( Exception e )
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( test )
|
||||||
|
{
|
||||||
|
TestMetaWriting( files );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG" )]
|
||||||
|
private void TestMetaWriting( Dictionary< string, byte[] > files )
|
||||||
|
{
|
||||||
|
var meta = new HashSet< MetaManipulation >( Manipulations.Count );
|
||||||
|
foreach( var (file, data) in files )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var x = file.EndsWith( "rgsp" )
|
||||||
|
? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges )
|
||||||
|
: new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges );
|
||||||
|
meta.UnionWith( x.MetaManipulations );
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !Manipulations.SetEquals( meta ) )
|
||||||
|
{
|
||||||
|
Penumbra.Log.Information( "Meta Sets do not equal." );
|
||||||
|
foreach( var (m1, m2) in Manipulations.Zip( meta ) )
|
||||||
|
{
|
||||||
|
Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" );
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach( var m in Manipulations.Skip( meta.Count ) )
|
||||||
|
{
|
||||||
|
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach( var m in meta.Skip( Manipulations.Count ) )
|
||||||
|
{
|
||||||
|
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Penumbra.Log.Information( "Meta Sets are equal." );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ using Penumbra.String.Classes;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
// Contains the settings for a given mod.
|
/// <summary> Contains the settings for a given mod. </summary>
|
||||||
public class ModSettings
|
public class ModSettings
|
||||||
{
|
{
|
||||||
public static readonly ModSettings Empty = new();
|
public static readonly ModSettings Empty = new();
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,10 @@ public class TemporaryMod : IMod
|
||||||
public IEnumerable< ISubMod > AllSubMods
|
public IEnumerable< ISubMod > AllSubMods
|
||||||
=> new[] { Default };
|
=> new[] { Default };
|
||||||
|
|
||||||
private readonly Mod.SubMod _default;
|
private readonly SubMod _default;
|
||||||
|
|
||||||
public TemporaryMod()
|
public TemporaryMod()
|
||||||
=> _default = new Mod.SubMod( this );
|
=> _default = new SubMod( this );
|
||||||
|
|
||||||
public void SetFile( Utf8GamePath gamePath, FullPath fullPath )
|
public void SetFile( Utf8GamePath gamePath, FullPath fullPath )
|
||||||
=> _default.FileData[ gamePath ] = fullPath;
|
=> _default.FileData[ gamePath ] = fullPath;
|
||||||
|
|
@ -44,7 +44,7 @@ public class TemporaryMod : IMod
|
||||||
_default.ManipulationData = manips;
|
_default.ManipulationData = manips;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SaveTempCollection( Mod.Manager modManager, ModCollection collection, string? character = null )
|
public static void SaveTempCollection( ModManager modManager, ModCollection collection, string? character = null )
|
||||||
{
|
{
|
||||||
DirectoryInfo? dir = null;
|
DirectoryInfo? dir = null;
|
||||||
try
|
try
|
||||||
|
|
@ -54,7 +54,7 @@ public class TemporaryMod : IMod
|
||||||
modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor,
|
modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor,
|
||||||
$"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null );
|
$"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null );
|
||||||
var mod = new Mod( dir );
|
var mod = new Mod( dir );
|
||||||
var defaultMod = (Mod.SubMod) mod.Default;
|
var defaultMod = (SubMod) mod.Default;
|
||||||
foreach( var (gamePath, fullPath) in collection.ResolvedFiles )
|
foreach( var (gamePath, fullPath) in collection.ResolvedFiles )
|
||||||
{
|
{
|
||||||
if( gamePath.Path.EndsWith( ".imc"u8 ) )
|
if( gamePath.Path.EndsWith( ".imc"u8 ) )
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,8 @@ public class Penumbra : IDalamudPlugin
|
||||||
public static CharacterUtility CharacterUtility { get; private set; } = null!;
|
public static CharacterUtility CharacterUtility { get; private set; } = null!;
|
||||||
public static GameEventManager GameEvents { get; private set; } = null!;
|
public static GameEventManager GameEvents { get; private set; } = null!;
|
||||||
public static MetaFileManager MetaFileManager { get; private set; } = null!;
|
public static MetaFileManager MetaFileManager { get; private set; } = null!;
|
||||||
public static Mod.Manager ModManager { get; private set; } = null!;
|
public static ModManager ModManager { get; private set; } = null!;
|
||||||
public static ModCollection.Manager CollectionManager { get; private set; } = null!;
|
public static CollectionManager CollectionManager { get; private set; } = null!;
|
||||||
public static TempCollectionManager TempCollections { get; private set; } = null!;
|
public static TempCollectionManager TempCollections { get; private set; } = null!;
|
||||||
public static TempModManager TempMods { get; private set; } = null!;
|
public static TempModManager TempMods { get; private set; } = null!;
|
||||||
public static ResourceLoader ResourceLoader { get; private set; } = null!;
|
public static ResourceLoader ResourceLoader { get; private set; } = null!;
|
||||||
|
|
@ -96,8 +96,8 @@ public class Penumbra : IDalamudPlugin
|
||||||
TempMods = _tmp.Services.GetRequiredService<TempModManager>();
|
TempMods = _tmp.Services.GetRequiredService<TempModManager>();
|
||||||
ResidentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>();
|
ResidentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>();
|
||||||
_tmp.Services.GetRequiredService<ResourceManagerService>();
|
_tmp.Services.GetRequiredService<ResourceManagerService>();
|
||||||
ModManager = _tmp.Services.GetRequiredService<Mod.Manager>();
|
ModManager = _tmp.Services.GetRequiredService<ModManager>();
|
||||||
CollectionManager = _tmp.Services.GetRequiredService<ModCollection.Manager>();
|
CollectionManager = _tmp.Services.GetRequiredService<CollectionManager>();
|
||||||
TempCollections = _tmp.Services.GetRequiredService<TempCollectionManager>();
|
TempCollections = _tmp.Services.GetRequiredService<TempCollectionManager>();
|
||||||
ModFileSystem = _tmp.Services.GetRequiredService<ModFileSystem>();
|
ModFileSystem = _tmp.Services.GetRequiredService<ModFileSystem>();
|
||||||
RedrawService = _tmp.Services.GetRequiredService<RedrawService>();
|
RedrawService = _tmp.Services.GetRequiredService<RedrawService>();
|
||||||
|
|
|
||||||
|
|
@ -88,12 +88,12 @@ public class PenumbraNew
|
||||||
// Add Collection Services
|
// Add Collection Services
|
||||||
services.AddTransient<IndividualCollections>()
|
services.AddTransient<IndividualCollections>()
|
||||||
.AddSingleton<TempCollectionManager>()
|
.AddSingleton<TempCollectionManager>()
|
||||||
.AddSingleton<ModCollection.Manager>();
|
.AddSingleton<CollectionManager>();
|
||||||
|
|
||||||
// Add Mod Services
|
// Add Mod Services
|
||||||
services.AddSingleton<TempModManager>()
|
services.AddSingleton<TempModManager>()
|
||||||
.AddSingleton<ModDataEditor>()
|
.AddSingleton<ModDataEditor>()
|
||||||
.AddSingleton<Mod.Manager>()
|
.AddSingleton<ModManager>()
|
||||||
.AddSingleton<ModFileSystem>();
|
.AddSingleton<ModFileSystem>();
|
||||||
|
|
||||||
// Add Resource services
|
// Add Resource services
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ public class ConfigMigrationService
|
||||||
if (_config.Version != 6)
|
if (_config.Version != 6)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ModCollection.Manager.MigrateUngenderedCollections(_fileNames);
|
CollectionManager.MigrateUngenderedCollections(_fileNames);
|
||||||
_config.Version = 7;
|
_config.Version = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,7 +113,7 @@ public class ConfigMigrationService
|
||||||
if (_config.Version != 4)
|
if (_config.Version != 4)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Mod.Manager.MigrateModBackups = true;
|
ModManager.MigrateModBackups = true;
|
||||||
_config.Version = 5;
|
_config.Version = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,11 +257,11 @@ public class ConfigMigrationService
|
||||||
using var j = new JsonTextWriter(writer);
|
using var j = new JsonTextWriter(writer);
|
||||||
j.Formatting = Formatting.Indented;
|
j.Formatting = Formatting.Indented;
|
||||||
j.WriteStartObject();
|
j.WriteStartObject();
|
||||||
j.WritePropertyName(nameof(ModCollection.Manager.Default));
|
j.WritePropertyName(nameof(CollectionManager.Default));
|
||||||
j.WriteValue(def);
|
j.WriteValue(def);
|
||||||
j.WritePropertyName(nameof(ModCollection.Manager.Interface));
|
j.WritePropertyName(nameof(CollectionManager.Interface));
|
||||||
j.WriteValue(ui);
|
j.WriteValue(ui);
|
||||||
j.WritePropertyName(nameof(ModCollection.Manager.Current));
|
j.WritePropertyName(nameof(CollectionManager.Current));
|
||||||
j.WriteValue(current);
|
j.WriteValue(current);
|
||||||
foreach (var (type, collection) in special)
|
foreach (var (type, collection) in special)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,12 @@ public class ItemSwapTab : IDisposable, ITab
|
||||||
{
|
{
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly CommunicatorService _communicator;
|
||||||
private readonly ItemService _itemService;
|
private readonly ItemService _itemService;
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
|
|
||||||
public ItemSwapTab(CommunicatorService communicator, ItemService itemService, ModCollection.Manager collectionManager,
|
public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager,
|
||||||
Mod.Manager modManager, Configuration config)
|
ModManager modManager, Configuration config)
|
||||||
{
|
{
|
||||||
_communicator = communicator;
|
_communicator = communicator;
|
||||||
_itemService = itemService;
|
_itemService = itemService;
|
||||||
|
|
|
||||||
|
|
@ -297,7 +297,7 @@ public partial class ModEditWindow
|
||||||
var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made.";
|
var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made.";
|
||||||
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes))
|
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes))
|
||||||
{
|
{
|
||||||
var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod)_editor.Option!);
|
var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!);
|
||||||
if (failedFiles > 0)
|
if (failedFiles > 0)
|
||||||
Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.FullName}.");
|
Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.FullName}.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ public partial class ModEditWindow
|
||||||
_editor.FileEditor.Revert(_editor.Mod!, _editor.Option!);
|
_editor.FileEditor.Revert(_editor.Mod!, _editor.Option!);
|
||||||
var fileRegistry = _editor.Files.Available.First(file => file.File.FullName == _targetPath);
|
var fileRegistry = _editor.Files.Available.First(file => file.File.FullName == _targetPath);
|
||||||
_editor.FileEditor.AddPathsToSelected(_editor.Option!, new []{ fileRegistry }, _subDirs);
|
_editor.FileEditor.AddPathsToSelected(_editor.Option!, new []{ fileRegistry }, _subDirs);
|
||||||
_editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod) _editor.Option!);
|
_editor.FileEditor.Apply(_editor.Mod!, (SubMod) _editor.Option!);
|
||||||
|
|
||||||
return fileRegistry;
|
return fileRegistry;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ namespace Penumbra.UI.CollectionTab;
|
||||||
|
|
||||||
public sealed class CollectionSelector : FilterComboCache<ModCollection>
|
public sealed class CollectionSelector : FilterComboCache<ModCollection>
|
||||||
{
|
{
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
|
|
||||||
public CollectionSelector(ModCollection.Manager manager, Func<IReadOnlyList<ModCollection>> items)
|
public CollectionSelector(CollectionManager manager, Func<IReadOnlyList<ModCollection>> items)
|
||||||
: base(items)
|
: base(items)
|
||||||
=> _collectionManager = manager;
|
=> _collectionManager = manager;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,10 @@ namespace Penumbra.UI.CollectionTab;
|
||||||
public class IndividualCollectionUi
|
public class IndividualCollectionUi
|
||||||
{
|
{
|
||||||
private readonly ActorService _actorService;
|
private readonly ActorService _actorService;
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
private readonly CollectionSelector _withEmpty;
|
private readonly CollectionSelector _withEmpty;
|
||||||
|
|
||||||
public IndividualCollectionUi(ActorService actors, ModCollection.Manager collectionManager, CollectionSelector withEmpty)
|
public IndividualCollectionUi(ActorService actors, CollectionManager collectionManager, CollectionSelector withEmpty)
|
||||||
{
|
{
|
||||||
_actorService = actors;
|
_actorService = actors;
|
||||||
_collectionManager = collectionManager;
|
_collectionManager = collectionManager;
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@ public class InheritanceUi
|
||||||
private const int InheritedCollectionHeight = 9;
|
private const int InheritedCollectionHeight = 9;
|
||||||
private const string InheritanceDragDropLabel = "##InheritanceMove";
|
private const string InheritanceDragDropLabel = "##InheritanceMove";
|
||||||
|
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
|
|
||||||
public InheritanceUi(ModCollection.Manager collectionManager)
|
public InheritanceUi(CollectionManager collectionManager)
|
||||||
=> _collectionManager = collectionManager;
|
=> _collectionManager = collectionManager;
|
||||||
|
|
||||||
/// <summary> Draw the whole inheritance block. </summary>
|
/// <summary> Draw the whole inheritance block. </summary>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ namespace Penumbra.UI.CollectionTab;
|
||||||
|
|
||||||
public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, string)>
|
public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, string)>
|
||||||
{
|
{
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
|
|
||||||
public (CollectionType, string, string)? CurrentType
|
public (CollectionType, string, string)? CurrentType
|
||||||
=> CollectionTypeExtensions.Special[CurrentIdx];
|
=> CollectionTypeExtensions.Special[CurrentIdx];
|
||||||
|
|
@ -16,7 +16,7 @@ public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, stri
|
||||||
private readonly float _unscaledWidth;
|
private readonly float _unscaledWidth;
|
||||||
private readonly string _label;
|
private readonly string _label;
|
||||||
|
|
||||||
public SpecialCombo(ModCollection.Manager collectionManager, string label, float unscaledWidth)
|
public SpecialCombo(CollectionManager collectionManager, string label, float unscaledWidth)
|
||||||
: base(CollectionTypeExtensions.Special, false)
|
: base(CollectionTypeExtensions.Special, false)
|
||||||
{
|
{
|
||||||
_collectionManager = collectionManager;
|
_collectionManager = collectionManager;
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ namespace Penumbra.UI;
|
||||||
|
|
||||||
public class FileDialogService : IDisposable
|
public class FileDialogService : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Mod.Manager _mods;
|
private readonly ModManager _mods;
|
||||||
private readonly FileDialogManager _manager;
|
private readonly FileDialogManager _manager;
|
||||||
private readonly ConcurrentDictionary<string, string> _startPaths = new();
|
private readonly ConcurrentDictionary<string, string> _startPaths = new();
|
||||||
private bool _isOpen;
|
private bool _isOpen;
|
||||||
|
|
||||||
public FileDialogService(Mod.Manager mods, Configuration config)
|
public FileDialogService(ModManager mods, Configuration config)
|
||||||
{
|
{
|
||||||
_mods = mods;
|
_mods = mods;
|
||||||
_manager = SetupFileManager(config.ModDirectory);
|
_manager = SetupFileManager(config.ModDirectory);
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
|
||||||
private readonly ChatService _chat;
|
private readonly ChatService _chat;
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly FileDialogService _fileDialog;
|
private readonly FileDialogService _fileDialog;
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
private readonly TutorialService _tutorial;
|
private readonly TutorialService _tutorial;
|
||||||
private readonly ModEditor _modEditor;
|
private readonly ModEditor _modEditor;
|
||||||
|
|
||||||
|
|
@ -38,8 +38,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
|
||||||
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
|
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
|
||||||
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
|
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
|
||||||
|
|
||||||
public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem, Mod.Manager modManager,
|
public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem, ModManager modManager,
|
||||||
ModCollection.Manager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, ChatService chat,
|
CollectionManager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, ChatService chat,
|
||||||
ModEditor modEditor)
|
ModEditor modEditor)
|
||||||
: base(fileSystem, DalamudServices.KeyState, HandleException)
|
: base(fileSystem, DalamudServices.KeyState, HandleException)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ namespace Penumbra.UI.ModsTab;
|
||||||
public class ModPanelConflictsTab : ITab
|
public class ModPanelConflictsTab : ITab
|
||||||
{
|
{
|
||||||
private readonly ModFileSystemSelector _selector;
|
private readonly ModFileSystemSelector _selector;
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
|
|
||||||
public ModPanelConflictsTab(ModCollection.Manager collectionManager, ModFileSystemSelector selector)
|
public ModPanelConflictsTab(CollectionManager collectionManager, ModFileSystemSelector selector)
|
||||||
{
|
{
|
||||||
_collectionManager = collectionManager;
|
_collectionManager = collectionManager;
|
||||||
_selector = selector;
|
_selector = selector;
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,11 @@ public class ModPanelDescriptionTab : ITab
|
||||||
{
|
{
|
||||||
private readonly ModFileSystemSelector _selector;
|
private readonly ModFileSystemSelector _selector;
|
||||||
private readonly TutorialService _tutorial;
|
private readonly TutorialService _tutorial;
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly TagButtons _localTags = new();
|
private readonly TagButtons _localTags = new();
|
||||||
private readonly TagButtons _modTags = new();
|
private readonly TagButtons _modTags = new();
|
||||||
|
|
||||||
public ModPanelDescriptionTab(ModFileSystemSelector selector, TutorialService tutorial, Mod.Manager modManager)
|
public ModPanelDescriptionTab(ModFileSystemSelector selector, TutorialService tutorial, ModManager modManager)
|
||||||
{
|
{
|
||||||
_selector = selector;
|
_selector = selector;
|
||||||
_tutorial = tutorial;
|
_tutorial = tutorial;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ namespace Penumbra.UI.ModsTab;
|
||||||
public class ModPanelEditTab : ITab
|
public class ModPanelEditTab : ITab
|
||||||
{
|
{
|
||||||
private readonly ChatService _chat;
|
private readonly ChatService _chat;
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly ModFileSystem _fileSystem;
|
private readonly ModFileSystem _fileSystem;
|
||||||
private readonly ModFileSystemSelector _selector;
|
private readonly ModFileSystemSelector _selector;
|
||||||
private readonly ModEditWindow _editWindow;
|
private readonly ModEditWindow _editWindow;
|
||||||
|
|
@ -33,7 +33,7 @@ public class ModPanelEditTab : ITab
|
||||||
private ModFileSystem.Leaf _leaf = null!;
|
private ModFileSystem.Leaf _leaf = null!;
|
||||||
private Mod _mod = null!;
|
private Mod _mod = null!;
|
||||||
|
|
||||||
public ModPanelEditTab(Mod.Manager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat,
|
public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat,
|
||||||
ModEditWindow editWindow, ModEditor editor)
|
ModEditWindow editWindow, ModEditor editor)
|
||||||
{
|
{
|
||||||
_modManager = modManager;
|
_modManager = modManager;
|
||||||
|
|
@ -219,7 +219,7 @@ public class ModPanelEditTab : ITab
|
||||||
public static void Reset()
|
public static void Reset()
|
||||||
=> _newGroupName = string.Empty;
|
=> _newGroupName = string.Empty;
|
||||||
|
|
||||||
public static void Draw(Mod.Manager modManager, Mod mod)
|
public static void Draw(ModManager modManager, Mod mod)
|
||||||
{
|
{
|
||||||
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3));
|
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3));
|
||||||
ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3);
|
ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3);
|
||||||
|
|
@ -250,15 +250,15 @@ public class ModPanelEditTab : ITab
|
||||||
private static class MoveDirectory
|
private static class MoveDirectory
|
||||||
{
|
{
|
||||||
private static string? _currentModDirectory;
|
private static string? _currentModDirectory;
|
||||||
private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical;
|
private static ModManager.NewDirectoryState _state = ModManager.NewDirectoryState.Identical;
|
||||||
|
|
||||||
public static void Reset()
|
public static void Reset()
|
||||||
{
|
{
|
||||||
_currentModDirectory = null;
|
_currentModDirectory = null;
|
||||||
_state = Mod.Manager.NewDirectoryState.Identical;
|
_state = ModManager.NewDirectoryState.Identical;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Draw(Mod.Manager modManager, Mod mod, Vector2 buttonSize)
|
public static void Draw(ModManager modManager, Mod mod, Vector2 buttonSize)
|
||||||
{
|
{
|
||||||
ImGui.SetNextItemWidth(buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X);
|
ImGui.SetNextItemWidth(buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X);
|
||||||
var tmp = _currentModDirectory ?? mod.ModPath.Name;
|
var tmp = _currentModDirectory ?? mod.ModPath.Name;
|
||||||
|
|
@ -270,13 +270,13 @@ public class ModPanelEditTab : ITab
|
||||||
|
|
||||||
var (disabled, tt) = _state switch
|
var (disabled, tt) = _state switch
|
||||||
{
|
{
|
||||||
Mod.Manager.NewDirectoryState.Identical => (true, "Current directory name is identical to new one."),
|
ModManager.NewDirectoryState.Identical => (true, "Current directory name is identical to new one."),
|
||||||
Mod.Manager.NewDirectoryState.Empty => (true, "Please enter a new directory name first."),
|
ModManager.NewDirectoryState.Empty => (true, "Please enter a new directory name first."),
|
||||||
Mod.Manager.NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."),
|
ModManager.NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."),
|
||||||
Mod.Manager.NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."),
|
ModManager.NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."),
|
||||||
Mod.Manager.NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."),
|
ModManager.NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."),
|
||||||
Mod.Manager.NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."),
|
ModManager.NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."),
|
||||||
Mod.Manager.NewDirectoryState.ContainsInvalidSymbols => (true,
|
ModManager.NewDirectoryState.ContainsInvalidSymbols => (true,
|
||||||
$"{_currentModDirectory} contains invalid symbols for FFXIV."),
|
$"{_currentModDirectory} contains invalid symbols for FFXIV."),
|
||||||
_ => (true, "Unknown error."),
|
_ => (true, "Unknown error."),
|
||||||
};
|
};
|
||||||
|
|
@ -317,7 +317,7 @@ public class ModPanelEditTab : ITab
|
||||||
ImGui.OpenPopup(PopupName);
|
ImGui.OpenPopup(PopupName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void DrawPopup(Mod.Manager modManager)
|
public static void DrawPopup(ModManager modManager)
|
||||||
{
|
{
|
||||||
if (_mod == null)
|
if (_mod == null)
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ namespace Penumbra.UI.ModsTab;
|
||||||
public class ModPanelSettingsTab : ITab
|
public class ModPanelSettingsTab : ITab
|
||||||
{
|
{
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
private readonly ModFileSystemSelector _selector;
|
private readonly ModFileSystemSelector _selector;
|
||||||
private readonly TutorialService _tutorial;
|
private readonly TutorialService _tutorial;
|
||||||
private readonly PenumbraApi _api;
|
private readonly PenumbraApi _api;
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
|
|
||||||
private bool _inherited;
|
private bool _inherited;
|
||||||
private ModSettings _settings = null!;
|
private ModSettings _settings = null!;
|
||||||
|
|
@ -30,7 +30,7 @@ public class ModPanelSettingsTab : ITab
|
||||||
private bool _empty;
|
private bool _empty;
|
||||||
private int? _currentPriority = null;
|
private int? _currentPriority = null;
|
||||||
|
|
||||||
public ModPanelSettingsTab(ModCollection.Manager collectionManager, Mod.Manager modManager, ModFileSystemSelector selector,
|
public ModPanelSettingsTab(CollectionManager collectionManager, ModManager modManager, ModFileSystemSelector selector,
|
||||||
TutorialService tutorial, PenumbraApi api, Configuration config)
|
TutorialService tutorial, PenumbraApi api, Configuration config)
|
||||||
{
|
{
|
||||||
_collectionManager = collectionManager;
|
_collectionManager = collectionManager;
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ public class ModPanelTabBar
|
||||||
public readonly ModPanelChangedItemsTab ChangedItems;
|
public readonly ModPanelChangedItemsTab ChangedItems;
|
||||||
public readonly ModPanelEditTab Edit;
|
public readonly ModPanelEditTab Edit;
|
||||||
private readonly ModEditWindow _modEditWindow;
|
private readonly ModEditWindow _modEditWindow;
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly TutorialService _tutorial;
|
private readonly TutorialService _tutorial;
|
||||||
|
|
||||||
public readonly ITab[] Tabs;
|
public readonly ITab[] Tabs;
|
||||||
|
|
@ -35,7 +35,7 @@ public class ModPanelTabBar
|
||||||
private Mod? _lastMod = null;
|
private Mod? _lastMod = null;
|
||||||
|
|
||||||
public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description,
|
public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description,
|
||||||
ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, Mod.Manager modManager,
|
ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, ModManager modManager,
|
||||||
TutorialService tutorial)
|
TutorialService tutorial)
|
||||||
{
|
{
|
||||||
_modEditWindow = modEditWindow;
|
_modEditWindow = modEditWindow;
|
||||||
|
|
@ -107,7 +107,7 @@ public class ModPanelTabBar
|
||||||
if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip))
|
if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip))
|
||||||
{
|
{
|
||||||
_modEditWindow.ChangeMod(mod);
|
_modEditWindow.ChangeMod(mod);
|
||||||
_modEditWindow.ChangeOption((Mod.SubMod) mod.Default);
|
_modEditWindow.ChangeOption((SubMod) mod.Default);
|
||||||
_modEditWindow.IsOpen = true;
|
_modEditWindow.IsOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,10 @@ namespace Penumbra.UI.Tabs;
|
||||||
|
|
||||||
public class ChangedItemsTab : ITab
|
public class ChangedItemsTab : ITab
|
||||||
{
|
{
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
private readonly PenumbraApi _api;
|
private readonly PenumbraApi _api;
|
||||||
|
|
||||||
public ChangedItemsTab(ModCollection.Manager collectionManager, PenumbraApi api)
|
public ChangedItemsTab(CollectionManager collectionManager, PenumbraApi api)
|
||||||
{
|
{
|
||||||
_collectionManager = collectionManager;
|
_collectionManager = collectionManager;
|
||||||
_api = api;
|
_api = api;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ public class CollectionsTab : IDisposable, ITab
|
||||||
{
|
{
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly CommunicatorService _communicator;
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
private readonly TutorialService _tutorial;
|
private readonly TutorialService _tutorial;
|
||||||
private readonly SpecialCombo _specialCollectionCombo;
|
private readonly SpecialCombo _specialCollectionCombo;
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ public class CollectionsTab : IDisposable, ITab
|
||||||
private readonly InheritanceUi _inheritance;
|
private readonly InheritanceUi _inheritance;
|
||||||
private readonly IndividualCollectionUi _individualCollections;
|
private readonly IndividualCollectionUi _individualCollections;
|
||||||
|
|
||||||
public CollectionsTab(ActorService actorService, CommunicatorService communicator, ModCollection.Manager collectionManager,
|
public CollectionsTab(ActorService actorService, CommunicatorService communicator, CollectionManager collectionManager,
|
||||||
TutorialService tutorial, Configuration config)
|
TutorialService tutorial, Configuration config)
|
||||||
{
|
{
|
||||||
_communicator = communicator;
|
_communicator = communicator;
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,8 @@ public class DebugTab : ITab
|
||||||
private readonly StartTracker _timer;
|
private readonly StartTracker _timer;
|
||||||
private readonly PerformanceTracker _performance;
|
private readonly PerformanceTracker _performance;
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly ValidityChecker _validityChecker;
|
private readonly ValidityChecker _validityChecker;
|
||||||
private readonly HttpApi _httpApi;
|
private readonly HttpApi _httpApi;
|
||||||
private readonly ActorService _actorService;
|
private readonly ActorService _actorService;
|
||||||
|
|
@ -52,8 +52,8 @@ public class DebugTab : ITab
|
||||||
private readonly IdentifiedCollectionCache _identifiedCollectionCache;
|
private readonly IdentifiedCollectionCache _identifiedCollectionCache;
|
||||||
private readonly CutsceneService _cutsceneService;
|
private readonly CutsceneService _cutsceneService;
|
||||||
|
|
||||||
public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager,
|
public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager,
|
||||||
ValidityChecker validityChecker, Mod.Manager modManager, HttpApi httpApi, ActorService actorService,
|
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService,
|
||||||
DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources,
|
DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources,
|
||||||
ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver,
|
ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver,
|
||||||
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
|
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ namespace Penumbra.UI.Tabs;
|
||||||
|
|
||||||
public class EffectiveTab : ITab
|
public class EffectiveTab : ITab
|
||||||
{
|
{
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
|
|
||||||
public EffectiveTab(ModCollection.Manager collectionManager)
|
public EffectiveTab(CollectionManager collectionManager)
|
||||||
=> _collectionManager = collectionManager;
|
=> _collectionManager = collectionManager;
|
||||||
|
|
||||||
public ReadOnlySpan<byte> Label
|
public ReadOnlySpan<byte> Label
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,13 @@ public class ModsTab : ITab
|
||||||
private readonly ModFileSystemSelector _selector;
|
private readonly ModFileSystemSelector _selector;
|
||||||
private readonly ModPanel _panel;
|
private readonly ModPanel _panel;
|
||||||
private readonly TutorialService _tutorial;
|
private readonly TutorialService _tutorial;
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly ModCollection.Manager _collectionManager;
|
private readonly CollectionManager _collectionManager;
|
||||||
private readonly RedrawService _redrawService;
|
private readonly RedrawService _redrawService;
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly CollectionsTab _collectionsTab;
|
private readonly CollectionsTab _collectionsTab;
|
||||||
|
|
||||||
public ModsTab(Mod.Manager modManager, ModCollection.Manager collectionManager, ModFileSystemSelector selector, ModPanel panel,
|
public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel,
|
||||||
TutorialService tutorial, RedrawService redrawService, Configuration config, CollectionsTab collectionsTab)
|
TutorialService tutorial, RedrawService redrawService, Configuration config, CollectionsTab collectionsTab)
|
||||||
{
|
{
|
||||||
_modManager = modManager;
|
_modManager = modManager;
|
||||||
|
|
|
||||||
|
|
@ -31,14 +31,14 @@ public class SettingsTab : ITab
|
||||||
private readonly TutorialService _tutorial;
|
private readonly TutorialService _tutorial;
|
||||||
private readonly Penumbra _penumbra;
|
private readonly Penumbra _penumbra;
|
||||||
private readonly FileDialogService _fileDialog;
|
private readonly FileDialogService _fileDialog;
|
||||||
private readonly Mod.Manager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly ModFileSystemSelector _selector;
|
private readonly ModFileSystemSelector _selector;
|
||||||
private readonly CharacterUtility _characterUtility;
|
private readonly CharacterUtility _characterUtility;
|
||||||
private readonly ResidentResourceManager _residentResources;
|
private readonly ResidentResourceManager _residentResources;
|
||||||
private readonly DalamudServices _dalamud;
|
private readonly DalamudServices _dalamud;
|
||||||
|
|
||||||
public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra,
|
public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra,
|
||||||
FileDialogService fileDialog, Mod.Manager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility,
|
FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility,
|
||||||
ResidentResourceManager residentResources, DalamudServices dalamud)
|
ResidentResourceManager residentResources, DalamudServices dalamud)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue