Now that's a collection manager.

This commit is contained in:
Ottermandias 2023-04-06 15:47:33 +02:00
parent 5a817db069
commit f85fc46fb7
55 changed files with 2433 additions and 2317 deletions

@ -1 +1 @@
Subproject commit d87dfa44ff6efcf4fe576d8a877c78f4ac0dc893
Subproject commit 6c6533ac60ee6e5e401bb9a65b31ad843d1757cd

View file

@ -12,14 +12,14 @@ using System.Numerics;
using Dalamud.Utility;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Collections;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
using Penumbra.Collections.Manager;
namespace Penumbra.Api;
public class IpcTester : IDisposable
@ -1213,7 +1213,7 @@ public class IpcTester : IDisposable
DrawIntro(Ipc.CreateTemporaryCollection.Label, "Copy Existing Collection");
if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero,
"Copies the effective list from the collection named in Temporary Mod Name...",
!Penumbra.CollectionManager.ByName(_tempModName, out var copyCollection))
!Penumbra.CollectionManager.Storage.ByName(_tempModName, out var copyCollection))
&& copyCollection is { HasCache: true })
{
var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());

View file

@ -22,6 +22,7 @@ using Penumbra.Mods.Manager;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.Services;
using Penumbra.Collections.Manager;
namespace Penumbra.Api;
@ -59,7 +60,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
return;
CheckInitialized();
_communicator.CreatingCharacterBase.Event += new Action<nint, string, nint, nint, nint>(value);
_communicator.CreatingCharacterBase.Subscribe(new Action<nint, string, nint, nint, nint>(value));
}
remove
{
@ -67,7 +68,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
return;
CheckInitialized();
_communicator.CreatingCharacterBase.Event -= new Action<nint, string, nint, nint, nint>(value);
_communicator.CreatingCharacterBase.Unsubscribe(new Action<nint, string, nint, nint, nint>(value));
}
}
@ -79,7 +80,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
return;
CheckInitialized();
_communicator.CreatedCharacterBase.Event += new Action<nint, string, nint>(value);
_communicator.CreatedCharacterBase.Subscribe(new Action<nint, string, nint>(value));
}
remove
{
@ -87,7 +88,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
return;
CheckInitialized();
_communicator.CreatedCharacterBase.Event -= new Action<nint, string, nint>(value);
_communicator.CreatedCharacterBase.Unsubscribe(new Action<nint, string, nint>(value));
}
}
@ -129,12 +130,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi
_lumina = (Lumina.GameData?)_dalamud.GameData.GetType()
.GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic)
?.GetValue(_dalamud.GameData);
foreach (var collection in _collectionManager)
foreach (var collection in _collectionManager.Storage)
SubscribeToCollection(collection);
_communicator.CollectionChange.Event += SubscribeToNewCollections;
_resourceLoader.ResourceLoaded += OnResourceLoaded;
_communicator.ModPathChanged.Event += ModPathChangeSubscriber;
_communicator.CollectionChange.Subscribe(SubscribeToNewCollections);
_resourceLoader.ResourceLoaded += OnResourceLoaded;
_communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber);
}
public unsafe void Dispose()
@ -142,28 +143,28 @@ public class PenumbraApi : IDisposable, IPenumbraApi
if (!Valid)
return;
foreach (var collection in _collectionManager)
foreach (var collection in _collectionManager.Storage)
{
if (_delegates.TryGetValue(collection, out var del))
collection.ModSettingChanged -= del;
}
_resourceLoader.ResourceLoaded -= OnResourceLoaded;
_communicator.CollectionChange.Event -= SubscribeToNewCollections;
_communicator.ModPathChanged.Event -= ModPathChangeSubscriber;
_lumina = null;
_communicator = null!;
_penumbra = null!;
_modManager = null!;
_resourceLoader = null!;
_config = null!;
_collectionManager = null!;
_dalamud = null!;
_tempCollections = null!;
_tempMods = null!;
_actors = null!;
_collectionResolver = null!;
_cutsceneService = null!;
_resourceLoader.ResourceLoaded -= OnResourceLoaded;
_communicator.CollectionChange.Unsubscribe(SubscribeToNewCollections);
_communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber);
_lumina = null;
_communicator = null!;
_penumbra = null!;
_modManager = null!;
_resourceLoader = null!;
_config = null!;
_collectionManager = null!;
_dalamud = null!;
_tempCollections = null!;
_tempMods = null!;
_actors = null!;
_collectionResolver = null!;
_cutsceneService = null!;
}
public event ChangedItemClick? ChangedItemClicked;
@ -187,12 +188,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi
add
{
CheckInitialized();
_communicator.ModDirectoryChanged.Event += value;
_communicator.ModDirectoryChanged.Subscribe(value!);
}
remove
{
CheckInitialized();
_communicator.ModDirectoryChanged.Event -= value;
_communicator.ModDirectoryChanged.Unsubscribe(value!);
}
}
@ -283,13 +284,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public string ResolveDefaultPath(string path)
{
CheckInitialized();
return ResolvePath(path, _modManager, _collectionManager.Default);
return ResolvePath(path, _modManager, _collectionManager.Active.Default);
}
public string ResolveInterfacePath(string path)
{
CheckInitialized();
return ResolvePath(path, _modManager, _collectionManager.Interface);
return ResolvePath(path, _modManager, _collectionManager.Active.Interface);
}
public string ResolvePlayerPath(string path)
@ -313,7 +314,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{
CheckInitialized();
return ResolvePath(path, _modManager,
_collectionManager.Individual(NameToIdentifier(characterName, worldId)));
_collectionManager.Active.Individual(NameToIdentifier(characterName, worldId)));
}
// TODO: cleanup when incrementing API level
@ -329,7 +330,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
path,
};
var ret = _collectionManager.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path));
var ret = _collectionManager.Active.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path));
return ret.Select(r => r.ToString()).ToArray();
}
@ -386,7 +387,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
CheckInitialized();
try
{
if (!_collectionManager.ByName(collectionName, out var collection))
if (!_collectionManager.Storage.ByName(collectionName, out var collection))
collection = ModCollection.Empty;
if (collection.HasCache)
@ -408,7 +409,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
if (!Enum.IsDefined(type))
return string.Empty;
var collection = _collectionManager.ByType((CollectionType)type);
var collection = _collectionManager.Active.ByType((CollectionType)type);
return collection?.Name ?? string.Empty;
}
@ -419,7 +420,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
if (!Enum.IsDefined(type))
return (PenumbraApiEc.InvalidArgument, string.Empty);
var oldCollection = _collectionManager.ByType((CollectionType)type)?.Name ?? string.Empty;
var oldCollection = _collectionManager.Active.ByType((CollectionType)type)?.Name ?? string.Empty;
if (collectionName.Length == 0)
{
@ -429,11 +430,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi
if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface)
return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection);
_collectionManager.RemoveSpecialCollection((CollectionType)type);
_collectionManager.Active.RemoveSpecialCollection((CollectionType)type);
return (PenumbraApiEc.Success, oldCollection);
}
if (!_collectionManager.ByName(collectionName, out var collection))
if (!_collectionManager.Storage.ByName(collectionName, out var collection))
return (PenumbraApiEc.CollectionMissing, oldCollection);
if (oldCollection.Length == 0)
@ -441,14 +442,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi
if (!allowCreateNew)
return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection);
_collectionManager.CreateSpecialCollection((CollectionType)type);
_collectionManager.Active.CreateSpecialCollection((CollectionType)type);
}
else if (oldCollection == collection.Name)
{
return (PenumbraApiEc.NothingChanged, oldCollection);
}
_collectionManager.SetCollection(collection, (CollectionType)type);
_collectionManager.Active.SetCollection(collection, (CollectionType)type);
return (PenumbraApiEc.Success, oldCollection);
}
@ -457,9 +458,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi
CheckInitialized();
var id = AssociatedIdentifier(gameObjectIdx);
if (!id.IsValid)
return (false, false, _collectionManager.Default.Name);
return (false, false, _collectionManager.Active.Default.Name);
if (_collectionManager.Individuals.TryGetValue(id, out var collection))
if (_collectionManager.Active.Individuals.TryGetValue(id, out var collection))
return (true, true, collection.Name);
AssociatedCollection(gameObjectIdx, out collection);
@ -472,9 +473,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi
CheckInitialized();
var id = AssociatedIdentifier(gameObjectIdx);
if (!id.IsValid)
return (PenumbraApiEc.InvalidIdentifier, _collectionManager.Default.Name);
return (PenumbraApiEc.InvalidIdentifier, _collectionManager.Active.Default.Name);
var oldCollection = _collectionManager.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty;
var oldCollection = _collectionManager.Active.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty;
if (collectionName.Length == 0)
{
@ -484,12 +485,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi
if (!allowDelete)
return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection);
var idx = _collectionManager.Individuals.Index(id);
_collectionManager.RemoveIndividualCollection(idx);
var idx = _collectionManager.Active.Individuals.Index(id);
_collectionManager.Active.RemoveIndividualCollection(idx);
return (PenumbraApiEc.Success, oldCollection);
}
if (!_collectionManager.ByName(collectionName, out var collection))
if (!_collectionManager.Storage.ByName(collectionName, out var collection))
return (PenumbraApiEc.CollectionMissing, oldCollection);
if (oldCollection.Length == 0)
@ -497,40 +498,40 @@ public class PenumbraApi : IDisposable, IPenumbraApi
if (!allowCreateNew)
return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection);
var ids = _collectionManager.Individuals.GetGroup(id);
_collectionManager.CreateIndividualCollection(ids);
var ids = _collectionManager.Active.Individuals.GetGroup(id);
_collectionManager.Active.CreateIndividualCollection(ids);
}
else if (oldCollection == collection.Name)
{
return (PenumbraApiEc.NothingChanged, oldCollection);
}
_collectionManager.SetCollection(collection, CollectionType.Individual, _collectionManager.Individuals.Index(id));
_collectionManager.Active.SetCollection(collection, CollectionType.Individual, _collectionManager.Active.Individuals.Index(id));
return (PenumbraApiEc.Success, oldCollection);
}
public IList<string> GetCollections()
{
CheckInitialized();
return _collectionManager.Select(c => c.Name).ToArray();
return _collectionManager.Storage.Select(c => c.Name).ToArray();
}
public string GetCurrentCollection()
{
CheckInitialized();
return _collectionManager.Current.Name;
return _collectionManager.Active.Current.Name;
}
public string GetDefaultCollection()
{
CheckInitialized();
return _collectionManager.Default.Name;
return _collectionManager.Active.Default.Name;
}
public string GetInterfaceCollection()
{
CheckInitialized();
return _collectionManager.Interface.Name;
return _collectionManager.Active.Interface.Name;
}
// TODO: cleanup when incrementing API level
@ -540,9 +541,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public (string, bool) GetCharacterCollection(string characterName, ushort worldId)
{
CheckInitialized();
return _collectionManager.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection)
return _collectionManager.Active.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection)
? (collection.Name, true)
: (_collectionManager.Default.Name, false);
: (_collectionManager.Active.Default.Name, false);
}
public unsafe (nint, string) GetDrawObjectInfo(nint drawObject)
@ -576,7 +577,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
string modDirectory, string modName, bool allowInheritance)
{
CheckInitialized();
if (!_collectionManager.ByName(collectionName, out var collection))
if (!_collectionManager.Storage.ByName(collectionName, out var collection))
return (PenumbraApiEc.CollectionMissing, null);
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
@ -679,7 +680,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public PenumbraApiEc TryInheritMod(string collectionName, string modDirectory, string modName, bool inherit)
{
CheckInitialized();
if (!_collectionManager.ByName(collectionName, out var collection))
if (!_collectionManager.Storage.ByName(collectionName, out var collection))
return PenumbraApiEc.CollectionMissing;
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
@ -692,7 +693,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public PenumbraApiEc TrySetMod(string collectionName, string modDirectory, string modName, bool enabled)
{
CheckInitialized();
if (!_collectionManager.ByName(collectionName, out var collection))
if (!_collectionManager.Storage.ByName(collectionName, out var collection))
return PenumbraApiEc.CollectionMissing;
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
@ -704,7 +705,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public PenumbraApiEc TrySetModPriority(string collectionName, string modDirectory, string modName, int priority)
{
CheckInitialized();
if (!_collectionManager.ByName(collectionName, out var collection))
if (!_collectionManager.Storage.ByName(collectionName, out var collection))
return PenumbraApiEc.CollectionMissing;
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
@ -717,7 +718,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
string optionName)
{
CheckInitialized();
if (!_collectionManager.ByName(collectionName, out var collection))
if (!_collectionManager.Storage.ByName(collectionName, out var collection))
return PenumbraApiEc.CollectionMissing;
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
@ -740,7 +741,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
IReadOnlyList<string> optionNames)
{
CheckInitialized();
if (!_collectionManager.ByName(collectionName, out var collection))
if (!_collectionManager.Storage.ByName(collectionName, out var collection))
return PenumbraApiEc.CollectionMissing;
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
@ -788,9 +789,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi
.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase))?.Index
?? -1;
if (string.IsNullOrEmpty(collectionName))
foreach (var collection in _collectionManager)
foreach (var collection in _collectionManager.Storage)
collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo);
else if (_collectionManager.ByName(collectionName, out var collection))
else if (_collectionManager.Storage.ByName(collectionName, out var collection))
collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo);
else
return PenumbraApiEc.CollectionMissing;
@ -809,7 +810,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
if (!identifier.IsValid)
return (PenumbraApiEc.InvalidArgument, string.Empty);
if (!forceOverwriteCharacter && _collectionManager.Individuals.ContainsKey(identifier)
if (!forceOverwriteCharacter && _collectionManager.Active.Individuals.ContainsKey(identifier)
|| _tempCollections.Collections.ContainsKey(identifier))
return (PenumbraApiEc.CharacterCollectionExists, string.Empty);
@ -859,7 +860,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
return PenumbraApiEc.AssignmentDeletionFailed;
}
else if (_tempCollections.Collections.ContainsKey(identifier)
|| _collectionManager.Individuals.ContainsKey(identifier))
|| _collectionManager.Active.Individuals.ContainsKey(identifier))
{
return PenumbraApiEc.CharacterCollectionExists;
}
@ -907,7 +908,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{
CheckInitialized();
if (!_tempCollections.CollectionByName(collectionName, out var collection)
&& !_collectionManager.ByName(collectionName, out collection))
&& !_collectionManager.Storage.ByName(collectionName, out collection))
return PenumbraApiEc.CollectionMissing;
if (!ConvertPaths(paths, out var p))
@ -938,7 +939,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{
CheckInitialized();
if (!_tempCollections.CollectionByName(collectionName, out var collection)
&& !_collectionManager.ByName(collectionName, out collection))
&& !_collectionManager.Storage.ByName(collectionName, out collection))
return PenumbraApiEc.CollectionMissing;
return _tempMods.Unregister(tag, collection, priority) switch
@ -967,7 +968,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
var identifier = NameToIdentifier(characterName, worldId);
var collection = _tempCollections.Collections.TryGetCollection(identifier, out var c)
? c
: _collectionManager.Individual(identifier);
: _collectionManager.Active.Individual(identifier);
var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty<MetaManipulation>();
return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
}
@ -1002,7 +1003,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection)
{
collection = _collectionManager.Default;
collection = _collectionManager.Active.Default;
if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length)
return false;

View file

@ -5,7 +5,8 @@ using Penumbra.Mods;
using System.Collections.Generic;
using Penumbra.Services;
using Penumbra.String.Classes;
using Penumbra.Collections.Manager;
namespace Penumbra.Api;
public enum RedirectResult
@ -26,12 +27,12 @@ public class TempModManager : IDisposable
public TempModManager(CommunicatorService communicator)
{
_communicator = communicator;
_communicator.CollectionChange.Event += OnCollectionChange;
_communicator.CollectionChange.Subscribe(OnCollectionChange);
}
public void Dispose()
{
_communicator.CollectionChange.Event -= OnCollectionChange;
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
}
public IReadOnlyDictionary<ModCollection, List<TemporaryMod>> Mods

View file

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

View file

@ -1,480 +0,0 @@
using OtterGui;
using OtterGui.Filesystem;
using Penumbra.Mods;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Penumbra.Api;
using Penumbra.GameData.Actors;
using Penumbra.Interop.Services;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.Util;
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
namespace Penumbra.Collections;
public sealed partial class CollectionManager : IDisposable, IEnumerable<ModCollection>
{
private readonly ModManager _modManager;
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
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()
{
ModCollection.Empty,
};
public ModCollection this[Index idx]
=> _collections[idx];
public ModCollection? this[string name]
=> ByName(name, out var c) ? c : null;
public int Count
=> _collections.Count;
// Obtain a collection case-independently by name.
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
=> _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
// Default enumeration skips the empty collection.
public IEnumerator<ModCollection> GetEnumerator()
=> _collections.Skip(1).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public IEnumerable<ModCollection> GetEnumeratorWithEmpty()
=> _collections;
public CollectionManager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility,
ResidentResourceManager residentResources, Configuration config, ModManager modManager, IndividualCollections individuals,
SaveService saveService)
{
using var time = timer.Measure(StartTimeType.Collections);
_communicator = communicator;
_characterUtility = characterUtility;
_residentResources = residentResources;
_config = config;
_modManager = modManager;
_saveService = saveService;
Individuals = individuals;
// The collection manager reacts to changes in mods by itself.
_communicator.ModDiscoveryStarted.Event += OnModDiscoveryStarted;
_communicator.ModDiscoveryFinished.Event += OnModDiscoveryFinished;
_communicator.ModOptionChanged.Event += OnModOptionsChanged;
_communicator.ModPathChanged.Event += 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;
_communicator.ModDiscoveryStarted.Event -= OnModDiscoveryStarted;
_communicator.ModDiscoveryFinished.Event -= OnModDiscoveryFinished;
_communicator.ModOptionChanged.Event -= OnModOptionsChanged;
_communicator.ModPathChanged.Event -= 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))
{
fixedName = string.Empty;
return false;
}
name = name.RemoveInvalidPathSymbols().ToLowerInvariant();
if (name.Length == 0
|| name == ModCollection.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.
// If duplicate is not-null, the new collection will be a duplicate of it.
// If the name of the collection would result in an already existing filename, skip it.
// Returns true if the collection was successfully created and fires a Inactive event.
// Also sets the current collection to the new collection afterwards.
public bool AddCollection(string name, ModCollection? duplicate)
{
if (!CanAddCollection(name, out var fixedName))
{
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) ?? ModCollection.CreateNewEmpty(name);
newCollection.Index = _collections.Count;
_collections.Add(newCollection);
Penumbra.SaveService.ImmediateSave(newCollection);
Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}.");
_communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
SetCollection(newCollection.Index, CollectionType.Current);
return true;
}
// Remove the given collection if it exists and is neither the empty nor the default-named collection.
// If the removed collection was active, it also sets the corresponding collection to the appropriate default.
// Also removes the collection from inheritances of all other collections.
public bool RemoveCollection(int idx)
{
if (idx <= ModCollection.Empty.Index || idx >= _collections.Count)
{
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(ModCollection.Empty.Index, CollectionType.Default);
for (var i = 0; i < _specialCollections.Length; ++i)
{
if (idx == _specialCollections[i]?.Index)
SetCollection(ModCollection.Empty, (CollectionType)i);
}
for (var i = 0; i < Individuals.Count; ++i)
{
if (Individuals[i].Collection.Index == idx)
SetCollection(ModCollection.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)
=> 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)
{
case ModPathChangeType.Added:
foreach (var collection in this)
collection.AddMod(mod);
OnModAddedActive(mod);
break;
case ModPathChangeType.Deleted:
OnModRemovedActive(mod);
foreach (var collection in this)
collection.RemoveMod(mod, mod.Index);
break;
case ModPathChangeType.Moved:
OnModMovedActive(mod);
foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
Penumbra.SaveService.QueueSave(collection);
break;
case ModPathChangeType.StartingReload:
OnModRemovedActive(mod);
break;
case ModPathChangeType.Reloaded:
OnModAddedActive(mod);
break;
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
// 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)
_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);
_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)
_saveService.ImmediateSave(collection);
}
}
// Read all collection files in the Collection Directory.
// Ensure that the default named collection exists, and apply inheritances afterwards.
// Duplicate collection files are not deleted, just not added here.
private void ReadCollections(FilenameService files)
{
var inheritances = new List<IReadOnlyList<string>>();
foreach (var file in files.CollectionFiles)
{
var collection = ModCollection.LoadFromFile(file, out var inheritance);
if (collection == null || collection.Name.Length == 0)
continue;
if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json")
Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}.");
if (this[collection.Name] != null)
{
Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists.");
}
else
{
inheritances.Add(inheritance);
collection.Index = _collections.Count;
_collections.Add(collection);
}
}
AddDefaultCollection();
ApplyInheritances(inheritances);
}
public string RedundancyCheck(CollectionType type, ActorIdentifier id)
{
var checkAssignment = ByType(type, id);
if (checkAssignment == null)
return string.Empty;
switch (type)
{
// Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap.
case CollectionType.Individual:
switch (id.Type)
{
case IdentifierType.Player when id.HomeWorld != ushort.MaxValue:
{
var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue));
return global?.Index == checkAssignment.Index
? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."
: string.Empty;
}
case IdentifierType.Owned:
if (id.HomeWorld != ushort.MaxValue)
{
var global = ByType(CollectionType.Individual,
Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId));
if (global?.Index == checkAssignment.Index)
return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it.";
}
var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId));
return unowned?.Index == checkAssignment.Index
? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it."
: string.Empty;
}
break;
// The group of all Characters is redundant if they are all equal to Default or unassigned.
case CollectionType.MalePlayerCharacter:
case CollectionType.MaleNonPlayerCharacter:
case CollectionType.FemalePlayerCharacter:
case CollectionType.FemaleNonPlayerCharacter:
var first = ByType(CollectionType.MalePlayerCharacter) ?? Default;
var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default;
var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default;
var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default;
if (first.Index == second.Index
&& first.Index == third.Index
&& first.Index == fourth.Index
&& first.Index == Default.Index)
return
"Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n"
+ "You can keep just the Default Assignment.";
break;
// Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default.
case CollectionType.NonPlayerChild:
case CollectionType.NonPlayerElderly:
var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter);
var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter);
var collection1 = CollectionType.MaleNonPlayerCharacter;
var collection2 = CollectionType.FemaleNonPlayerCharacter;
if (maleNpc == null)
{
maleNpc = Default;
if (maleNpc.Index != checkAssignment.Index)
return string.Empty;
collection1 = CollectionType.Default;
}
if (femaleNpc == null)
{
femaleNpc = Default;
if (femaleNpc.Index != checkAssignment.Index)
return string.Empty;
collection2 = CollectionType.Default;
}
return collection1 == collection2
? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them."
: $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them.";
// For other assignments, check the inheritance order, unassigned means fall-through,
// assigned needs identical assignments to be redundant.
default:
var group = type.InheritanceOrder();
foreach (var parentType in group)
{
var assignment = ByType(parentType);
if (assignment == null)
continue;
if (assignment.Index == checkAssignment.Index)
return
$"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it.";
}
break;
}
return string.Empty;
}
}

View file

@ -1,584 +0,0 @@
using Penumbra.GameData.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
namespace Penumbra.Collections;
public enum CollectionType : byte
{
// Special Collections
Yourself = Api.Enums.ApiCollectionType.Yourself,
MalePlayerCharacter = Api.Enums.ApiCollectionType.MalePlayerCharacter,
FemalePlayerCharacter = Api.Enums.ApiCollectionType.FemalePlayerCharacter,
MaleNonPlayerCharacter = Api.Enums.ApiCollectionType.MaleNonPlayerCharacter,
FemaleNonPlayerCharacter = Api.Enums.ApiCollectionType.FemaleNonPlayerCharacter,
NonPlayerChild = Api.Enums.ApiCollectionType.NonPlayerChild,
NonPlayerElderly = Api.Enums.ApiCollectionType.NonPlayerElderly,
MaleMidlander = Api.Enums.ApiCollectionType.MaleMidlander,
FemaleMidlander = Api.Enums.ApiCollectionType.FemaleMidlander,
MaleHighlander = Api.Enums.ApiCollectionType.MaleHighlander,
FemaleHighlander = Api.Enums.ApiCollectionType.FemaleHighlander,
MaleWildwood = Api.Enums.ApiCollectionType.MaleWildwood,
FemaleWildwood = Api.Enums.ApiCollectionType.FemaleWildwood,
MaleDuskwight = Api.Enums.ApiCollectionType.MaleDuskwight,
FemaleDuskwight = Api.Enums.ApiCollectionType.FemaleDuskwight,
MalePlainsfolk = Api.Enums.ApiCollectionType.MalePlainsfolk,
FemalePlainsfolk = Api.Enums.ApiCollectionType.FemalePlainsfolk,
MaleDunesfolk = Api.Enums.ApiCollectionType.MaleDunesfolk,
FemaleDunesfolk = Api.Enums.ApiCollectionType.FemaleDunesfolk,
MaleSeekerOfTheSun = Api.Enums.ApiCollectionType.MaleSeekerOfTheSun,
FemaleSeekerOfTheSun = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSun,
MaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoon,
FemaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoon,
MaleSeawolf = Api.Enums.ApiCollectionType.MaleSeawolf,
FemaleSeawolf = Api.Enums.ApiCollectionType.FemaleSeawolf,
MaleHellsguard = Api.Enums.ApiCollectionType.MaleHellsguard,
FemaleHellsguard = Api.Enums.ApiCollectionType.FemaleHellsguard,
MaleRaen = Api.Enums.ApiCollectionType.MaleRaen,
FemaleRaen = Api.Enums.ApiCollectionType.FemaleRaen,
MaleXaela = Api.Enums.ApiCollectionType.MaleXaela,
FemaleXaela = Api.Enums.ApiCollectionType.FemaleXaela,
MaleHelion = Api.Enums.ApiCollectionType.MaleHelion,
FemaleHelion = Api.Enums.ApiCollectionType.FemaleHelion,
MaleLost = Api.Enums.ApiCollectionType.MaleLost,
FemaleLost = Api.Enums.ApiCollectionType.FemaleLost,
MaleRava = Api.Enums.ApiCollectionType.MaleRava,
FemaleRava = Api.Enums.ApiCollectionType.FemaleRava,
MaleVeena = Api.Enums.ApiCollectionType.MaleVeena,
FemaleVeena = Api.Enums.ApiCollectionType.FemaleVeena,
MaleMidlanderNpc = Api.Enums.ApiCollectionType.MaleMidlanderNpc,
FemaleMidlanderNpc = Api.Enums.ApiCollectionType.FemaleMidlanderNpc,
MaleHighlanderNpc = Api.Enums.ApiCollectionType.MaleHighlanderNpc,
FemaleHighlanderNpc = Api.Enums.ApiCollectionType.FemaleHighlanderNpc,
MaleWildwoodNpc = Api.Enums.ApiCollectionType.MaleWildwoodNpc,
FemaleWildwoodNpc = Api.Enums.ApiCollectionType.FemaleWildwoodNpc,
MaleDuskwightNpc = Api.Enums.ApiCollectionType.MaleDuskwightNpc,
FemaleDuskwightNpc = Api.Enums.ApiCollectionType.FemaleDuskwightNpc,
MalePlainsfolkNpc = Api.Enums.ApiCollectionType.MalePlainsfolkNpc,
FemalePlainsfolkNpc = Api.Enums.ApiCollectionType.FemalePlainsfolkNpc,
MaleDunesfolkNpc = Api.Enums.ApiCollectionType.MaleDunesfolkNpc,
FemaleDunesfolkNpc = Api.Enums.ApiCollectionType.FemaleDunesfolkNpc,
MaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.MaleSeekerOfTheSunNpc,
FemaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSunNpc,
MaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoonNpc,
FemaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoonNpc,
MaleSeawolfNpc = Api.Enums.ApiCollectionType.MaleSeawolfNpc,
FemaleSeawolfNpc = Api.Enums.ApiCollectionType.FemaleSeawolfNpc,
MaleHellsguardNpc = Api.Enums.ApiCollectionType.MaleHellsguardNpc,
FemaleHellsguardNpc = Api.Enums.ApiCollectionType.FemaleHellsguardNpc,
MaleRaenNpc = Api.Enums.ApiCollectionType.MaleRaenNpc,
FemaleRaenNpc = Api.Enums.ApiCollectionType.FemaleRaenNpc,
MaleXaelaNpc = Api.Enums.ApiCollectionType.MaleXaelaNpc,
FemaleXaelaNpc = Api.Enums.ApiCollectionType.FemaleXaelaNpc,
MaleHelionNpc = Api.Enums.ApiCollectionType.MaleHelionNpc,
FemaleHelionNpc = Api.Enums.ApiCollectionType.FemaleHelionNpc,
MaleLostNpc = Api.Enums.ApiCollectionType.MaleLostNpc,
FemaleLostNpc = Api.Enums.ApiCollectionType.FemaleLostNpc,
MaleRavaNpc = Api.Enums.ApiCollectionType.MaleRavaNpc,
FemaleRavaNpc = Api.Enums.ApiCollectionType.FemaleRavaNpc,
MaleVeenaNpc = Api.Enums.ApiCollectionType.MaleVeenaNpc,
FemaleVeenaNpc = Api.Enums.ApiCollectionType.FemaleVeenaNpc,
Default = Api.Enums.ApiCollectionType.Default, // The default collection was changed
Interface = Api.Enums.ApiCollectionType.Interface, // The ui collection was changed
Current = Api.Enums.ApiCollectionType.Current, // The current collection was changed
Individual, // An individual collection was changed
Inactive, // A collection was added or removed
Temporary, // A temporary collections was set or deleted via IPC
}
public static class CollectionTypeExtensions
{
public static bool IsSpecial( this CollectionType collectionType )
=> collectionType < CollectionType.Default;
public static readonly (CollectionType, string, string)[] Special = Enum.GetValues< CollectionType >()
.Where( IsSpecial )
.Select( s => ( s, s.ToName(), s.ToDescription() ) )
.ToArray();
public static CollectionType FromParts( Gender gender, bool npc )
{
gender = gender switch
{
Gender.MaleNpc => Gender.Male,
Gender.FemaleNpc => Gender.Female,
_ => gender,
};
return ( gender, npc ) switch
{
(Gender.Male, false) => CollectionType.MalePlayerCharacter,
(Gender.Female, false) => CollectionType.FemalePlayerCharacter,
(Gender.Male, true) => CollectionType.MaleNonPlayerCharacter,
(Gender.Female, true) => CollectionType.FemaleNonPlayerCharacter,
_ => CollectionType.Inactive,
};
}
// @formatter:off
private static readonly IReadOnlyList<CollectionType> DefaultList = new[] { CollectionType.Default };
private static readonly IReadOnlyList<CollectionType> MalePlayerList = new[] { CollectionType.MalePlayerCharacter, CollectionType.Default };
private static readonly IReadOnlyList<CollectionType> FemalePlayerList = new[] { CollectionType.FemalePlayerCharacter, CollectionType.Default };
private static readonly IReadOnlyList<CollectionType> MaleNpcList = new[] { CollectionType.MaleNonPlayerCharacter, CollectionType.Default };
private static readonly IReadOnlyList<CollectionType> FemaleNpcList = new[] { CollectionType.FemaleNonPlayerCharacter, CollectionType.Default };
// @formatter:on
/// <summary> A list of definite redundancy possibilities. </summary>
public static IReadOnlyList<CollectionType> InheritanceOrder(this CollectionType collectionType)
=> collectionType switch
{
CollectionType.Yourself => DefaultList,
CollectionType.MalePlayerCharacter => DefaultList,
CollectionType.FemalePlayerCharacter => DefaultList,
CollectionType.MaleNonPlayerCharacter => DefaultList,
CollectionType.FemaleNonPlayerCharacter => DefaultList,
CollectionType.MaleMidlander => MalePlayerList,
CollectionType.FemaleMidlander => FemalePlayerList,
CollectionType.MaleHighlander => MalePlayerList,
CollectionType.FemaleHighlander => FemalePlayerList,
CollectionType.MaleWildwood => MalePlayerList,
CollectionType.FemaleWildwood => FemalePlayerList,
CollectionType.MaleDuskwight => MalePlayerList,
CollectionType.FemaleDuskwight => FemalePlayerList,
CollectionType.MalePlainsfolk => MalePlayerList,
CollectionType.FemalePlainsfolk => FemalePlayerList,
CollectionType.MaleDunesfolk => MalePlayerList,
CollectionType.FemaleDunesfolk => FemalePlayerList,
CollectionType.MaleSeekerOfTheSun => MalePlayerList,
CollectionType.FemaleSeekerOfTheSun => FemalePlayerList,
CollectionType.MaleKeeperOfTheMoon => MalePlayerList,
CollectionType.FemaleKeeperOfTheMoon => FemalePlayerList,
CollectionType.MaleSeawolf => MalePlayerList,
CollectionType.FemaleSeawolf => FemalePlayerList,
CollectionType.MaleHellsguard => MalePlayerList,
CollectionType.FemaleHellsguard => FemalePlayerList,
CollectionType.MaleRaen => MalePlayerList,
CollectionType.FemaleRaen => FemalePlayerList,
CollectionType.MaleXaela => MalePlayerList,
CollectionType.FemaleXaela => FemalePlayerList,
CollectionType.MaleHelion => MalePlayerList,
CollectionType.FemaleHelion => FemalePlayerList,
CollectionType.MaleLost => MalePlayerList,
CollectionType.FemaleLost => FemalePlayerList,
CollectionType.MaleRava => MalePlayerList,
CollectionType.FemaleRava => FemalePlayerList,
CollectionType.MaleVeena => MalePlayerList,
CollectionType.FemaleVeena => FemalePlayerList,
CollectionType.MaleMidlanderNpc => MaleNpcList,
CollectionType.FemaleMidlanderNpc => FemaleNpcList,
CollectionType.MaleHighlanderNpc => MaleNpcList,
CollectionType.FemaleHighlanderNpc => FemaleNpcList,
CollectionType.MaleWildwoodNpc => MaleNpcList,
CollectionType.FemaleWildwoodNpc => FemaleNpcList,
CollectionType.MaleDuskwightNpc => MaleNpcList,
CollectionType.FemaleDuskwightNpc => FemaleNpcList,
CollectionType.MalePlainsfolkNpc => MaleNpcList,
CollectionType.FemalePlainsfolkNpc => FemaleNpcList,
CollectionType.MaleDunesfolkNpc => MaleNpcList,
CollectionType.FemaleDunesfolkNpc => FemaleNpcList,
CollectionType.MaleSeekerOfTheSunNpc => MaleNpcList,
CollectionType.FemaleSeekerOfTheSunNpc => FemaleNpcList,
CollectionType.MaleKeeperOfTheMoonNpc => MaleNpcList,
CollectionType.FemaleKeeperOfTheMoonNpc => FemaleNpcList,
CollectionType.MaleSeawolfNpc => MaleNpcList,
CollectionType.FemaleSeawolfNpc => FemaleNpcList,
CollectionType.MaleHellsguardNpc => MaleNpcList,
CollectionType.FemaleHellsguardNpc => FemaleNpcList,
CollectionType.MaleRaenNpc => MaleNpcList,
CollectionType.FemaleRaenNpc => FemaleNpcList,
CollectionType.MaleXaelaNpc => MaleNpcList,
CollectionType.FemaleXaelaNpc => FemaleNpcList,
CollectionType.MaleHelionNpc => MaleNpcList,
CollectionType.FemaleHelionNpc => FemaleNpcList,
CollectionType.MaleLostNpc => MaleNpcList,
CollectionType.FemaleLostNpc => FemaleNpcList,
CollectionType.MaleRavaNpc => MaleNpcList,
CollectionType.FemaleRavaNpc => FemaleNpcList,
CollectionType.MaleVeenaNpc => MaleNpcList,
CollectionType.FemaleVeenaNpc => FemaleNpcList,
CollectionType.Individual => DefaultList,
_ => Array.Empty<CollectionType>(),
};
public static CollectionType FromParts( SubRace race, Gender gender, bool npc )
{
gender = gender switch
{
Gender.MaleNpc => Gender.Male,
Gender.FemaleNpc => Gender.Female,
_ => gender,
};
return ( race, gender, npc ) switch
{
(SubRace.Midlander, Gender.Male, false) => CollectionType.MaleMidlander,
(SubRace.Highlander, Gender.Male, false) => CollectionType.MaleHighlander,
(SubRace.Wildwood, Gender.Male, false) => CollectionType.MaleWildwood,
(SubRace.Duskwight, Gender.Male, false) => CollectionType.MaleDuskwight,
(SubRace.Plainsfolk, Gender.Male, false) => CollectionType.MalePlainsfolk,
(SubRace.Dunesfolk, Gender.Male, false) => CollectionType.MaleDunesfolk,
(SubRace.SeekerOfTheSun, Gender.Male, false) => CollectionType.MaleSeekerOfTheSun,
(SubRace.KeeperOfTheMoon, Gender.Male, false) => CollectionType.MaleKeeperOfTheMoon,
(SubRace.Seawolf, Gender.Male, false) => CollectionType.MaleSeawolf,
(SubRace.Hellsguard, Gender.Male, false) => CollectionType.MaleHellsguard,
(SubRace.Raen, Gender.Male, false) => CollectionType.MaleRaen,
(SubRace.Xaela, Gender.Male, false) => CollectionType.MaleXaela,
(SubRace.Helion, Gender.Male, false) => CollectionType.MaleHelion,
(SubRace.Lost, Gender.Male, false) => CollectionType.MaleLost,
(SubRace.Rava, Gender.Male, false) => CollectionType.MaleRava,
(SubRace.Veena, Gender.Male, false) => CollectionType.MaleVeena,
(SubRace.Midlander, Gender.Female, false) => CollectionType.FemaleMidlander,
(SubRace.Highlander, Gender.Female, false) => CollectionType.FemaleHighlander,
(SubRace.Wildwood, Gender.Female, false) => CollectionType.FemaleWildwood,
(SubRace.Duskwight, Gender.Female, false) => CollectionType.FemaleDuskwight,
(SubRace.Plainsfolk, Gender.Female, false) => CollectionType.FemalePlainsfolk,
(SubRace.Dunesfolk, Gender.Female, false) => CollectionType.FemaleDunesfolk,
(SubRace.SeekerOfTheSun, Gender.Female, false) => CollectionType.FemaleSeekerOfTheSun,
(SubRace.KeeperOfTheMoon, Gender.Female, false) => CollectionType.FemaleKeeperOfTheMoon,
(SubRace.Seawolf, Gender.Female, false) => CollectionType.FemaleSeawolf,
(SubRace.Hellsguard, Gender.Female, false) => CollectionType.FemaleHellsguard,
(SubRace.Raen, Gender.Female, false) => CollectionType.FemaleRaen,
(SubRace.Xaela, Gender.Female, false) => CollectionType.FemaleXaela,
(SubRace.Helion, Gender.Female, false) => CollectionType.FemaleHelion,
(SubRace.Lost, Gender.Female, false) => CollectionType.FemaleLost,
(SubRace.Rava, Gender.Female, false) => CollectionType.FemaleRava,
(SubRace.Veena, Gender.Female, false) => CollectionType.FemaleVeena,
(SubRace.Midlander, Gender.Male, true) => CollectionType.MaleMidlanderNpc,
(SubRace.Highlander, Gender.Male, true) => CollectionType.MaleHighlanderNpc,
(SubRace.Wildwood, Gender.Male, true) => CollectionType.MaleWildwoodNpc,
(SubRace.Duskwight, Gender.Male, true) => CollectionType.MaleDuskwightNpc,
(SubRace.Plainsfolk, Gender.Male, true) => CollectionType.MalePlainsfolkNpc,
(SubRace.Dunesfolk, Gender.Male, true) => CollectionType.MaleDunesfolkNpc,
(SubRace.SeekerOfTheSun, Gender.Male, true) => CollectionType.MaleSeekerOfTheSunNpc,
(SubRace.KeeperOfTheMoon, Gender.Male, true) => CollectionType.MaleKeeperOfTheMoonNpc,
(SubRace.Seawolf, Gender.Male, true) => CollectionType.MaleSeawolfNpc,
(SubRace.Hellsguard, Gender.Male, true) => CollectionType.MaleHellsguardNpc,
(SubRace.Raen, Gender.Male, true) => CollectionType.MaleRaenNpc,
(SubRace.Xaela, Gender.Male, true) => CollectionType.MaleXaelaNpc,
(SubRace.Helion, Gender.Male, true) => CollectionType.MaleHelionNpc,
(SubRace.Lost, Gender.Male, true) => CollectionType.MaleLostNpc,
(SubRace.Rava, Gender.Male, true) => CollectionType.MaleRavaNpc,
(SubRace.Veena, Gender.Male, true) => CollectionType.MaleVeenaNpc,
(SubRace.Midlander, Gender.Female, true) => CollectionType.FemaleMidlanderNpc,
(SubRace.Highlander, Gender.Female, true) => CollectionType.FemaleHighlanderNpc,
(SubRace.Wildwood, Gender.Female, true) => CollectionType.FemaleWildwoodNpc,
(SubRace.Duskwight, Gender.Female, true) => CollectionType.FemaleDuskwightNpc,
(SubRace.Plainsfolk, Gender.Female, true) => CollectionType.FemalePlainsfolkNpc,
(SubRace.Dunesfolk, Gender.Female, true) => CollectionType.FemaleDunesfolkNpc,
(SubRace.SeekerOfTheSun, Gender.Female, true) => CollectionType.FemaleSeekerOfTheSunNpc,
(SubRace.KeeperOfTheMoon, Gender.Female, true) => CollectionType.FemaleKeeperOfTheMoonNpc,
(SubRace.Seawolf, Gender.Female, true) => CollectionType.FemaleSeawolfNpc,
(SubRace.Hellsguard, Gender.Female, true) => CollectionType.FemaleHellsguardNpc,
(SubRace.Raen, Gender.Female, true) => CollectionType.FemaleRaenNpc,
(SubRace.Xaela, Gender.Female, true) => CollectionType.FemaleXaelaNpc,
(SubRace.Helion, Gender.Female, true) => CollectionType.FemaleHelionNpc,
(SubRace.Lost, Gender.Female, true) => CollectionType.FemaleLostNpc,
(SubRace.Rava, Gender.Female, true) => CollectionType.FemaleRavaNpc,
(SubRace.Veena, Gender.Female, true) => CollectionType.FemaleVeenaNpc,
_ => CollectionType.Inactive,
};
}
public static bool TryParse( string text, out CollectionType type )
{
if( Enum.TryParse( text, true, out type ) )
{
return type is not CollectionType.Inactive and not CollectionType.Temporary;
}
if( string.Equals( text, "character", StringComparison.OrdinalIgnoreCase ) )
{
type = CollectionType.Individual;
return true;
}
if( string.Equals( text, "base", StringComparison.OrdinalIgnoreCase ) )
{
type = CollectionType.Default;
return true;
}
if( string.Equals( text, "ui", StringComparison.OrdinalIgnoreCase ) )
{
type = CollectionType.Interface;
return true;
}
if( string.Equals( text, "selected", StringComparison.OrdinalIgnoreCase ) )
{
type = CollectionType.Current;
return true;
}
foreach( var t in Enum.GetValues< CollectionType >() )
{
if( t is CollectionType.Inactive or CollectionType.Temporary )
{
continue;
}
if( string.Equals( text, t.ToName(), StringComparison.OrdinalIgnoreCase ) )
{
type = t;
return true;
}
}
return false;
}
public static string ToName( this CollectionType collectionType )
=> collectionType switch
{
CollectionType.Yourself => "Your Character",
CollectionType.NonPlayerChild => "Non-Player Children",
CollectionType.NonPlayerElderly => "Non-Player Elderly",
CollectionType.MalePlayerCharacter => "Male Player Characters",
CollectionType.MaleNonPlayerCharacter => "Male Non-Player Characters",
CollectionType.MaleMidlander => $"Male {SubRace.Midlander.ToName()}",
CollectionType.MaleHighlander => $"Male {SubRace.Highlander.ToName()}",
CollectionType.MaleWildwood => $"Male {SubRace.Wildwood.ToName()}",
CollectionType.MaleDuskwight => $"Male {SubRace.Duskwight.ToName()}",
CollectionType.MalePlainsfolk => $"Male {SubRace.Plainsfolk.ToName()}",
CollectionType.MaleDunesfolk => $"Male {SubRace.Dunesfolk.ToName()}",
CollectionType.MaleSeekerOfTheSun => $"Male {SubRace.SeekerOfTheSun.ToName()}",
CollectionType.MaleKeeperOfTheMoon => $"Male {SubRace.KeeperOfTheMoon.ToName()}",
CollectionType.MaleSeawolf => $"Male {SubRace.Seawolf.ToName()}",
CollectionType.MaleHellsguard => $"Male {SubRace.Hellsguard.ToName()}",
CollectionType.MaleRaen => $"Male {SubRace.Raen.ToName()}",
CollectionType.MaleXaela => $"Male {SubRace.Xaela.ToName()}",
CollectionType.MaleHelion => $"Male {SubRace.Helion.ToName()}",
CollectionType.MaleLost => $"Male {SubRace.Lost.ToName()}",
CollectionType.MaleRava => $"Male {SubRace.Rava.ToName()}",
CollectionType.MaleVeena => $"Male {SubRace.Veena.ToName()}",
CollectionType.MaleMidlanderNpc => $"Male {SubRace.Midlander.ToName()} (NPC)",
CollectionType.MaleHighlanderNpc => $"Male {SubRace.Highlander.ToName()} (NPC)",
CollectionType.MaleWildwoodNpc => $"Male {SubRace.Wildwood.ToName()} (NPC)",
CollectionType.MaleDuskwightNpc => $"Male {SubRace.Duskwight.ToName()} (NPC)",
CollectionType.MalePlainsfolkNpc => $"Male {SubRace.Plainsfolk.ToName()} (NPC)",
CollectionType.MaleDunesfolkNpc => $"Male {SubRace.Dunesfolk.ToName()} (NPC)",
CollectionType.MaleSeekerOfTheSunNpc => $"Male {SubRace.SeekerOfTheSun.ToName()} (NPC)",
CollectionType.MaleKeeperOfTheMoonNpc => $"Male {SubRace.KeeperOfTheMoon.ToName()} (NPC)",
CollectionType.MaleSeawolfNpc => $"Male {SubRace.Seawolf.ToName()} (NPC)",
CollectionType.MaleHellsguardNpc => $"Male {SubRace.Hellsguard.ToName()} (NPC)",
CollectionType.MaleRaenNpc => $"Male {SubRace.Raen.ToName()} (NPC)",
CollectionType.MaleXaelaNpc => $"Male {SubRace.Xaela.ToName()} (NPC)",
CollectionType.MaleHelionNpc => $"Male {SubRace.Helion.ToName()} (NPC)",
CollectionType.MaleLostNpc => $"Male {SubRace.Lost.ToName()} (NPC)",
CollectionType.MaleRavaNpc => $"Male {SubRace.Rava.ToName()} (NPC)",
CollectionType.MaleVeenaNpc => $"Male {SubRace.Veena.ToName()} (NPC)",
CollectionType.FemalePlayerCharacter => "Female Player Characters",
CollectionType.FemaleNonPlayerCharacter => "Female Non-Player Characters",
CollectionType.FemaleMidlander => $"Female {SubRace.Midlander.ToName()}",
CollectionType.FemaleHighlander => $"Female {SubRace.Highlander.ToName()}",
CollectionType.FemaleWildwood => $"Female {SubRace.Wildwood.ToName()}",
CollectionType.FemaleDuskwight => $"Female {SubRace.Duskwight.ToName()}",
CollectionType.FemalePlainsfolk => $"Female {SubRace.Plainsfolk.ToName()}",
CollectionType.FemaleDunesfolk => $"Female {SubRace.Dunesfolk.ToName()}",
CollectionType.FemaleSeekerOfTheSun => $"Female {SubRace.SeekerOfTheSun.ToName()}",
CollectionType.FemaleKeeperOfTheMoon => $"Female {SubRace.KeeperOfTheMoon.ToName()}",
CollectionType.FemaleSeawolf => $"Female {SubRace.Seawolf.ToName()}",
CollectionType.FemaleHellsguard => $"Female {SubRace.Hellsguard.ToName()}",
CollectionType.FemaleRaen => $"Female {SubRace.Raen.ToName()}",
CollectionType.FemaleXaela => $"Female {SubRace.Xaela.ToName()}",
CollectionType.FemaleHelion => $"Female {SubRace.Helion.ToName()}",
CollectionType.FemaleLost => $"Female {SubRace.Lost.ToName()}",
CollectionType.FemaleRava => $"Female {SubRace.Rava.ToName()}",
CollectionType.FemaleVeena => $"Female {SubRace.Veena.ToName()}",
CollectionType.FemaleMidlanderNpc => $"Female {SubRace.Midlander.ToName()} (NPC)",
CollectionType.FemaleHighlanderNpc => $"Female {SubRace.Highlander.ToName()} (NPC)",
CollectionType.FemaleWildwoodNpc => $"Female {SubRace.Wildwood.ToName()} (NPC)",
CollectionType.FemaleDuskwightNpc => $"Female {SubRace.Duskwight.ToName()} (NPC)",
CollectionType.FemalePlainsfolkNpc => $"Female {SubRace.Plainsfolk.ToName()} (NPC)",
CollectionType.FemaleDunesfolkNpc => $"Female {SubRace.Dunesfolk.ToName()} (NPC)",
CollectionType.FemaleSeekerOfTheSunNpc => $"Female {SubRace.SeekerOfTheSun.ToName()} (NPC)",
CollectionType.FemaleKeeperOfTheMoonNpc => $"Female {SubRace.KeeperOfTheMoon.ToName()} (NPC)",
CollectionType.FemaleSeawolfNpc => $"Female {SubRace.Seawolf.ToName()} (NPC)",
CollectionType.FemaleHellsguardNpc => $"Female {SubRace.Hellsguard.ToName()} (NPC)",
CollectionType.FemaleRaenNpc => $"Female {SubRace.Raen.ToName()} (NPC)",
CollectionType.FemaleXaelaNpc => $"Female {SubRace.Xaela.ToName()} (NPC)",
CollectionType.FemaleHelionNpc => $"Female {SubRace.Helion.ToName()} (NPC)",
CollectionType.FemaleLostNpc => $"Female {SubRace.Lost.ToName()} (NPC)",
CollectionType.FemaleRavaNpc => $"Female {SubRace.Rava.ToName()} (NPC)",
CollectionType.FemaleVeenaNpc => $"Female {SubRace.Veena.ToName()} (NPC)",
CollectionType.Inactive => "Collection",
CollectionType.Default => "Default",
CollectionType.Interface => "Interface",
CollectionType.Individual => "Individual",
CollectionType.Current => "Current",
_ => string.Empty,
};
public static string ToDescription( this CollectionType collectionType )
=> collectionType switch
{
CollectionType.Yourself => "This collection applies to your own character, regardless of its name.\n"
+ "It takes precedence before all other collections except for explicitly named individual collections.",
CollectionType.NonPlayerChild =>
"This collection applies to all non-player characters with a child body-type.\n"
+ "It takes precedence before all other collections except for explicitly named individual collections.",
CollectionType.NonPlayerElderly =>
"This collection applies to all non-player characters with an elderly body-type.\n"
+ "It takes precedence before all other collections except for explicitly named individual collections.",
CollectionType.MalePlayerCharacter =>
"This collection applies to all male player characters that do not have a more specific character or racial collections associated.",
CollectionType.MaleNonPlayerCharacter =>
"This collection applies to all human male non-player characters except those explicitly named. It takes precedence before the default and racial collections.",
CollectionType.MaleMidlander =>
"This collection applies to all male player character Midlander Hyur that do not have a more specific character collection associated.",
CollectionType.MaleHighlander =>
"This collection applies to all male player character Highlander Hyur that do not have a more specific character collection associated.",
CollectionType.MaleWildwood =>
"This collection applies to all male player character Wildwood Elezen that do not have a more specific character collection associated.",
CollectionType.MaleDuskwight =>
"This collection applies to all male player character Duskwight Elezen that do not have a more specific character collection associated.",
CollectionType.MalePlainsfolk =>
"This collection applies to all male player character Plainsfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.MaleDunesfolk =>
"This collection applies to all male player character Dunesfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.MaleSeekerOfTheSun =>
"This collection applies to all male player character Seekers of the Sun that do not have a more specific character collection associated.",
CollectionType.MaleKeeperOfTheMoon =>
"This collection applies to all male player character Keepers of the Moon that do not have a more specific character collection associated.",
CollectionType.MaleSeawolf =>
"This collection applies to all male player character Sea Wolf Roegadyn that do not have a more specific character collection associated.",
CollectionType.MaleHellsguard =>
"This collection applies to all male player character Hellsguard Roegadyn that do not have a more specific character collection associated.",
CollectionType.MaleRaen =>
"This collection applies to all male player character Raen Au Ra that do not have a more specific character collection associated.",
CollectionType.MaleXaela =>
"This collection applies to all male player character Xaela Au Ra that do not have a more specific character collection associated.",
CollectionType.MaleHelion =>
"This collection applies to all male player character Helion Hrothgar that do not have a more specific character collection associated.",
CollectionType.MaleLost =>
"This collection applies to all male player character Lost Hrothgar that do not have a more specific character collection associated.",
CollectionType.MaleRava =>
"This collection applies to all male player character Rava Viera that do not have a more specific character collection associated.",
CollectionType.MaleVeena =>
"This collection applies to all male player character Veena Viera that do not have a more specific character collection associated.",
CollectionType.MaleMidlanderNpc =>
"This collection applies to all male non-player character Midlander Hyur that do not have a more specific character collection associated.",
CollectionType.MaleHighlanderNpc =>
"This collection applies to all male non-player character Highlander Hyur that do not have a more specific character collection associated.",
CollectionType.MaleWildwoodNpc =>
"This collection applies to all male non-player character Wildwood Elezen that do not have a more specific character collection associated.",
CollectionType.MaleDuskwightNpc =>
"This collection applies to all male non-player character Duskwight Elezen that do not have a more specific character collection associated.",
CollectionType.MalePlainsfolkNpc =>
"This collection applies to all male non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.MaleDunesfolkNpc =>
"This collection applies to all male non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.MaleSeekerOfTheSunNpc =>
"This collection applies to all male non-player character Seekers of the Sun that do not have a more specific character collection associated.",
CollectionType.MaleKeeperOfTheMoonNpc =>
"This collection applies to all male non-player character Keepers of the Moon that do not have a more specific character collection associated.",
CollectionType.MaleSeawolfNpc =>
"This collection applies to all male non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.",
CollectionType.MaleHellsguardNpc =>
"This collection applies to all male non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.",
CollectionType.MaleRaenNpc =>
"This collection applies to all male non-player character Raen Au Ra that do not have a more specific character collection associated.",
CollectionType.MaleXaelaNpc =>
"This collection applies to all male non-player character Xaela Au Ra that do not have a more specific character collection associated.",
CollectionType.MaleHelionNpc =>
"This collection applies to all male non-player character Helion Hrothgar that do not have a more specific character collection associated.",
CollectionType.MaleLostNpc =>
"This collection applies to all male non-player character Lost Hrothgar that do not have a more specific character collection associated.",
CollectionType.MaleRavaNpc =>
"This collection applies to all male non-player character Rava Viera that do not have a more specific character collection associated.",
CollectionType.MaleVeenaNpc =>
"This collection applies to all male non-player character Veena Viera that do not have a more specific character collection associated.",
CollectionType.FemalePlayerCharacter =>
"This collection applies to all female player characters that do not have a more specific character or racial collections associated.",
CollectionType.FemaleNonPlayerCharacter =>
"This collection applies to all human female non-player characters except those explicitly named. It takes precedence before the default and racial collections.",
CollectionType.FemaleMidlander =>
"This collection applies to all female player character Midlander Hyur that do not have a more specific character collection associated.",
CollectionType.FemaleHighlander =>
"This collection applies to all female player character Highlander Hyur that do not have a more specific character collection associated.",
CollectionType.FemaleWildwood =>
"This collection applies to all female player character Wildwood Elezen that do not have a more specific character collection associated.",
CollectionType.FemaleDuskwight =>
"This collection applies to all female player character Duskwight Elezen that do not have a more specific character collection associated.",
CollectionType.FemalePlainsfolk =>
"This collection applies to all female player character Plainsfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.FemaleDunesfolk =>
"This collection applies to all female player character Dunesfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.FemaleSeekerOfTheSun =>
"This collection applies to all female player character Seekers of the Sun that do not have a more specific character collection associated.",
CollectionType.FemaleKeeperOfTheMoon =>
"This collection applies to all female player character Keepers of the Moon that do not have a more specific character collection associated.",
CollectionType.FemaleSeawolf =>
"This collection applies to all female player character Sea Wolf Roegadyn that do not have a more specific character collection associated.",
CollectionType.FemaleHellsguard =>
"This collection applies to all female player character Hellsguard Roegadyn that do not have a more specific character collection associated.",
CollectionType.FemaleRaen =>
"This collection applies to all female player character Raen Au Ra that do not have a more specific character collection associated.",
CollectionType.FemaleXaela =>
"This collection applies to all female player character Xaela Au Ra that do not have a more specific character collection associated.",
CollectionType.FemaleHelion =>
"This collection applies to all female player character Helion Hrothgar that do not have a more specific character collection associated.",
CollectionType.FemaleLost =>
"This collection applies to all female player character Lost Hrothgar that do not have a more specific character collection associated.",
CollectionType.FemaleRava =>
"This collection applies to all female player character Rava Viera that do not have a more specific character collection associated.",
CollectionType.FemaleVeena =>
"This collection applies to all female player character Veena Viera that do not have a more specific character collection associated.",
CollectionType.FemaleMidlanderNpc =>
"This collection applies to all female non-player character Midlander Hyur that do not have a more specific character collection associated.",
CollectionType.FemaleHighlanderNpc =>
"This collection applies to all female non-player character Highlander Hyur that do not have a more specific character collection associated.",
CollectionType.FemaleWildwoodNpc =>
"This collection applies to all female non-player character Wildwood Elezen that do not have a more specific character collection associated.",
CollectionType.FemaleDuskwightNpc =>
"This collection applies to all female non-player character Duskwight Elezen that do not have a more specific character collection associated.",
CollectionType.FemalePlainsfolkNpc =>
"This collection applies to all female non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.FemaleDunesfolkNpc =>
"This collection applies to all female non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.FemaleSeekerOfTheSunNpc =>
"This collection applies to all female non-player character Seekers of the Sun that do not have a more specific character collection associated.",
CollectionType.FemaleKeeperOfTheMoonNpc =>
"This collection applies to all female non-player character Keepers of the Moon that do not have a more specific character collection associated.",
CollectionType.FemaleSeawolfNpc =>
"This collection applies to all female non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.",
CollectionType.FemaleHellsguardNpc =>
"This collection applies to all female non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.",
CollectionType.FemaleRaenNpc =>
"This collection applies to all female non-player character Raen Au Ra that do not have a more specific character collection associated.",
CollectionType.FemaleXaelaNpc =>
"This collection applies to all female non-player character Xaela Au Ra that do not have a more specific character collection associated.",
CollectionType.FemaleHelionNpc =>
"This collection applies to all female non-player character Helion Hrothgar that do not have a more specific character collection associated.",
CollectionType.FemaleLostNpc =>
"This collection applies to all female non-player character Lost Hrothgar that do not have a more specific character collection associated.",
CollectionType.FemaleRavaNpc =>
"This collection applies to all female non-player character Rava Viera that do not have a more specific character collection associated.",
CollectionType.FemaleVeenaNpc =>
"This collection applies to all female non-player character Veena Viera that do not have a more specific character collection associated.",
_ => string.Empty,
};
}

View file

@ -1,149 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface.Internal.Notifications;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Actors;
using Penumbra.String;
using Penumbra.Util;
namespace Penumbra.Collections;
public partial class IndividualCollections
{
public JArray ToJObject()
{
var ret = new JArray();
foreach( var (name, identifiers, collection) in Assignments )
{
var tmp = identifiers[0].ToJson();
tmp.Add( "Collection", collection.Name );
tmp.Add( "Display", name );
ret.Add( tmp );
}
return ret;
}
public bool ReadJObject( JArray? obj, CollectionManager manager )
{
if( obj == null )
{
return true;
}
var changes = false;
foreach( var data in obj )
{
try
{
var identifier = Penumbra.Actors.FromJson( data as JObject );
var group = GetGroup( identifier );
if( group.Length == 0 || group.Any( i => !i.IsValid ) )
{
changes = true;
Penumbra.ChatService.NotificationMessage( "Could not load an unknown individual collection, removed.", "Load Failure", NotificationType.Warning );
continue;
}
var collectionName = data[ "Collection" ]?.ToObject< string >() ?? string.Empty;
if( collectionName.Length == 0 || !manager.ByName( collectionName, out var collection ) )
{
changes = true;
Penumbra.ChatService.NotificationMessage( $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", "Load Failure",
NotificationType.Warning );
continue;
}
if( !Add( group, collection ) )
{
changes = true;
Penumbra.ChatService.NotificationMessage( $"Could not add an individual collection for {identifier}, removed.", "Load Failure",
NotificationType.Warning );
}
}
catch( Exception e )
{
changes = true;
Penumbra.ChatService.NotificationMessage( $"Could not load an unknown individual collection, removed:\n{e}", "Load Failure", NotificationType.Error );
}
}
return changes;
}
internal void Migrate0To1( Dictionary< string, ModCollection > old )
{
static bool FindDataId( string name, IReadOnlyDictionary< uint, string > data, out uint dataId )
{
var kvp = data.FirstOrDefault( kvp => kvp.Value.Equals( name, StringComparison.OrdinalIgnoreCase ),
new KeyValuePair< uint, string >( uint.MaxValue, string.Empty ) );
dataId = kvp.Key;
return kvp.Value.Length > 0;
}
foreach( var (name, collection) in old )
{
var kind = ObjectKind.None;
var lowerName = name.ToLowerInvariant();
// Prefer matching NPC names, fewer false positives than preferring players.
if( FindDataId( lowerName, _actorManager.Data.Companions, out var dataId ) )
{
kind = ObjectKind.Companion;
}
else if( FindDataId( lowerName, _actorManager.Data.Mounts, out dataId ) )
{
kind = ObjectKind.MountType;
}
else if( FindDataId( lowerName, _actorManager.Data.BNpcs, out dataId ) )
{
kind = ObjectKind.BattleNpc;
}
else if( FindDataId( lowerName, _actorManager.Data.ENpcs, out dataId ) )
{
kind = ObjectKind.EventNpc;
}
var identifier = _actorManager.CreateNpc( kind, dataId );
if( identifier.IsValid )
{
// If the name corresponds to a valid npc, add it as a group. If this fails, notify users.
var group = GetGroup( identifier );
var ids = string.Join( ", ", group.Select( i => i.DataId.ToString() ) );
if( Add( $"{_actorManager.Data.ToName( kind, dataId )} ({kind.ToName()})", group, collection ) )
{
Penumbra.Log.Information( $"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]." );
}
else
{
Penumbra.ChatService.NotificationMessage(
$"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.",
"Migration Failure", NotificationType.Error );
}
}
// If it is not a valid NPC name, check if it can be a player name.
else if( ActorManager.VerifyPlayerName( name ) )
{
identifier = _actorManager.CreatePlayer( ByteString.FromStringUnsafe( name, false ), ushort.MaxValue );
var shortName = string.Join( " ", name.Split().Select( n => $"{n[ 0 ]}." ) );
// Try to migrate the player name without logging full names.
if( Add( $"{name} ({_actorManager.Data.ToWorldName( identifier.HomeWorld )})", new[] { identifier }, collection ) )
{
Penumbra.Log.Information( $"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier." );
}
else
{
Penumbra.ChatService.NotificationMessage( $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.",
"Migration Failure", NotificationType.Error );
}
}
else
{
Penumbra.ChatService.NotificationMessage(
$"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.",
"Migration Failure", NotificationType.Error );
}
}
}
}

View file

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.Services;
namespace Penumbra.Collections.Manager;
public static class ActiveCollectionMigration
{
/// <summary> Migrate ungendered collections to Male and Female for 0.5.9.0. </summary>
public static void MigrateUngenderedCollections(FilenameService fileNames)
{
if (!ActiveCollections.Load(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);
}
/// <summary> Migrate individual collections to Identifiers for 0.6.0. </summary>
public static bool MigrateIndividualCollections(CollectionStorage storage, IndividualCollections individuals, JObject jObject)
{
var version = jObject[nameof(Version)]?.Value<int>() ?? 0;
if (version > 0)
return false;
// Load character collections. If a player name comes up multiple times, the last one is applied.
var characters = jObject["Characters"]?.ToObject<Dictionary<string, string>>() ?? new Dictionary<string, string>();
var dict = new Dictionary<string, ModCollection>(characters.Count);
foreach (var (player, collectionName) in characters)
{
if (!storage.ByName(collectionName, out var collection))
{
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, collection);
}
}
individuals.Migrate0To1(dict);
return true;
}
}

View file

@ -0,0 +1,469 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.GameData.Actors;
using Penumbra.Services;
using Penumbra.UI;
using Penumbra.Util;
using static OtterGui.Raii.ImRaii;
namespace Penumbra.Collections.Manager;
public class ActiveCollections : ISavable, IDisposable
{
public const int Version = 1;
private readonly CollectionStorage _storage;
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
public ActiveCollections(CollectionStorage storage, ActorService actors, CommunicatorService communicator, SaveService saveService)
{
_storage = storage;
_communicator = communicator;
_saveService = saveService;
Current = storage.DefaultNamed;
Default = storage.DefaultNamed;
Interface = storage.DefaultNamed;
Individuals = new IndividualCollections(actors.AwaitedService);
_communicator.CollectionChange.Subscribe(OnCollectionChange);
LoadCollections();
}
public void Dispose()
=> _communicator.CollectionChange.Unsubscribe(OnCollectionChange);
/// <summary> The collection currently selected for changing settings. </summary>
public ModCollection Current { get; private set; }
/// <summary> Whether the currently selected collection is used either directly via assignment or via inheritance. </summary>
public bool CurrentCollectionInUse { get; private set; }
/// <summary> The collection used for general file redirections and all characters not specifically named. </summary>
public ModCollection Default { get; private set; }
/// <summary> The collection used for all files categorized as UI files. </summary>
public ModCollection Interface { get; private set; }
/// <summary> The list of individual assignments. </summary>
public readonly IndividualCollections Individuals;
/// <summary> Get the collection assigned to an individual or Default if unassigned. </summary>
public ModCollection Individual(ActorIdentifier identifier)
=> Individuals.TryGetCollection(identifier, out var c) ? c : Default;
/// <summary> The list of group assignments. </summary>
private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues<Api.Enums.ApiCollectionType>().Length - 3];
/// <summary> Return all actually assigned group assignments. </summary>
public IEnumerable<KeyValuePair<CollectionType, ModCollection>> SpecialAssignments
{
get
{
for (var i = 0; i < _specialCollections.Length; ++i)
{
var collection = _specialCollections[i];
if (collection != null)
yield return new KeyValuePair<CollectionType, ModCollection>((CollectionType)i, collection);
}
}
}
/// <inheritdoc cref="ByType(CollectionType, ActorIdentifier)"/>
public ModCollection? ByType(CollectionType type)
=> ByType(type, ActorIdentifier.Invalid);
/// <summary> Return the configured collection for the given type or null. </summary>
public ModCollection? ByType(CollectionType type, ActorIdentifier identifier)
{
if (type.IsSpecial())
return _specialCollections[(int)type];
return type switch
{
CollectionType.Default => Default,
CollectionType.Interface => Interface,
CollectionType.Current => Current,
CollectionType.Individual => identifier.IsValid && Individuals.TryGetValue(identifier, out var c) ? c : null,
_ => null,
};
}
/// <summary> Create a special collection if it does not exist and set it to Empty. </summary>
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;
}
/// <summary> Remove a special collection if it exists </summary>
public void RemoveSpecialCollection(CollectionType collectionType)
{
if (!collectionType.IsSpecial())
return;
var old = _specialCollections[(int)collectionType];
if (old == null)
return;
_specialCollections[(int)collectionType] = null;
_communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty);
}
/// <summary>Create an individual collection if possible. </summary>
public void CreateIndividualCollection(params ActorIdentifier[] identifiers)
{
if (Individuals.Add(identifiers, Default))
_communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName);
}
/// <summary> Remove an individual collection if it exists. </summary>
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);
}
/// <summary> Move an individual collection from one index to another. </summary>
public void MoveIndividualCollection(int from, int to)
{
if (Individuals.Move(from, to))
_saveService.QueueSave(this);
}
/// <summary> Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. </summary>
public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1)
{
var oldCollection = collectionType switch
{
CollectionType.Default => Default,
CollectionType.Interface => Interface,
CollectionType.Current => Current,
CollectionType.Individual when individualIndex >= 0 && individualIndex < Individuals.Count => Individuals[individualIndex].Collection,
CollectionType.Individual => null,
_ when collectionType.IsSpecial() => _specialCollections[(int)collectionType] ?? Default,
_ => null,
};
if (oldCollection == null || collection == oldCollection || collection.Index >= _storage.Count)
return;
switch (collectionType)
{
case CollectionType.Default:
Default = collection;
break;
case CollectionType.Interface:
Interface = collection;
break;
case CollectionType.Current:
Current = collection;
break;
case CollectionType.Individual:
if (!Individuals.ChangeCollection(individualIndex, collection))
return;
break;
default:
_specialCollections[(int)collectionType] = collection;
break;
}
UpdateCurrentCollectionInUse();
_communicator.CollectionChange.Invoke(collectionType, oldCollection, collection,
collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty);
}
public string ToFilename(FilenameService fileNames)
=> fileNames.ActiveCollectionsFile;
public string TypeName
=> "Active Collections";
public string LogName(string _)
=> "to file";
public void Save(StreamWriter writer)
{
var jObj = new JObject
{
{ nameof(Version), Version },
{ nameof(Default), Default.Name },
{ nameof(Interface), Interface.Name },
{ nameof(Current), Current.Name },
};
foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null)
.Select(p => ((CollectionType)p.Index, p.Value!)))
jObj.Add(type.ToString(), collection.Name);
jObj.Add(nameof(Individuals), Individuals.ToJObject());
using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
jObj.WriteTo(j);
}
private void UpdateCurrentCollectionInUse()
=> CurrentCollectionInUse = _specialCollections
.OfType<ModCollection>()
.Prepend(Interface)
.Prepend(Default)
.Concat(Individuals.Assignments.Select(kvp => kvp.Collection))
.SelectMany(c => c.GetFlattenedInheritance()).Contains(Current);
/// <summary> Save if any of the active collections is changed and set new collections to Current. </summary>
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _3)
{
if (collectionType is CollectionType.Inactive)
{
if (newCollection != null)
{
SetCollection(newCollection, CollectionType.Current);
}
else if (oldCollection != null)
{
if (oldCollection == Default)
SetCollection(ModCollection.Empty, CollectionType.Default);
if (oldCollection == Interface)
SetCollection(ModCollection.Empty, CollectionType.Interface);
if (oldCollection == Current)
SetCollection(Default.Index > ModCollection.Empty.Index ? Default : _storage.DefaultNamed, CollectionType.Current);
for (var i = 0; i < _specialCollections.Length; ++i)
{
if (oldCollection == _specialCollections[i])
SetCollection(ModCollection.Empty, (CollectionType)i);
}
for (var i = 0; i < Individuals.Count; ++i)
{
if (oldCollection == Individuals[i].Collection)
SetCollection(ModCollection.Empty, CollectionType.Individual, i);
}
}
}
else if (collectionType is not CollectionType.Temporary)
{
_saveService.QueueSave(this);
}
}
/// <summary>
/// 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.
/// </summary>
private void LoadCollections()
{
var configChanged = !Load(_saveService.FileNames, out var jObject);
// Load the default collection. If the string does not exist take the Default name if no file existed or the Empty name if one existed.
var defaultName = jObject[nameof(Default)]?.ToObject<string>()
?? (configChanged ? ModCollection.DefaultCollectionName : ModCollection.Empty.Name);
if (!_storage.ByName(defaultName, out var defaultCollection))
{
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 = defaultCollection;
}
// Load the interface collection. If no string is set, use the name of whatever was set as Default.
var interfaceName = jObject[nameof(Interface)]?.ToObject<string>() ?? Default.Name;
if (!_storage.ByName(interfaceName, out var interfaceCollection))
{
Penumbra.ChatService.NotificationMessage(
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.",
"Load Failure", NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
}
else
{
Interface = interfaceCollection;
}
// Load the current collection.
var currentName = jObject[nameof(Current)]?.ToObject<string>() ?? Default.Name;
if (!_storage.ByName(currentName, out var currentCollection))
{
Penumbra.ChatService.NotificationMessage(
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.",
"Load Failure", NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
}
else
{
Current = currentCollection;
}
// Load special collections.
foreach (var (type, name, _) in CollectionTypeExtensions.Special)
{
var typeName = jObject[type.ToString()]?.ToObject<string>();
if (typeName != null)
{
if (!_storage.ByName(typeName, out var typeCollection))
{
Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.",
"Load Failure",
NotificationType.Warning);
configChanged = true;
}
else
{
_specialCollections[(int)type] = typeCollection;
}
}
}
configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject);
configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, _storage);
// Save any changes and create all required caches.
if (configChanged)
_saveService.ImmediateSave(this);
}
/// <summary>
/// Read the active collection file into a jObject.
/// Returns true if this is successful, false if the file does not exist or it is unsuccessful.
/// </summary>
public static bool Load(FilenameService fileNames, out JObject ret)
{
var file = fileNames.ActiveCollectionsFile;
if (File.Exists(file))
try
{
ret = JObject.Parse(File.ReadAllText(file));
return true;
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}");
}
ret = new JObject();
return false;
}
public string RedundancyCheck(CollectionType type, ActorIdentifier id)
{
var checkAssignment = ByType(type, id);
if (checkAssignment == null)
return string.Empty;
switch (type)
{
// Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap.
case CollectionType.Individual:
switch (id.Type)
{
case IdentifierType.Player when id.HomeWorld != ushort.MaxValue:
{
var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue));
return global?.Index == checkAssignment.Index
? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."
: string.Empty;
}
case IdentifierType.Owned:
if (id.HomeWorld != ushort.MaxValue)
{
var global = ByType(CollectionType.Individual,
Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId));
if (global?.Index == checkAssignment.Index)
return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it.";
}
var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId));
return unowned?.Index == checkAssignment.Index
? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it."
: string.Empty;
}
break;
// The group of all Characters is redundant if they are all equal to Default or unassigned.
case CollectionType.MalePlayerCharacter:
case CollectionType.MaleNonPlayerCharacter:
case CollectionType.FemalePlayerCharacter:
case CollectionType.FemaleNonPlayerCharacter:
var first = ByType(CollectionType.MalePlayerCharacter) ?? Default;
var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default;
var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default;
var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default;
if (first.Index == second.Index
&& first.Index == third.Index
&& first.Index == fourth.Index
&& first.Index == Default.Index)
return
"Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n"
+ "You can keep just the Default Assignment.";
break;
// Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default.
case CollectionType.NonPlayerChild:
case CollectionType.NonPlayerElderly:
var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter);
var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter);
var collection1 = CollectionType.MaleNonPlayerCharacter;
var collection2 = CollectionType.FemaleNonPlayerCharacter;
if (maleNpc == null)
{
maleNpc = Default;
if (maleNpc.Index != checkAssignment.Index)
return string.Empty;
collection1 = CollectionType.Default;
}
if (femaleNpc == null)
{
femaleNpc = Default;
if (femaleNpc.Index != checkAssignment.Index)
return string.Empty;
collection2 = CollectionType.Default;
}
return collection1 == collection2
? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them."
: $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them.";
// For other assignments, check the inheritance order, unassigned means fall-through,
// assigned needs identical assignments to be redundant.
default:
var group = type.InheritanceOrder();
foreach (var parentType in group)
{
var assignment = ByType(parentType);
if (assignment == null)
continue;
if (assignment.Index == checkAssignment.Index)
return
$"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it.";
}
break;
}
return string.Empty;
}
}

View file

@ -0,0 +1,173 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Penumbra.Api;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
namespace Penumbra.Collections.Manager;
public class CollectionCacheManager : IDisposable, IReadOnlyDictionary<ModCollection, ModCollectionCache>
{
private readonly ActiveCollections _active;
private readonly CommunicatorService _communicator;
private readonly Dictionary<ModCollection, ModCollectionCache> _cache = new();
public int Count
=> _cache.Count;
public IEnumerator<KeyValuePair<ModCollection, ModCollectionCache>> GetEnumerator()
=> _cache.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public bool ContainsKey(ModCollection key)
=> _cache.ContainsKey(key);
public bool TryGetValue(ModCollection key, [NotNullWhen(true)] out ModCollectionCache? value)
=> _cache.TryGetValue(key, out value);
public ModCollectionCache this[ModCollection key]
=> _cache[key];
public IEnumerable<ModCollection> Keys
=> _cache.Keys;
public IEnumerable<ModCollectionCache> Values
=> _cache.Values;
public IEnumerable<ModCollection> Active
=> _cache.Keys.Where(c => c.Index > ModCollection.Empty.Index);
public CollectionCacheManager(ActiveCollections active, CommunicatorService communicator)
{
_active = active;
_communicator = communicator;
_communicator.CollectionChange.Subscribe(OnCollectionChange);
_communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100);
_communicator.ModPathChanged.Subscribe(OnModChangeRemoval, 100);
_communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange);
_communicator.ModOptionChanged.Subscribe(OnModOptionChange, -100);
CreateNecessaryCaches();
}
public void Dispose()
{
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
_communicator.ModPathChanged.Unsubscribe(OnModChangeAddition);
_communicator.ModPathChanged.Unsubscribe(OnModChangeRemoval);
_communicator.TemporaryGlobalModChange.Unsubscribe(OnGlobalModChange);
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
}
/// <summary>
/// Cache handling. Usually recreate caches on the next framework tick,
/// but at launch create all of them at once.
/// </summary>
public void CreateNecessaryCaches()
{
var tasks = _active.SpecialAssignments.Select(p => p.Value)
.Concat(_active.Individuals.Select(p => p.Collection))
.Prepend(_active.Current)
.Prepend(_active.Default)
.Prepend(_active.Interface)
.Distinct()
.Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == _active.Default)))
.ToArray();
Task.WaitAll(tasks);
}
private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? newCollection, string displayName)
{
if (type is CollectionType.Inactive)
return;
var isDefault = type is CollectionType.Default;
if (newCollection?.Index > ModCollection.Empty.Index)
{
newCollection.CreateCache(isDefault);
_cache.TryAdd(newCollection, newCollection._cache!);
}
RemoveCache(old);
}
private void OnModChangeRemoval(ModPathChangeType type, Mod mod, DirectoryInfo? oldModPath, DirectoryInfo? newModPath)
{
switch (type)
{
case ModPathChangeType.Deleted:
case ModPathChangeType.StartingReload:
foreach (var collection in _cache.Keys.Where(c => c[mod.Index].Settings?.Enabled == true))
collection._cache!.RemoveMod(mod, true);
break;
case ModPathChangeType.Moved:
foreach (var collection in _cache.Keys.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
collection._cache!.ReloadMod(mod, true);
break;
}
}
private void OnModChangeAddition(ModPathChangeType type, Mod mod, DirectoryInfo? oldModPath, DirectoryInfo? newModPath)
{
if (type is not (ModPathChangeType.Added or ModPathChangeType.Reloaded))
return;
foreach (var collection in _cache.Keys.Where(c => c[mod.Index].Settings?.Enabled == true))
collection._cache!.AddMod(mod, true);
}
/// <summary> Apply a mod change to all collections with a cache. </summary>
private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed)
=> TempModManager.OnGlobalModChange(_cache.Keys, mod, created, removed);
/// <summary> Remove a cache from a collection if it is active. </summary>
private void RemoveCache(ModCollection? collection)
{
if (collection != null
&& collection.Index > ModCollection.Empty.Index
&& collection.Index != _active.Default.Index
&& collection.Index != _active.Interface.Index
&& collection.Index != _active.Current.Index
&& _active.SpecialAssignments.All(c => c.Value.Index != collection.Index)
&& _active.Individuals.All(c => c.Collection.Index != collection.Index))
{
_cache.Remove(collection);
collection.ClearCache();
}
}
/// <summary> Prepare Changes by removing mods from caches with collections or add or reload mods. </summary>
private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
{
if (type is ModOptionChangeType.PrepareChange)
{
foreach (var collection in _cache.Keys.Where(collection => collection[mod.Index].Settings is { Enabled: true }))
collection._cache!.RemoveMod(mod, false);
return;
}
type.HandlingInfo(out _, out var recomputeList, out var reload);
if (!recomputeList)
return;
foreach (var collection in _cache.Keys.Where(collection => collection[mod.Index].Settings is { Enabled: true }))
{
if (reload)
collection._cache!.ReloadMod(mod, true);
else
collection._cache!.AddMod(mod, true);
}
}
}

View file

@ -0,0 +1,20 @@
namespace Penumbra.Collections.Manager;
public class CollectionManager
{
public readonly CollectionStorage Storage;
public readonly ActiveCollections Active;
public readonly InheritanceManager Inheritances;
public readonly CollectionCacheManager Caches;
public readonly TempCollectionManager Temp;
public CollectionManager(CollectionStorage storage, ActiveCollections active, InheritanceManager inheritances,
CollectionCacheManager caches, TempCollectionManager temp)
{
Storage = storage;
Active = active;
Inheritances = inheritances;
Caches = caches;
Temp = temp;
}
}

View file

@ -0,0 +1,306 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using OtterGui.Filesystem;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Collections.Manager;
public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
{
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
/// <remarks> The empty collection is always available at Index 0. </remarks>
private readonly List<ModCollection> _collections = new()
{
ModCollection.Empty,
};
public readonly ModCollection DefaultNamed;
/// <summary> Default enumeration skips the empty collection. </summary>
public IEnumerator<ModCollection> GetEnumerator()
=> _collections.Skip(1).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public IEnumerator<ModCollection> GetEnumeratorWithEmpty()
=> _collections.GetEnumerator();
public int Count
=> _collections.Count;
public ModCollection this[int index]
=> _collections[index];
/// <summary> Find a collection by its name. If the name is empty or None, the empty collection is returned. </summary>
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
{
if (name.Length != 0)
return _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
collection = ModCollection.Empty;
return true;
}
public CollectionStorage(CommunicatorService communicator, SaveService saveService)
{
_communicator = communicator;
_saveService = saveService;
_communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted);
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished);
_communicator.ModPathChanged.Subscribe(OnModPathChange, 10);
_communicator.ModOptionChanged.Subscribe(OnModOptionChange, 100);
ReadCollections(out DefaultNamed);
}
public void Dispose()
{
_communicator.ModDiscoveryStarted.Unsubscribe(OnModDiscoveryStarted);
_communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished);
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
}
/// <summary>
/// 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. Also returns the fixed name.
/// </summary>
public bool CanAddCollection(string name, out string fixedName)
{
if (!IsValidName(name))
{
fixedName = string.Empty;
return false;
}
name = name.ToLowerInvariant();
if (name.Length == 0
|| name == ModCollection.Empty.Name.ToLowerInvariant()
|| _collections.Any(c => c.Name.ToLowerInvariant() == name))
{
fixedName = string.Empty;
return false;
}
fixedName = name;
return true;
}
/// <summary>
/// Add a new collection of the given name.
/// If duplicate is not-null, the new collection will be a duplicate of it.
/// If the name of the collection would result in an already existing filename, skip it.
/// Returns true if the collection was successfully created and fires a Inactive event.
/// Also sets the current collection to the new collection afterwards.
/// </summary>
public bool AddCollection(string name, ModCollection? duplicate)
{
if (!CanAddCollection(name, out var fixedName))
{
Penumbra.ChatService.NotificationMessage(
$"The new collection {name} would lead to the same path {fixedName} as one that already exists.", "Warning",
NotificationType.Warning);
return false;
}
var newCollection = duplicate?.Duplicate(name) ?? ModCollection.CreateNewEmpty(name);
newCollection.Index = _collections.Count;
_collections.Add(newCollection);
_saveService.ImmediateSave(newCollection);
Penumbra.ChatService.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", "Success",
NotificationType.Success);
_communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
return true;
}
/// <summary> Whether the given collection can be deleted. </summary>
public bool CanRemoveCollection(ModCollection collection)
=> collection.Index > ModCollection.Empty.Index && collection.Index < Count && collection.Index != DefaultNamed.Index;
/// <summary>
/// Remove the given collection if it exists and is neither the empty nor the default-named collection.
/// </summary>
public bool RemoveCollection(ModCollection collection)
{
if (collection.Index <= ModCollection.Empty.Index || collection.Index >= _collections.Count)
{
Penumbra.ChatService.NotificationMessage("Can not remove the empty collection.", "Error", NotificationType.Error);
return false;
}
if (collection.Index == DefaultNamed.Index)
{
Penumbra.ChatService.NotificationMessage("Can not remove the default collection.", "Error", NotificationType.Error);
return false;
}
_saveService.ImmediateDelete(collection);
_collections.RemoveAt(collection.Index);
// Update indices.
for (var i = collection.Index; i < Count; ++i)
_collections[i].Index = i;
Penumbra.ChatService.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", "Success", NotificationType.Success);
_communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
return true;
}
/// <summary> Stored after loading to be consumed and passed to the inheritance manager later. </summary>
private List<IReadOnlyList<string>>? _inheritancesByName = new();
/// <summary> Return an enumerable of collections and the collections they should inherit. </summary>
public IEnumerable<(ModCollection Collection, IReadOnlyList<ModCollection> Inheritance, bool LoadChanges)> ConsumeInheritanceNames()
{
if (_inheritancesByName == null)
throw new Exception("Inheritances were already consumed. This method can not be called twice.");
var inheritances = _inheritancesByName;
_inheritancesByName = null;
var list = new List<ModCollection>();
foreach (var (collection, inheritance) in _collections.Zip(inheritances))
{
list.Clear();
var changes = false;
foreach (var subCollectionName in inheritance)
{
if (ByName(subCollectionName, out var subCollection))
{
list.Add(subCollection);
}
else
{
Penumbra.ChatService.NotificationMessage(
$"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", "Warning",
NotificationType.Warning);
changes = true;
}
}
yield return (collection, list, changes);
}
}
/// <summary>
/// Check if a name is valid to use for a collection.
/// Does not check for uniqueness.
/// </summary>
private static bool IsValidName(string name)
=> name.Length > 0 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath());
/// <summary>
/// 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.
/// </summary>
private void ReadCollections(out ModCollection defaultNamedCollection)
{
_inheritancesByName?.Clear();
_inheritancesByName?.Add(Array.Empty<string>()); // None.
foreach (var file in _saveService.FileNames.CollectionFiles)
{
var collection = ModCollection.LoadFromFile(file, out var inheritance);
if (collection == null || collection.Name.Length == 0)
continue;
if (ByName(collection.Name, out _))
{
Penumbra.ChatService.NotificationMessage($"Duplicate collection found: {collection.Name} already exists. Import skipped.",
"Warning", NotificationType.Warning);
continue;
}
var correctName = _saveService.FileNames.CollectionFile(collection);
if (file.FullName != correctName)
Penumbra.ChatService.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", "Warning",
NotificationType.Warning);
_inheritancesByName?.Add(inheritance);
collection.Index = _collections.Count;
_collections.Add(collection);
}
defaultNamedCollection = SetDefaultNamedCollection();
}
/// <summary>
/// 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.
/// </summary>
private ModCollection SetDefaultNamedCollection()
{
if (ByName(ModCollection.DefaultCollectionName, out var collection))
return collection;
if (AddCollection(ModCollection.DefaultCollectionName, null))
return _collections[^1];
Penumbra.ChatService.NotificationMessage(
$"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.", "Error",
NotificationType.Error);
return Count > 1 ? _collections[1] : _collections[0];
}
/// <summary> Move all settings in all collections to unused settings. </summary>
private void OnModDiscoveryStarted()
{
foreach (var collection in this)
collection.PrepareModDiscovery();
}
/// <summary> Restore all settings in all collections to mods. </summary>
private void OnModDiscoveryFinished()
{
// Re-apply all mod settings.
foreach (var collection in this)
collection.ApplyModSettings();
}
/// <summary> Add or remove a mod from all collections, or re-save all collections where the mod has settings. </summary>
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory)
{
switch (type)
{
case ModPathChangeType.Added:
foreach (var collection in this)
collection.AddMod(mod);
break;
case ModPathChangeType.Deleted:
foreach (var collection in this)
collection.RemoveMod(mod, mod.Index);
break;
case ModPathChangeType.Moved:
foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
_saveService.QueueSave(collection);
break;
}
}
/// <summary> Save all collections where the mod has settings and the change requires saving. </summary>
private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
{
type.HandlingInfo(out var requiresSaving, out _, out _);
if (!requiresSaving)
return;
foreach (var collection in this)
{
if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false)
_saveService.QueueSave(collection);
}
}
}

View file

@ -0,0 +1,583 @@
using Penumbra.GameData.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Penumbra.Collections.Manager;
public enum CollectionType : byte
{
// Special Collections
Yourself = Api.Enums.ApiCollectionType.Yourself,
MalePlayerCharacter = Api.Enums.ApiCollectionType.MalePlayerCharacter,
FemalePlayerCharacter = Api.Enums.ApiCollectionType.FemalePlayerCharacter,
MaleNonPlayerCharacter = Api.Enums.ApiCollectionType.MaleNonPlayerCharacter,
FemaleNonPlayerCharacter = Api.Enums.ApiCollectionType.FemaleNonPlayerCharacter,
NonPlayerChild = Api.Enums.ApiCollectionType.NonPlayerChild,
NonPlayerElderly = Api.Enums.ApiCollectionType.NonPlayerElderly,
MaleMidlander = Api.Enums.ApiCollectionType.MaleMidlander,
FemaleMidlander = Api.Enums.ApiCollectionType.FemaleMidlander,
MaleHighlander = Api.Enums.ApiCollectionType.MaleHighlander,
FemaleHighlander = Api.Enums.ApiCollectionType.FemaleHighlander,
MaleWildwood = Api.Enums.ApiCollectionType.MaleWildwood,
FemaleWildwood = Api.Enums.ApiCollectionType.FemaleWildwood,
MaleDuskwight = Api.Enums.ApiCollectionType.MaleDuskwight,
FemaleDuskwight = Api.Enums.ApiCollectionType.FemaleDuskwight,
MalePlainsfolk = Api.Enums.ApiCollectionType.MalePlainsfolk,
FemalePlainsfolk = Api.Enums.ApiCollectionType.FemalePlainsfolk,
MaleDunesfolk = Api.Enums.ApiCollectionType.MaleDunesfolk,
FemaleDunesfolk = Api.Enums.ApiCollectionType.FemaleDunesfolk,
MaleSeekerOfTheSun = Api.Enums.ApiCollectionType.MaleSeekerOfTheSun,
FemaleSeekerOfTheSun = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSun,
MaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoon,
FemaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoon,
MaleSeawolf = Api.Enums.ApiCollectionType.MaleSeawolf,
FemaleSeawolf = Api.Enums.ApiCollectionType.FemaleSeawolf,
MaleHellsguard = Api.Enums.ApiCollectionType.MaleHellsguard,
FemaleHellsguard = Api.Enums.ApiCollectionType.FemaleHellsguard,
MaleRaen = Api.Enums.ApiCollectionType.MaleRaen,
FemaleRaen = Api.Enums.ApiCollectionType.FemaleRaen,
MaleXaela = Api.Enums.ApiCollectionType.MaleXaela,
FemaleXaela = Api.Enums.ApiCollectionType.FemaleXaela,
MaleHelion = Api.Enums.ApiCollectionType.MaleHelion,
FemaleHelion = Api.Enums.ApiCollectionType.FemaleHelion,
MaleLost = Api.Enums.ApiCollectionType.MaleLost,
FemaleLost = Api.Enums.ApiCollectionType.FemaleLost,
MaleRava = Api.Enums.ApiCollectionType.MaleRava,
FemaleRava = Api.Enums.ApiCollectionType.FemaleRava,
MaleVeena = Api.Enums.ApiCollectionType.MaleVeena,
FemaleVeena = Api.Enums.ApiCollectionType.FemaleVeena,
MaleMidlanderNpc = Api.Enums.ApiCollectionType.MaleMidlanderNpc,
FemaleMidlanderNpc = Api.Enums.ApiCollectionType.FemaleMidlanderNpc,
MaleHighlanderNpc = Api.Enums.ApiCollectionType.MaleHighlanderNpc,
FemaleHighlanderNpc = Api.Enums.ApiCollectionType.FemaleHighlanderNpc,
MaleWildwoodNpc = Api.Enums.ApiCollectionType.MaleWildwoodNpc,
FemaleWildwoodNpc = Api.Enums.ApiCollectionType.FemaleWildwoodNpc,
MaleDuskwightNpc = Api.Enums.ApiCollectionType.MaleDuskwightNpc,
FemaleDuskwightNpc = Api.Enums.ApiCollectionType.FemaleDuskwightNpc,
MalePlainsfolkNpc = Api.Enums.ApiCollectionType.MalePlainsfolkNpc,
FemalePlainsfolkNpc = Api.Enums.ApiCollectionType.FemalePlainsfolkNpc,
MaleDunesfolkNpc = Api.Enums.ApiCollectionType.MaleDunesfolkNpc,
FemaleDunesfolkNpc = Api.Enums.ApiCollectionType.FemaleDunesfolkNpc,
MaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.MaleSeekerOfTheSunNpc,
FemaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSunNpc,
MaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoonNpc,
FemaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoonNpc,
MaleSeawolfNpc = Api.Enums.ApiCollectionType.MaleSeawolfNpc,
FemaleSeawolfNpc = Api.Enums.ApiCollectionType.FemaleSeawolfNpc,
MaleHellsguardNpc = Api.Enums.ApiCollectionType.MaleHellsguardNpc,
FemaleHellsguardNpc = Api.Enums.ApiCollectionType.FemaleHellsguardNpc,
MaleRaenNpc = Api.Enums.ApiCollectionType.MaleRaenNpc,
FemaleRaenNpc = Api.Enums.ApiCollectionType.FemaleRaenNpc,
MaleXaelaNpc = Api.Enums.ApiCollectionType.MaleXaelaNpc,
FemaleXaelaNpc = Api.Enums.ApiCollectionType.FemaleXaelaNpc,
MaleHelionNpc = Api.Enums.ApiCollectionType.MaleHelionNpc,
FemaleHelionNpc = Api.Enums.ApiCollectionType.FemaleHelionNpc,
MaleLostNpc = Api.Enums.ApiCollectionType.MaleLostNpc,
FemaleLostNpc = Api.Enums.ApiCollectionType.FemaleLostNpc,
MaleRavaNpc = Api.Enums.ApiCollectionType.MaleRavaNpc,
FemaleRavaNpc = Api.Enums.ApiCollectionType.FemaleRavaNpc,
MaleVeenaNpc = Api.Enums.ApiCollectionType.MaleVeenaNpc,
FemaleVeenaNpc = Api.Enums.ApiCollectionType.FemaleVeenaNpc,
Default = Api.Enums.ApiCollectionType.Default, // The default collection was changed
Interface = Api.Enums.ApiCollectionType.Interface, // The ui collection was changed
Current = Api.Enums.ApiCollectionType.Current, // The current collection was changed
Individual, // An individual collection was changed
Inactive, // A collection was added or removed
Temporary, // A temporary collections was set or deleted via IPC
}
public static class CollectionTypeExtensions
{
public static bool IsSpecial(this CollectionType collectionType)
=> collectionType < CollectionType.Default;
public static readonly (CollectionType, string, string)[] Special = Enum.GetValues<CollectionType>()
.Where(IsSpecial)
.Select(s => (s, s.ToName(), s.ToDescription()))
.ToArray();
public static CollectionType FromParts(Gender gender, bool npc)
{
gender = gender switch
{
Gender.MaleNpc => Gender.Male,
Gender.FemaleNpc => Gender.Female,
_ => gender,
};
return (gender, npc) switch
{
(Gender.Male, false) => CollectionType.MalePlayerCharacter,
(Gender.Female, false) => CollectionType.FemalePlayerCharacter,
(Gender.Male, true) => CollectionType.MaleNonPlayerCharacter,
(Gender.Female, true) => CollectionType.FemaleNonPlayerCharacter,
_ => CollectionType.Inactive,
};
}
// @formatter:off
private static readonly IReadOnlyList<CollectionType> DefaultList = new[] { CollectionType.Default };
private static readonly IReadOnlyList<CollectionType> MalePlayerList = new[] { CollectionType.MalePlayerCharacter, CollectionType.Default };
private static readonly IReadOnlyList<CollectionType> FemalePlayerList = new[] { CollectionType.FemalePlayerCharacter, CollectionType.Default };
private static readonly IReadOnlyList<CollectionType> MaleNpcList = new[] { CollectionType.MaleNonPlayerCharacter, CollectionType.Default };
private static readonly IReadOnlyList<CollectionType> FemaleNpcList = new[] { CollectionType.FemaleNonPlayerCharacter, CollectionType.Default };
// @formatter:on
/// <summary> A list of definite redundancy possibilities. </summary>
public static IReadOnlyList<CollectionType> InheritanceOrder(this CollectionType collectionType)
=> collectionType switch
{
CollectionType.Yourself => DefaultList,
CollectionType.MalePlayerCharacter => DefaultList,
CollectionType.FemalePlayerCharacter => DefaultList,
CollectionType.MaleNonPlayerCharacter => DefaultList,
CollectionType.FemaleNonPlayerCharacter => DefaultList,
CollectionType.MaleMidlander => MalePlayerList,
CollectionType.FemaleMidlander => FemalePlayerList,
CollectionType.MaleHighlander => MalePlayerList,
CollectionType.FemaleHighlander => FemalePlayerList,
CollectionType.MaleWildwood => MalePlayerList,
CollectionType.FemaleWildwood => FemalePlayerList,
CollectionType.MaleDuskwight => MalePlayerList,
CollectionType.FemaleDuskwight => FemalePlayerList,
CollectionType.MalePlainsfolk => MalePlayerList,
CollectionType.FemalePlainsfolk => FemalePlayerList,
CollectionType.MaleDunesfolk => MalePlayerList,
CollectionType.FemaleDunesfolk => FemalePlayerList,
CollectionType.MaleSeekerOfTheSun => MalePlayerList,
CollectionType.FemaleSeekerOfTheSun => FemalePlayerList,
CollectionType.MaleKeeperOfTheMoon => MalePlayerList,
CollectionType.FemaleKeeperOfTheMoon => FemalePlayerList,
CollectionType.MaleSeawolf => MalePlayerList,
CollectionType.FemaleSeawolf => FemalePlayerList,
CollectionType.MaleHellsguard => MalePlayerList,
CollectionType.FemaleHellsguard => FemalePlayerList,
CollectionType.MaleRaen => MalePlayerList,
CollectionType.FemaleRaen => FemalePlayerList,
CollectionType.MaleXaela => MalePlayerList,
CollectionType.FemaleXaela => FemalePlayerList,
CollectionType.MaleHelion => MalePlayerList,
CollectionType.FemaleHelion => FemalePlayerList,
CollectionType.MaleLost => MalePlayerList,
CollectionType.FemaleLost => FemalePlayerList,
CollectionType.MaleRava => MalePlayerList,
CollectionType.FemaleRava => FemalePlayerList,
CollectionType.MaleVeena => MalePlayerList,
CollectionType.FemaleVeena => FemalePlayerList,
CollectionType.MaleMidlanderNpc => MaleNpcList,
CollectionType.FemaleMidlanderNpc => FemaleNpcList,
CollectionType.MaleHighlanderNpc => MaleNpcList,
CollectionType.FemaleHighlanderNpc => FemaleNpcList,
CollectionType.MaleWildwoodNpc => MaleNpcList,
CollectionType.FemaleWildwoodNpc => FemaleNpcList,
CollectionType.MaleDuskwightNpc => MaleNpcList,
CollectionType.FemaleDuskwightNpc => FemaleNpcList,
CollectionType.MalePlainsfolkNpc => MaleNpcList,
CollectionType.FemalePlainsfolkNpc => FemaleNpcList,
CollectionType.MaleDunesfolkNpc => MaleNpcList,
CollectionType.FemaleDunesfolkNpc => FemaleNpcList,
CollectionType.MaleSeekerOfTheSunNpc => MaleNpcList,
CollectionType.FemaleSeekerOfTheSunNpc => FemaleNpcList,
CollectionType.MaleKeeperOfTheMoonNpc => MaleNpcList,
CollectionType.FemaleKeeperOfTheMoonNpc => FemaleNpcList,
CollectionType.MaleSeawolfNpc => MaleNpcList,
CollectionType.FemaleSeawolfNpc => FemaleNpcList,
CollectionType.MaleHellsguardNpc => MaleNpcList,
CollectionType.FemaleHellsguardNpc => FemaleNpcList,
CollectionType.MaleRaenNpc => MaleNpcList,
CollectionType.FemaleRaenNpc => FemaleNpcList,
CollectionType.MaleXaelaNpc => MaleNpcList,
CollectionType.FemaleXaelaNpc => FemaleNpcList,
CollectionType.MaleHelionNpc => MaleNpcList,
CollectionType.FemaleHelionNpc => FemaleNpcList,
CollectionType.MaleLostNpc => MaleNpcList,
CollectionType.FemaleLostNpc => FemaleNpcList,
CollectionType.MaleRavaNpc => MaleNpcList,
CollectionType.FemaleRavaNpc => FemaleNpcList,
CollectionType.MaleVeenaNpc => MaleNpcList,
CollectionType.FemaleVeenaNpc => FemaleNpcList,
CollectionType.Individual => DefaultList,
_ => Array.Empty<CollectionType>(),
};
public static CollectionType FromParts(SubRace race, Gender gender, bool npc)
{
gender = gender switch
{
Gender.MaleNpc => Gender.Male,
Gender.FemaleNpc => Gender.Female,
_ => gender,
};
return (race, gender, npc) switch
{
(SubRace.Midlander, Gender.Male, false) => CollectionType.MaleMidlander,
(SubRace.Highlander, Gender.Male, false) => CollectionType.MaleHighlander,
(SubRace.Wildwood, Gender.Male, false) => CollectionType.MaleWildwood,
(SubRace.Duskwight, Gender.Male, false) => CollectionType.MaleDuskwight,
(SubRace.Plainsfolk, Gender.Male, false) => CollectionType.MalePlainsfolk,
(SubRace.Dunesfolk, Gender.Male, false) => CollectionType.MaleDunesfolk,
(SubRace.SeekerOfTheSun, Gender.Male, false) => CollectionType.MaleSeekerOfTheSun,
(SubRace.KeeperOfTheMoon, Gender.Male, false) => CollectionType.MaleKeeperOfTheMoon,
(SubRace.Seawolf, Gender.Male, false) => CollectionType.MaleSeawolf,
(SubRace.Hellsguard, Gender.Male, false) => CollectionType.MaleHellsguard,
(SubRace.Raen, Gender.Male, false) => CollectionType.MaleRaen,
(SubRace.Xaela, Gender.Male, false) => CollectionType.MaleXaela,
(SubRace.Helion, Gender.Male, false) => CollectionType.MaleHelion,
(SubRace.Lost, Gender.Male, false) => CollectionType.MaleLost,
(SubRace.Rava, Gender.Male, false) => CollectionType.MaleRava,
(SubRace.Veena, Gender.Male, false) => CollectionType.MaleVeena,
(SubRace.Midlander, Gender.Female, false) => CollectionType.FemaleMidlander,
(SubRace.Highlander, Gender.Female, false) => CollectionType.FemaleHighlander,
(SubRace.Wildwood, Gender.Female, false) => CollectionType.FemaleWildwood,
(SubRace.Duskwight, Gender.Female, false) => CollectionType.FemaleDuskwight,
(SubRace.Plainsfolk, Gender.Female, false) => CollectionType.FemalePlainsfolk,
(SubRace.Dunesfolk, Gender.Female, false) => CollectionType.FemaleDunesfolk,
(SubRace.SeekerOfTheSun, Gender.Female, false) => CollectionType.FemaleSeekerOfTheSun,
(SubRace.KeeperOfTheMoon, Gender.Female, false) => CollectionType.FemaleKeeperOfTheMoon,
(SubRace.Seawolf, Gender.Female, false) => CollectionType.FemaleSeawolf,
(SubRace.Hellsguard, Gender.Female, false) => CollectionType.FemaleHellsguard,
(SubRace.Raen, Gender.Female, false) => CollectionType.FemaleRaen,
(SubRace.Xaela, Gender.Female, false) => CollectionType.FemaleXaela,
(SubRace.Helion, Gender.Female, false) => CollectionType.FemaleHelion,
(SubRace.Lost, Gender.Female, false) => CollectionType.FemaleLost,
(SubRace.Rava, Gender.Female, false) => CollectionType.FemaleRava,
(SubRace.Veena, Gender.Female, false) => CollectionType.FemaleVeena,
(SubRace.Midlander, Gender.Male, true) => CollectionType.MaleMidlanderNpc,
(SubRace.Highlander, Gender.Male, true) => CollectionType.MaleHighlanderNpc,
(SubRace.Wildwood, Gender.Male, true) => CollectionType.MaleWildwoodNpc,
(SubRace.Duskwight, Gender.Male, true) => CollectionType.MaleDuskwightNpc,
(SubRace.Plainsfolk, Gender.Male, true) => CollectionType.MalePlainsfolkNpc,
(SubRace.Dunesfolk, Gender.Male, true) => CollectionType.MaleDunesfolkNpc,
(SubRace.SeekerOfTheSun, Gender.Male, true) => CollectionType.MaleSeekerOfTheSunNpc,
(SubRace.KeeperOfTheMoon, Gender.Male, true) => CollectionType.MaleKeeperOfTheMoonNpc,
(SubRace.Seawolf, Gender.Male, true) => CollectionType.MaleSeawolfNpc,
(SubRace.Hellsguard, Gender.Male, true) => CollectionType.MaleHellsguardNpc,
(SubRace.Raen, Gender.Male, true) => CollectionType.MaleRaenNpc,
(SubRace.Xaela, Gender.Male, true) => CollectionType.MaleXaelaNpc,
(SubRace.Helion, Gender.Male, true) => CollectionType.MaleHelionNpc,
(SubRace.Lost, Gender.Male, true) => CollectionType.MaleLostNpc,
(SubRace.Rava, Gender.Male, true) => CollectionType.MaleRavaNpc,
(SubRace.Veena, Gender.Male, true) => CollectionType.MaleVeenaNpc,
(SubRace.Midlander, Gender.Female, true) => CollectionType.FemaleMidlanderNpc,
(SubRace.Highlander, Gender.Female, true) => CollectionType.FemaleHighlanderNpc,
(SubRace.Wildwood, Gender.Female, true) => CollectionType.FemaleWildwoodNpc,
(SubRace.Duskwight, Gender.Female, true) => CollectionType.FemaleDuskwightNpc,
(SubRace.Plainsfolk, Gender.Female, true) => CollectionType.FemalePlainsfolkNpc,
(SubRace.Dunesfolk, Gender.Female, true) => CollectionType.FemaleDunesfolkNpc,
(SubRace.SeekerOfTheSun, Gender.Female, true) => CollectionType.FemaleSeekerOfTheSunNpc,
(SubRace.KeeperOfTheMoon, Gender.Female, true) => CollectionType.FemaleKeeperOfTheMoonNpc,
(SubRace.Seawolf, Gender.Female, true) => CollectionType.FemaleSeawolfNpc,
(SubRace.Hellsguard, Gender.Female, true) => CollectionType.FemaleHellsguardNpc,
(SubRace.Raen, Gender.Female, true) => CollectionType.FemaleRaenNpc,
(SubRace.Xaela, Gender.Female, true) => CollectionType.FemaleXaelaNpc,
(SubRace.Helion, Gender.Female, true) => CollectionType.FemaleHelionNpc,
(SubRace.Lost, Gender.Female, true) => CollectionType.FemaleLostNpc,
(SubRace.Rava, Gender.Female, true) => CollectionType.FemaleRavaNpc,
(SubRace.Veena, Gender.Female, true) => CollectionType.FemaleVeenaNpc,
_ => CollectionType.Inactive,
};
}
public static bool TryParse(string text, out CollectionType type)
{
if (Enum.TryParse(text, true, out type))
{
return type is not CollectionType.Inactive and not CollectionType.Temporary;
}
if (string.Equals(text, "character", StringComparison.OrdinalIgnoreCase))
{
type = CollectionType.Individual;
return true;
}
if (string.Equals(text, "base", StringComparison.OrdinalIgnoreCase))
{
type = CollectionType.Default;
return true;
}
if (string.Equals(text, "ui", StringComparison.OrdinalIgnoreCase))
{
type = CollectionType.Interface;
return true;
}
if (string.Equals(text, "selected", StringComparison.OrdinalIgnoreCase))
{
type = CollectionType.Current;
return true;
}
foreach (var t in Enum.GetValues<CollectionType>())
{
if (t is CollectionType.Inactive or CollectionType.Temporary)
{
continue;
}
if (string.Equals(text, t.ToName(), StringComparison.OrdinalIgnoreCase))
{
type = t;
return true;
}
}
return false;
}
public static string ToName(this CollectionType collectionType)
=> collectionType switch
{
CollectionType.Yourself => "Your Character",
CollectionType.NonPlayerChild => "Non-Player Children",
CollectionType.NonPlayerElderly => "Non-Player Elderly",
CollectionType.MalePlayerCharacter => "Male Player Characters",
CollectionType.MaleNonPlayerCharacter => "Male Non-Player Characters",
CollectionType.MaleMidlander => $"Male {SubRace.Midlander.ToName()}",
CollectionType.MaleHighlander => $"Male {SubRace.Highlander.ToName()}",
CollectionType.MaleWildwood => $"Male {SubRace.Wildwood.ToName()}",
CollectionType.MaleDuskwight => $"Male {SubRace.Duskwight.ToName()}",
CollectionType.MalePlainsfolk => $"Male {SubRace.Plainsfolk.ToName()}",
CollectionType.MaleDunesfolk => $"Male {SubRace.Dunesfolk.ToName()}",
CollectionType.MaleSeekerOfTheSun => $"Male {SubRace.SeekerOfTheSun.ToName()}",
CollectionType.MaleKeeperOfTheMoon => $"Male {SubRace.KeeperOfTheMoon.ToName()}",
CollectionType.MaleSeawolf => $"Male {SubRace.Seawolf.ToName()}",
CollectionType.MaleHellsguard => $"Male {SubRace.Hellsguard.ToName()}",
CollectionType.MaleRaen => $"Male {SubRace.Raen.ToName()}",
CollectionType.MaleXaela => $"Male {SubRace.Xaela.ToName()}",
CollectionType.MaleHelion => $"Male {SubRace.Helion.ToName()}",
CollectionType.MaleLost => $"Male {SubRace.Lost.ToName()}",
CollectionType.MaleRava => $"Male {SubRace.Rava.ToName()}",
CollectionType.MaleVeena => $"Male {SubRace.Veena.ToName()}",
CollectionType.MaleMidlanderNpc => $"Male {SubRace.Midlander.ToName()} (NPC)",
CollectionType.MaleHighlanderNpc => $"Male {SubRace.Highlander.ToName()} (NPC)",
CollectionType.MaleWildwoodNpc => $"Male {SubRace.Wildwood.ToName()} (NPC)",
CollectionType.MaleDuskwightNpc => $"Male {SubRace.Duskwight.ToName()} (NPC)",
CollectionType.MalePlainsfolkNpc => $"Male {SubRace.Plainsfolk.ToName()} (NPC)",
CollectionType.MaleDunesfolkNpc => $"Male {SubRace.Dunesfolk.ToName()} (NPC)",
CollectionType.MaleSeekerOfTheSunNpc => $"Male {SubRace.SeekerOfTheSun.ToName()} (NPC)",
CollectionType.MaleKeeperOfTheMoonNpc => $"Male {SubRace.KeeperOfTheMoon.ToName()} (NPC)",
CollectionType.MaleSeawolfNpc => $"Male {SubRace.Seawolf.ToName()} (NPC)",
CollectionType.MaleHellsguardNpc => $"Male {SubRace.Hellsguard.ToName()} (NPC)",
CollectionType.MaleRaenNpc => $"Male {SubRace.Raen.ToName()} (NPC)",
CollectionType.MaleXaelaNpc => $"Male {SubRace.Xaela.ToName()} (NPC)",
CollectionType.MaleHelionNpc => $"Male {SubRace.Helion.ToName()} (NPC)",
CollectionType.MaleLostNpc => $"Male {SubRace.Lost.ToName()} (NPC)",
CollectionType.MaleRavaNpc => $"Male {SubRace.Rava.ToName()} (NPC)",
CollectionType.MaleVeenaNpc => $"Male {SubRace.Veena.ToName()} (NPC)",
CollectionType.FemalePlayerCharacter => "Female Player Characters",
CollectionType.FemaleNonPlayerCharacter => "Female Non-Player Characters",
CollectionType.FemaleMidlander => $"Female {SubRace.Midlander.ToName()}",
CollectionType.FemaleHighlander => $"Female {SubRace.Highlander.ToName()}",
CollectionType.FemaleWildwood => $"Female {SubRace.Wildwood.ToName()}",
CollectionType.FemaleDuskwight => $"Female {SubRace.Duskwight.ToName()}",
CollectionType.FemalePlainsfolk => $"Female {SubRace.Plainsfolk.ToName()}",
CollectionType.FemaleDunesfolk => $"Female {SubRace.Dunesfolk.ToName()}",
CollectionType.FemaleSeekerOfTheSun => $"Female {SubRace.SeekerOfTheSun.ToName()}",
CollectionType.FemaleKeeperOfTheMoon => $"Female {SubRace.KeeperOfTheMoon.ToName()}",
CollectionType.FemaleSeawolf => $"Female {SubRace.Seawolf.ToName()}",
CollectionType.FemaleHellsguard => $"Female {SubRace.Hellsguard.ToName()}",
CollectionType.FemaleRaen => $"Female {SubRace.Raen.ToName()}",
CollectionType.FemaleXaela => $"Female {SubRace.Xaela.ToName()}",
CollectionType.FemaleHelion => $"Female {SubRace.Helion.ToName()}",
CollectionType.FemaleLost => $"Female {SubRace.Lost.ToName()}",
CollectionType.FemaleRava => $"Female {SubRace.Rava.ToName()}",
CollectionType.FemaleVeena => $"Female {SubRace.Veena.ToName()}",
CollectionType.FemaleMidlanderNpc => $"Female {SubRace.Midlander.ToName()} (NPC)",
CollectionType.FemaleHighlanderNpc => $"Female {SubRace.Highlander.ToName()} (NPC)",
CollectionType.FemaleWildwoodNpc => $"Female {SubRace.Wildwood.ToName()} (NPC)",
CollectionType.FemaleDuskwightNpc => $"Female {SubRace.Duskwight.ToName()} (NPC)",
CollectionType.FemalePlainsfolkNpc => $"Female {SubRace.Plainsfolk.ToName()} (NPC)",
CollectionType.FemaleDunesfolkNpc => $"Female {SubRace.Dunesfolk.ToName()} (NPC)",
CollectionType.FemaleSeekerOfTheSunNpc => $"Female {SubRace.SeekerOfTheSun.ToName()} (NPC)",
CollectionType.FemaleKeeperOfTheMoonNpc => $"Female {SubRace.KeeperOfTheMoon.ToName()} (NPC)",
CollectionType.FemaleSeawolfNpc => $"Female {SubRace.Seawolf.ToName()} (NPC)",
CollectionType.FemaleHellsguardNpc => $"Female {SubRace.Hellsguard.ToName()} (NPC)",
CollectionType.FemaleRaenNpc => $"Female {SubRace.Raen.ToName()} (NPC)",
CollectionType.FemaleXaelaNpc => $"Female {SubRace.Xaela.ToName()} (NPC)",
CollectionType.FemaleHelionNpc => $"Female {SubRace.Helion.ToName()} (NPC)",
CollectionType.FemaleLostNpc => $"Female {SubRace.Lost.ToName()} (NPC)",
CollectionType.FemaleRavaNpc => $"Female {SubRace.Rava.ToName()} (NPC)",
CollectionType.FemaleVeenaNpc => $"Female {SubRace.Veena.ToName()} (NPC)",
CollectionType.Inactive => "Collection",
CollectionType.Default => "Default",
CollectionType.Interface => "Interface",
CollectionType.Individual => "Individual",
CollectionType.Current => "Current",
_ => string.Empty,
};
public static string ToDescription(this CollectionType collectionType)
=> collectionType switch
{
CollectionType.Yourself => "This collection applies to your own character, regardless of its name.\n"
+ "It takes precedence before all other collections except for explicitly named individual collections.",
CollectionType.NonPlayerChild =>
"This collection applies to all non-player characters with a child body-type.\n"
+ "It takes precedence before all other collections except for explicitly named individual collections.",
CollectionType.NonPlayerElderly =>
"This collection applies to all non-player characters with an elderly body-type.\n"
+ "It takes precedence before all other collections except for explicitly named individual collections.",
CollectionType.MalePlayerCharacter =>
"This collection applies to all male player characters that do not have a more specific character or racial collections associated.",
CollectionType.MaleNonPlayerCharacter =>
"This collection applies to all human male non-player characters except those explicitly named. It takes precedence before the default and racial collections.",
CollectionType.MaleMidlander =>
"This collection applies to all male player character Midlander Hyur that do not have a more specific character collection associated.",
CollectionType.MaleHighlander =>
"This collection applies to all male player character Highlander Hyur that do not have a more specific character collection associated.",
CollectionType.MaleWildwood =>
"This collection applies to all male player character Wildwood Elezen that do not have a more specific character collection associated.",
CollectionType.MaleDuskwight =>
"This collection applies to all male player character Duskwight Elezen that do not have a more specific character collection associated.",
CollectionType.MalePlainsfolk =>
"This collection applies to all male player character Plainsfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.MaleDunesfolk =>
"This collection applies to all male player character Dunesfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.MaleSeekerOfTheSun =>
"This collection applies to all male player character Seekers of the Sun that do not have a more specific character collection associated.",
CollectionType.MaleKeeperOfTheMoon =>
"This collection applies to all male player character Keepers of the Moon that do not have a more specific character collection associated.",
CollectionType.MaleSeawolf =>
"This collection applies to all male player character Sea Wolf Roegadyn that do not have a more specific character collection associated.",
CollectionType.MaleHellsguard =>
"This collection applies to all male player character Hellsguard Roegadyn that do not have a more specific character collection associated.",
CollectionType.MaleRaen =>
"This collection applies to all male player character Raen Au Ra that do not have a more specific character collection associated.",
CollectionType.MaleXaela =>
"This collection applies to all male player character Xaela Au Ra that do not have a more specific character collection associated.",
CollectionType.MaleHelion =>
"This collection applies to all male player character Helion Hrothgar that do not have a more specific character collection associated.",
CollectionType.MaleLost =>
"This collection applies to all male player character Lost Hrothgar that do not have a more specific character collection associated.",
CollectionType.MaleRava =>
"This collection applies to all male player character Rava Viera that do not have a more specific character collection associated.",
CollectionType.MaleVeena =>
"This collection applies to all male player character Veena Viera that do not have a more specific character collection associated.",
CollectionType.MaleMidlanderNpc =>
"This collection applies to all male non-player character Midlander Hyur that do not have a more specific character collection associated.",
CollectionType.MaleHighlanderNpc =>
"This collection applies to all male non-player character Highlander Hyur that do not have a more specific character collection associated.",
CollectionType.MaleWildwoodNpc =>
"This collection applies to all male non-player character Wildwood Elezen that do not have a more specific character collection associated.",
CollectionType.MaleDuskwightNpc =>
"This collection applies to all male non-player character Duskwight Elezen that do not have a more specific character collection associated.",
CollectionType.MalePlainsfolkNpc =>
"This collection applies to all male non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.MaleDunesfolkNpc =>
"This collection applies to all male non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.MaleSeekerOfTheSunNpc =>
"This collection applies to all male non-player character Seekers of the Sun that do not have a more specific character collection associated.",
CollectionType.MaleKeeperOfTheMoonNpc =>
"This collection applies to all male non-player character Keepers of the Moon that do not have a more specific character collection associated.",
CollectionType.MaleSeawolfNpc =>
"This collection applies to all male non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.",
CollectionType.MaleHellsguardNpc =>
"This collection applies to all male non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.",
CollectionType.MaleRaenNpc =>
"This collection applies to all male non-player character Raen Au Ra that do not have a more specific character collection associated.",
CollectionType.MaleXaelaNpc =>
"This collection applies to all male non-player character Xaela Au Ra that do not have a more specific character collection associated.",
CollectionType.MaleHelionNpc =>
"This collection applies to all male non-player character Helion Hrothgar that do not have a more specific character collection associated.",
CollectionType.MaleLostNpc =>
"This collection applies to all male non-player character Lost Hrothgar that do not have a more specific character collection associated.",
CollectionType.MaleRavaNpc =>
"This collection applies to all male non-player character Rava Viera that do not have a more specific character collection associated.",
CollectionType.MaleVeenaNpc =>
"This collection applies to all male non-player character Veena Viera that do not have a more specific character collection associated.",
CollectionType.FemalePlayerCharacter =>
"This collection applies to all female player characters that do not have a more specific character or racial collections associated.",
CollectionType.FemaleNonPlayerCharacter =>
"This collection applies to all human female non-player characters except those explicitly named. It takes precedence before the default and racial collections.",
CollectionType.FemaleMidlander =>
"This collection applies to all female player character Midlander Hyur that do not have a more specific character collection associated.",
CollectionType.FemaleHighlander =>
"This collection applies to all female player character Highlander Hyur that do not have a more specific character collection associated.",
CollectionType.FemaleWildwood =>
"This collection applies to all female player character Wildwood Elezen that do not have a more specific character collection associated.",
CollectionType.FemaleDuskwight =>
"This collection applies to all female player character Duskwight Elezen that do not have a more specific character collection associated.",
CollectionType.FemalePlainsfolk =>
"This collection applies to all female player character Plainsfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.FemaleDunesfolk =>
"This collection applies to all female player character Dunesfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.FemaleSeekerOfTheSun =>
"This collection applies to all female player character Seekers of the Sun that do not have a more specific character collection associated.",
CollectionType.FemaleKeeperOfTheMoon =>
"This collection applies to all female player character Keepers of the Moon that do not have a more specific character collection associated.",
CollectionType.FemaleSeawolf =>
"This collection applies to all female player character Sea Wolf Roegadyn that do not have a more specific character collection associated.",
CollectionType.FemaleHellsguard =>
"This collection applies to all female player character Hellsguard Roegadyn that do not have a more specific character collection associated.",
CollectionType.FemaleRaen =>
"This collection applies to all female player character Raen Au Ra that do not have a more specific character collection associated.",
CollectionType.FemaleXaela =>
"This collection applies to all female player character Xaela Au Ra that do not have a more specific character collection associated.",
CollectionType.FemaleHelion =>
"This collection applies to all female player character Helion Hrothgar that do not have a more specific character collection associated.",
CollectionType.FemaleLost =>
"This collection applies to all female player character Lost Hrothgar that do not have a more specific character collection associated.",
CollectionType.FemaleRava =>
"This collection applies to all female player character Rava Viera that do not have a more specific character collection associated.",
CollectionType.FemaleVeena =>
"This collection applies to all female player character Veena Viera that do not have a more specific character collection associated.",
CollectionType.FemaleMidlanderNpc =>
"This collection applies to all female non-player character Midlander Hyur that do not have a more specific character collection associated.",
CollectionType.FemaleHighlanderNpc =>
"This collection applies to all female non-player character Highlander Hyur that do not have a more specific character collection associated.",
CollectionType.FemaleWildwoodNpc =>
"This collection applies to all female non-player character Wildwood Elezen that do not have a more specific character collection associated.",
CollectionType.FemaleDuskwightNpc =>
"This collection applies to all female non-player character Duskwight Elezen that do not have a more specific character collection associated.",
CollectionType.FemalePlainsfolkNpc =>
"This collection applies to all female non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.FemaleDunesfolkNpc =>
"This collection applies to all female non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.",
CollectionType.FemaleSeekerOfTheSunNpc =>
"This collection applies to all female non-player character Seekers of the Sun that do not have a more specific character collection associated.",
CollectionType.FemaleKeeperOfTheMoonNpc =>
"This collection applies to all female non-player character Keepers of the Moon that do not have a more specific character collection associated.",
CollectionType.FemaleSeawolfNpc =>
"This collection applies to all female non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.",
CollectionType.FemaleHellsguardNpc =>
"This collection applies to all female non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.",
CollectionType.FemaleRaenNpc =>
"This collection applies to all female non-player character Raen Au Ra that do not have a more specific character collection associated.",
CollectionType.FemaleXaelaNpc =>
"This collection applies to all female non-player character Xaela Au Ra that do not have a more specific character collection associated.",
CollectionType.FemaleHelionNpc =>
"This collection applies to all female non-player character Helion Hrothgar that do not have a more specific character collection associated.",
CollectionType.FemaleLostNpc =>
"This collection applies to all female non-player character Lost Hrothgar that do not have a more specific character collection associated.",
CollectionType.FemaleRavaNpc =>
"This collection applies to all female non-player character Rava Viera that do not have a more specific character collection associated.",
CollectionType.FemaleVeenaNpc =>
"This collection applies to all female non-player character Veena Viera that do not have a more specific character collection associated.",
_ => string.Empty,
};
}

View file

@ -7,7 +7,7 @@ using Dalamud.Game.ClientState.Objects.Types;
using Penumbra.GameData.Actors;
using Penumbra.String;
namespace Penumbra.Collections;
namespace Penumbra.Collections.Manager;
public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) >
{

View file

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface.Internal.Notifications;
using Newtonsoft.Json.Linq;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.String;
namespace Penumbra.Collections.Manager;
public partial class IndividualCollections
{
public JArray ToJObject()
{
var ret = new JArray();
foreach (var (name, identifiers, collection) in Assignments)
{
var tmp = identifiers[0].ToJson();
tmp.Add("Collection", collection.Name);
tmp.Add("Display", name);
ret.Add(tmp);
}
return ret;
}
public bool ReadJObject(JArray? obj, CollectionStorage storage)
{
if (obj == null)
return true;
var changes = false;
foreach (var data in obj)
{
try
{
var identifier = _actorManager.FromJson(data as JObject);
var group = GetGroup(identifier);
if (group.Length == 0 || group.Any(i => !i.IsValid))
{
changes = true;
Penumbra.ChatService.NotificationMessage("Could not load an unknown individual collection, removed.", "Load Failure",
NotificationType.Warning);
continue;
}
var collectionName = data["Collection"]?.ToObject<string>() ?? string.Empty;
if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection))
{
changes = true;
Penumbra.ChatService.NotificationMessage(
$"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.",
"Load Failure",
NotificationType.Warning);
continue;
}
if (!Add(group, collection))
{
changes = true;
Penumbra.ChatService.NotificationMessage($"Could not add an individual collection for {identifier}, removed.",
"Load Failure",
NotificationType.Warning);
}
}
catch (Exception e)
{
changes = true;
Penumbra.ChatService.NotificationMessage($"Could not load an unknown individual collection, removed:\n{e}", "Load Failure",
NotificationType.Error);
}
}
return changes;
}
internal void Migrate0To1(Dictionary<string, ModCollection> old)
{
static bool FindDataId(string name, IReadOnlyDictionary<uint, string> data, out uint dataId)
{
var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase),
new KeyValuePair<uint, string>(uint.MaxValue, string.Empty));
dataId = kvp.Key;
return kvp.Value.Length > 0;
}
foreach (var (name, collection) in old)
{
var kind = ObjectKind.None;
var lowerName = name.ToLowerInvariant();
// Prefer matching NPC names, fewer false positives than preferring players.
if (FindDataId(lowerName, _actorManager.Data.Companions, out var dataId))
kind = ObjectKind.Companion;
else if (FindDataId(lowerName, _actorManager.Data.Mounts, out dataId))
kind = ObjectKind.MountType;
else if (FindDataId(lowerName, _actorManager.Data.BNpcs, out dataId))
kind = ObjectKind.BattleNpc;
else if (FindDataId(lowerName, _actorManager.Data.ENpcs, out dataId))
kind = ObjectKind.EventNpc;
var identifier = _actorManager.CreateNpc(kind, dataId);
if (identifier.IsValid)
{
// If the name corresponds to a valid npc, add it as a group. If this fails, notify users.
var group = GetGroup(identifier);
var ids = string.Join(", ", group.Select(i => i.DataId.ToString()));
if (Add($"{_actorManager.Data.ToName(kind, dataId)} ({kind.ToName()})", group, collection))
Penumbra.Log.Information($"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}].");
else
Penumbra.ChatService.NotificationMessage(
$"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.",
"Migration Failure", NotificationType.Error);
}
// If it is not a valid NPC name, check if it can be a player name.
else if (ActorManager.VerifyPlayerName(name))
{
identifier = _actorManager.CreatePlayer(ByteString.FromStringUnsafe(name, false), ushort.MaxValue);
var shortName = string.Join(" ", name.Split().Select(n => $"{n[0]}."));
// Try to migrate the player name without logging full names.
if (Add($"{name} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})", new[]
{
identifier,
}, collection))
Penumbra.Log.Information($"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier.");
else
Penumbra.ChatService.NotificationMessage(
$"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.",
"Migration Failure", NotificationType.Error);
}
else
{
Penumbra.ChatService.NotificationMessage(
$"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.",
"Migration Failure", NotificationType.Error);
}
}
}
}

View file

@ -8,7 +8,7 @@ using Penumbra.GameData.Actors;
using Penumbra.Services;
using Penumbra.String;
namespace Penumbra.Collections;
namespace Penumbra.Collections.Manager;
public sealed partial class IndividualCollections
{
@ -234,7 +234,7 @@ public sealed partial class IndividualCollections
=> identifier.IsValid ? Index(DisplayString(identifier)) : -1;
private string DisplayString(ActorIdentifier identifier)
{
{
return identifier.Type switch
{
IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})",

View file

@ -0,0 +1,68 @@
using System;
using Dalamud.Interface.Internal.Notifications;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Collections.Manager;
public class InheritanceManager : IDisposable
{
private readonly CollectionStorage _storage;
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
public InheritanceManager(CollectionStorage storage, SaveService saveService, CommunicatorService communicator)
{
_storage = storage;
_saveService = saveService;
_communicator = communicator;
ApplyInheritances();
_communicator.CollectionChange.Subscribe(OnCollectionChange);
}
public void Dispose()
{
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
}
/// <summary>
/// Inheritances can not be setup before all collections are read,
/// so this happens after reading the collections in the constructor, consuming the stored strings.
/// </summary>
private void ApplyInheritances()
{
foreach (var (collection, inheritances, changes) in _storage.ConsumeInheritanceNames())
{
var localChanges = changes;
foreach (var subCollection in inheritances)
{
if (collection.AddInheritance(subCollection, false))
continue;
localChanges = true;
Penumbra.ChatService.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", "Warning",
NotificationType.Warning);
}
if (localChanges)
_saveService.ImmediateSave(collection);
}
}
private void OnCollectionChange(CollectionType collectionType, ModCollection? old, ModCollection? newCollection, string _3)
{
if (collectionType is not CollectionType.Inactive || old == null)
return;
foreach (var inheritance in old.Inheritance)
old.ClearSubscriptions(inheritance);
foreach (var c in _storage)
{
var inheritedIdx = c._inheritance.IndexOf(old);
if (inheritedIdx >= 0)
c.RemoveInheritance(inheritedIdx);
}
}
}

View file

@ -2,13 +2,13 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Penumbra.Collections;
using Penumbra.Api;
using Penumbra.GameData.Actors;
using Penumbra.Mods;
using Penumbra.Services;
using Penumbra.String;
namespace Penumbra.Api;
namespace Penumbra.Collections.Manager;
public class TempCollectionManager : IDisposable
{
@ -16,19 +16,21 @@ public class TempCollectionManager : IDisposable
public readonly IndividualCollections Collections;
private readonly CommunicatorService _communicator;
private readonly CollectionStorage _storage;
private readonly Dictionary<string, ModCollection> _customCollections = new();
public TempCollectionManager(CommunicatorService communicator, IndividualCollections collections)
public TempCollectionManager(CommunicatorService communicator, ActorService actors, CollectionStorage storage)
{
_communicator = communicator;
Collections = collections;
_storage = storage;
Collections = new IndividualCollections(actors);
_communicator.TemporaryGlobalModChange.Event += OnGlobalModChange;
_communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange);
}
public void Dispose()
{
_communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange;
_communicator.TemporaryGlobalModChange.Unsubscribe(OnGlobalModChange);
}
private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed)
@ -45,7 +47,7 @@ public class TempCollectionManager : IDisposable
public string CreateTemporaryCollection(string name)
{
if (Penumbra.CollectionManager.ByName(name, out _))
if (_storage.ByName(name, out _))
return string.Empty;
if (GlobalChangeCounter == int.MaxValue)

View file

@ -40,7 +40,7 @@ public partial class ModCollection
// Force an update with metadata for this cache.
internal void ForceCacheUpdate()
=> CalculateEffectiveFileList(this == Penumbra.CollectionManager.Default);
=> CalculateEffectiveFileList(this == Penumbra.CollectionManager.Active.Default);
// Handle temporary mods for this collection.
public void Apply(TemporaryMod tempMod, bool created)

View file

@ -20,7 +20,7 @@ public record ModConflicts( IMod Mod2, List< object > Conflicts, bool HasPriorit
/// The Cache contains all required temporary data to use a collection.
/// It will only be setup if a collection gets activated in any way.
/// </summary>
internal class ModCollectionCache : IDisposable
public class ModCollectionCache : IDisposable
{
private readonly ModCollection _collection;
private readonly SortedList< string, (SingleArray< IMod >, object?) > _changedItems = new();
@ -175,7 +175,7 @@ internal class ModCollectionCache : IDisposable
break;
case ModSettingChange.MultiInheritance:
case ModSettingChange.MultiEnableState:
FullRecalculation(_collection == Penumbra.CollectionManager.Default);
FullRecalculation(_collection == Penumbra.CollectionManager.Active.Default);
break;
}
}
@ -183,7 +183,7 @@ internal class ModCollectionCache : IDisposable
// Inheritance changes are too big to check for relevance,
// just recompute everything.
private void OnInheritanceChange( bool _ )
=> FullRecalculation(_collection == Penumbra.CollectionManager.Default);
=> FullRecalculation(_collection == Penumbra.CollectionManager.Active.Default);
public void FullRecalculation(bool isDefault)
{
@ -269,7 +269,7 @@ internal class ModCollectionCache : IDisposable
if( addMetaChanges )
{
++_collection.ChangeCounter;
if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods )
if( _collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods )
{
Penumbra.ResidentResources.Reload();
MetaManipulations.SetFiles();
@ -327,7 +327,7 @@ internal class ModCollectionCache : IDisposable
AddMetaFiles();
}
if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods )
if( _collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods )
{
Penumbra.ResidentResources.Reload();
MetaManipulations.SetFiles();

View file

@ -16,7 +16,7 @@ namespace Penumbra.Collections;
public partial class ModCollection
{
public const int CurrentVersion = 1;
public const string DefaultCollection = "Default";
public const string DefaultCollectionName = "Default";
public const string EmptyCollection = "None";
public static readonly ModCollection Empty = CreateEmpty();
@ -100,11 +100,6 @@ public partial class ModCollection
public ModCollection Duplicate(string name)
=> new(name, this);
// Check if a name is valid to use for a collection.
// Does not check for uniqueness.
public static bool IsValidName(string name)
=> name.Length > 0 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath());
// Remove all settings for not currently-installed mods.
public void CleanUnavailableSettings()
{

View file

@ -1,7 +1,4 @@
using System;
using System.Linq;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.GameData.Actors;
namespace Penumbra.Collections;
@ -14,7 +11,7 @@ public readonly struct ResolveData
public ModCollection ModCollection
=> _modCollection ?? ModCollection.Empty;
public readonly IntPtr AssociatedGameObject;
public readonly nint AssociatedGameObject;
public bool Valid
=> _modCollection != null;
@ -22,17 +19,17 @@ public readonly struct ResolveData
public ResolveData()
{
_modCollection = null!;
AssociatedGameObject = IntPtr.Zero;
AssociatedGameObject = nint.Zero;
}
public ResolveData(ModCollection collection, IntPtr gameObject)
public ResolveData(ModCollection collection, nint gameObject)
{
_modCollection = collection;
AssociatedGameObject = gameObject;
}
public ResolveData(ModCollection collection)
: this(collection, IntPtr.Zero)
: this(collection, nint.Zero)
{ }
public override string ToString()
@ -44,9 +41,9 @@ public static class ResolveDataExtensions
public static ResolveData ToResolveData(this ModCollection collection)
=> new(collection);
public static ResolveData ToResolveData(this ModCollection collection, IntPtr ptr)
public static ResolveData ToResolveData(this ModCollection collection, nint ptr)
=> new(collection, ptr);
public static unsafe ResolveData ToResolveData(this ModCollection collection, void* ptr)
=> new(collection, (IntPtr)ptr);
=> new(collection, (nint)ptr);
}

View file

@ -8,6 +8,7 @@ using Dalamud.Game.Text.SeStringHandling;
using ImGuiNET;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.Interop.Services;
using Penumbra.Mods;
@ -291,7 +292,7 @@ public class CommandHandler : IDisposable
}
}
var oldCollection = _collectionManager.ByType(type, identifier);
var oldCollection = _collectionManager.Active.ByType(type, identifier);
if (collection == oldCollection)
{
_chat.Print(collection == null
@ -300,30 +301,30 @@ public class CommandHandler : IDisposable
return false;
}
var individualIndex = _collectionManager.Individuals.Index(identifier);
var individualIndex = _collectionManager.Active.Individuals.Index(identifier);
if (oldCollection == null)
{
if (type.IsSpecial())
{
_collectionManager.CreateSpecialCollection(type);
_collectionManager.Active.CreateSpecialCollection(type);
}
else if (identifier.IsValid)
{
var identifiers = _collectionManager.Individuals.GetGroup(identifier);
individualIndex = _collectionManager.Individuals.Count;
_collectionManager.CreateIndividualCollection(identifiers);
var identifiers = _collectionManager.Active.Individuals.GetGroup(identifier);
individualIndex = _collectionManager.Active.Individuals.Count;
_collectionManager.Active.CreateIndividualCollection(identifiers);
}
}
else if (collection == null)
{
if (type.IsSpecial())
{
_collectionManager.RemoveSpecialCollection(type);
_collectionManager.Active.RemoveSpecialCollection(type);
}
else if (individualIndex >= 0)
{
_collectionManager.RemoveIndividualCollection(individualIndex);
_collectionManager.Active.RemoveIndividualCollection(individualIndex);
}
else
{
@ -337,7 +338,7 @@ public class CommandHandler : IDisposable
return true;
}
_collectionManager.SetCollection(collection!, type, individualIndex);
_collectionManager.Active.SetCollection(collection!, type, individualIndex);
Print($"Assigned {collection!.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}");
return true;
}
@ -454,15 +455,14 @@ public class CommandHandler : IDisposable
collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase)
? ModCollection.Empty
: _collectionManager[lowerName];
if (collection == null)
{
_chat.Print(new SeStringBuilder().AddText("The collection ").AddRed(collectionName, true).AddText(" does not exist.")
.BuiltString);
return false;
}
: _collectionManager.Storage.ByName(lowerName, out var c) ? c : null;
if (collection != null)
return true;
_chat.Print(new SeStringBuilder().AddText("The collection ").AddRed(collectionName, true).AddText(" does not exist.")
.BuiltString);
return false;
return true;
}
private static bool? ParseTrueFalseToggle(string value)

View file

@ -7,8 +7,8 @@ using Dalamud.Game.Gui;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Lumina.Excel.GeneratedSheets;
using OtterGui;
using Penumbra.Api;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.Services;
@ -61,15 +61,15 @@ public unsafe class CollectionResolver
using var performance = _performance.Measure(PerformanceType.IdentifyCollection);
var gameObject = (GameObject*)(_clientState.LocalPlayer?.Address ?? nint.Zero);
if (gameObject == null)
return _collectionManager.ByType(CollectionType.Yourself)
?? _collectionManager.Default;
return _collectionManager.Active.ByType(CollectionType.Yourself)
?? _collectionManager.Active.Default;
var player = _actors.AwaitedService.GetCurrentPlayer();
var _ = false;
return CollectionByIdentifier(player)
?? CheckYourself(player, gameObject)
?? CollectionByAttributes(gameObject, ref _)
?? _collectionManager.Default;
?? _collectionManager.Active.Default;
}
/// <summary> Identify the correct collection for a game object. </summary>
@ -78,7 +78,7 @@ public unsafe class CollectionResolver
using var t = _performance.Measure(PerformanceType.IdentifyCollection);
if (gameObject == null)
return _collectionManager.Default.ToResolveData();
return _collectionManager.Active.Default.ToResolveData();
try
{
@ -96,7 +96,7 @@ public unsafe class CollectionResolver
catch (Exception ex)
{
Penumbra.Log.Error($"Error identifying collection:\n{ex}");
return _collectionManager.Default.ToResolveData(gameObject);
return _collectionManager.Active.Default.ToResolveData(gameObject);
}
}
@ -137,9 +137,9 @@ public unsafe class CollectionResolver
}
var notYetReady = false;
var collection = _collectionManager.ByType(CollectionType.Yourself)
var collection = _collectionManager.Active.ByType(CollectionType.Yourself)
?? CollectionByAttributes(gameObject, ref notYetReady)
?? _collectionManager.Default;
?? _collectionManager.Active.Default;
ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject);
return true;
}
@ -156,9 +156,9 @@ public unsafe class CollectionResolver
var player = _actors.AwaitedService.GetCurrentPlayer();
var notYetReady = false;
var collection = (player.IsValid ? CollectionByIdentifier(player) : null)
?? _collectionManager.ByType(CollectionType.Yourself)
?? _collectionManager.Active.ByType(CollectionType.Yourself)
?? CollectionByAttributes(gameObject, ref notYetReady)
?? _collectionManager.Default;
?? _collectionManager.Active.Default;
ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject);
return true;
}
@ -172,7 +172,7 @@ public unsafe class CollectionResolver
var identifier = _actors.AwaitedService.FromObject(gameObject, out var owner, true, false, false);
if (identifier.Type is IdentifierType.Special)
{
(identifier, var type) = _collectionManager.Individuals.ConvertSpecialIdentifier(identifier);
(identifier, var type) = _collectionManager.Active.Individuals.ConvertSpecialIdentifier(identifier);
if (_config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect)
return _cache.Set(ModCollection.Empty, identifier, gameObject);
}
@ -182,7 +182,7 @@ public unsafe class CollectionResolver
?? CheckYourself(identifier, gameObject)
?? CollectionByAttributes(gameObject, ref notYetReady)
?? CheckOwnedCollection(identifier, owner, ref notYetReady)
?? _collectionManager.Default;
?? _collectionManager.Active.Default;
return notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, identifier, gameObject);
}
@ -190,7 +190,7 @@ public unsafe class CollectionResolver
/// <summary> Check both temporary and permanent character collections. Temporary first. </summary>
private ModCollection? CollectionByIdentifier(ActorIdentifier identifier)
=> _tempCollections.Collections.TryGetCollection(identifier, out var collection)
|| _collectionManager.Individuals.TryGetCollection(identifier, out collection)
|| _collectionManager.Active.Individuals.TryGetCollection(identifier, out collection)
? collection
: null;
@ -200,7 +200,7 @@ public unsafe class CollectionResolver
if (actor->ObjectIndex == 0
|| _cutscenes.GetParentIndex(actor->ObjectIndex) == 0
|| identifier.Equals(_actors.AwaitedService.GetCurrentPlayer()))
return _collectionManager.ByType(CollectionType.Yourself);
return _collectionManager.Active.ByType(CollectionType.Yourself);
return null;
}
@ -225,8 +225,8 @@ public unsafe class CollectionResolver
var bodyType = character->CustomizeData[2];
var collection = bodyType switch
{
3 => _collectionManager.ByType(CollectionType.NonPlayerElderly),
4 => _collectionManager.ByType(CollectionType.NonPlayerChild),
3 => _collectionManager.Active.ByType(CollectionType.NonPlayerElderly),
4 => _collectionManager.Active.ByType(CollectionType.NonPlayerChild),
_ => null,
};
if (collection != null)
@ -237,8 +237,8 @@ public unsafe class CollectionResolver
var isNpc = actor->ObjectKind != (byte)ObjectKind.Player;
var type = CollectionTypeExtensions.FromParts(race, gender, isNpc);
collection = _collectionManager.ByType(type);
collection ??= _collectionManager.ByType(CollectionTypeExtensions.FromParts(gender, isNpc));
collection = _collectionManager.Active.ByType(type);
collection ??= _collectionManager.Active.ByType(CollectionTypeExtensions.FromParts(gender, isNpc));
return collection;
}

View file

@ -1,17 +1,13 @@
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using Dalamud.Game.ClientState.Objects;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.Api;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Services;
using Penumbra.String.Classes;
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
namespace Penumbra.Interop.PathResolving;

View file

@ -5,6 +5,7 @@ using Dalamud.Game.ClientState;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.Interop.Services;
using Penumbra.Services;
@ -25,7 +26,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A
_communicator = communicator;
_events = events;
_communicator.CollectionChange.Event += CollectionChangeClear;
_communicator.CollectionChange.Subscribe(CollectionChangeClear);
_clientState.TerritoryChanged += TerritoryClear;
_events.CharacterDestructor += OnCharacterDestruct;
}
@ -61,7 +62,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A
public void Dispose()
{
_communicator.CollectionChange.Event -= CollectionChangeClear;
_communicator.CollectionChange.Unsubscribe(CollectionChangeClear);
_clientState.TerritoryChanged -= TerritoryClear;
_events.CharacterDestructor -= OnCharacterDestruct;
}

View file

@ -1,8 +1,8 @@
using System;
using System.Diagnostics.CodeAnalysis;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Enums;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.Structs;
@ -44,7 +44,7 @@ public class PathResolver : IDisposable
/// <summary> Obtain a temporary or permanent collection by name. </summary>
public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection)
=> _tempCollections.CollectionByName(name, out collection) || _collectionManager.ByName(name, out collection);
=> _tempCollections.CollectionByName(name, out collection) || _collectionManager.Storage.ByName(name, out collection);
/// <summary> Try to resolve the given game path to the replaced path. </summary>
public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType)
@ -57,8 +57,8 @@ public class PathResolver : IDisposable
return category switch
{
// Only Interface collection.
ResourceCategory.Ui => (_collectionManager.Interface.ResolvePath(path),
_collectionManager.Interface.ToResolveData()),
ResourceCategory.Ui => (_collectionManager.Active.Interface.ResolvePath(path),
_collectionManager.Active.Interface.ToResolveData()),
// Never allow changing scripts.
ResourceCategory.UiScript => (null, ResolveData.Invalid),
ResourceCategory.GameScript => (null, ResolveData.Invalid),
@ -93,7 +93,7 @@ public class PathResolver : IDisposable
|| _animationHookService.HandleFiles(type, gamePath, out resolveData)
|| _metaState.HandleDecalFile(type, gamePath, out resolveData);
if (!nonDefault || !resolveData.Valid)
resolveData = _collectionManager.Default.ToResolveData();
resolveData = _collectionManager.Active.Default.ToResolveData();
// Resolve using character/default collection first, otherwise forced, as usual.
var resolved = resolveData.ModCollection.ResolvePath(gamePath);
@ -115,8 +115,8 @@ public class PathResolver : IDisposable
/// <summary> Use the default method of path replacement. </summary>
private (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path)
{
var resolved = _collectionManager.Default.ResolvePath(path);
return (resolved, _collectionManager.Default.ToResolveData());
var resolved = _collectionManager.Active.Default.ResolvePath(path);
return (resolved, _collectionManager.Active.Default.ToResolveData());
}
/// <summary> After loading an IMC file, replace its contents with the modded IMC file. </summary>

View file

@ -97,7 +97,7 @@ public unsafe partial class CharacterUtility
=> SetResourceInternal(_defaultResourceData, _defaultResourceSize);
private void SetResourceToDefaultCollection()
=> Penumbra.CollectionManager.Default.SetMetaFile(GlobalMetaIndex);
=> Penumbra.CollectionManager.Active.Default.SetMetaFile(GlobalMetaIndex);
public void Dispose()
{

View file

@ -153,7 +153,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM
: 0;
}
if( Penumbra.CollectionManager.Default == _collection )
if( Penumbra.CollectionManager.Active.Default == _collection )
{
SetFiles();
Penumbra.ResidentResources.Reload();

View file

@ -21,7 +21,7 @@ public class ExportManager : IDisposable
_communicator = communicator;
_modManager = modManager;
UpdateExportDirectory(_config.ExportDirectory, false);
_communicator.ModPathChanged.Event += OnModPathChange;
_communicator.ModPathChanged.Subscribe(OnModPathChange);
}
/// <inheritdoc cref="UpdateExportDirectory(string, bool)"/>
@ -76,7 +76,7 @@ public class ExportManager : IDisposable
}
public void Dispose()
=> _communicator.ModPathChanged.Event -= OnModPathChange;
=> _communicator.ModPathChanged.Unsubscribe(OnModPathChange);
/// <summary> Automatically migrate the backup file to the new name if any exists. </summary>
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,

View file

@ -26,10 +26,10 @@ public class ModCacheManager : IDisposable, IReadOnlyList<ModCache>
_identifier = identifier;
_modManager = modManager;
_communicator.ModOptionChanged.Event += OnModOptionChange;
_communicator.ModPathChanged.Event += OnModPathChange;
_communicator.ModDataChanged.Event += OnModDataChange;
_communicator.ModDiscoveryFinished.Event += OnModDiscoveryFinished;
_communicator.ModOptionChanged.Subscribe(OnModOptionChange);
_communicator.ModPathChanged.Subscribe(OnModPathChange);
_communicator.ModDataChanged.Subscribe(OnModDataChange);
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished);
if (!identifier.Valid)
identifier.FinishedCreation += OnIdentifierCreation;
OnModDiscoveryFinished();
@ -51,10 +51,10 @@ public class ModCacheManager : IDisposable, IReadOnlyList<ModCache>
public void Dispose()
{
_communicator.ModOptionChanged.Event -= OnModOptionChange;
_communicator.ModPathChanged.Event -= OnModPathChange;
_communicator.ModDataChanged.Event -= OnModDataChange;
_communicator.ModDiscoveryFinished.Event -= OnModDiscoveryFinished;
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_communicator.ModDataChanged.Unsubscribe(OnModDataChange);
_communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished);
}
/// <summary> Compute the items changed by a given meta manipulation and put them into the changedItems dictionary. </summary>

View file

@ -5,10 +5,11 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using OtterGui.Filesystem;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Mods.Manager;
namespace Penumbra.Mods;
public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
{
@ -23,17 +24,17 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
_communicator = communicator;
_files = files;
Reload();
Changed += OnChange;
_communicator.ModDiscoveryFinished.Event += Reload;
_communicator.ModDataChanged.Event += OnDataChange;
_communicator.ModPathChanged.Event += OnModPathChange;
Changed += OnChange;
_communicator.ModDiscoveryFinished.Subscribe(Reload);
_communicator.ModDataChanged.Subscribe(OnDataChange);
_communicator.ModPathChanged.Subscribe(OnModPathChange);
}
public void Dispose()
{
_communicator.ModPathChanged.Event -= OnModPathChange;
_communicator.ModDiscoveryFinished.Event -= Reload;
_communicator.ModDataChanged.Event -= OnDataChange;
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_communicator.ModDiscoveryFinished.Unsubscribe(Reload);
_communicator.ModDataChanged.Unsubscribe(OnDataChange);
}
public struct ImportDate : ISortMode<Mod>

View file

@ -29,8 +29,10 @@ using DalamudUtil = Dalamud.Utility.Util;
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
using Penumbra.Services;
using Penumbra.Interop.Services;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager;
using Penumbra.Collections.Manager;
using Penumbra.Mods;
namespace Penumbra;
public class Penumbra : IDalamudPlugin
@ -183,7 +185,7 @@ public class Penumbra : IDalamudPlugin
{
if (CharacterUtility.Ready)
{
CollectionManager.Default.SetFiles();
CollectionManager.Active.Default.SetFiles();
ResidentResources.Reload();
RedrawService.RedrawAll(RedrawType.Redraw);
}
@ -269,23 +271,23 @@ public class Penumbra : IDalamudPlugin
+ $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority && x.Solved ? x.Conflicts.Count : 0)}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0)}\n");
sb.AppendLine("**Collections**");
sb.Append($"> **`#Collections: `** {CollectionManager.Count - 1}\n");
sb.Append($"> **`#Collections: `** {CollectionManager.Storage.Count - 1}\n");
sb.Append($"> **`#Temp Collections: `** {TempCollections.Count}\n");
sb.Append($"> **`Active Collections: `** {CollectionManager.Count(c => c.HasCache)}\n");
sb.Append($"> **`Base Collection: `** {CollectionManager.Default.AnonymizedName}\n");
sb.Append($"> **`Interface Collection: `** {CollectionManager.Interface.AnonymizedName}\n");
sb.Append($"> **`Selected Collection: `** {CollectionManager.Current.AnonymizedName}\n");
sb.Append($"> **`Active Collections: `** {CollectionManager.Caches.Count}\n");
sb.Append($"> **`Base Collection: `** {CollectionManager.Active.Default.AnonymizedName}\n");
sb.Append($"> **`Interface Collection: `** {CollectionManager.Active.Interface.AnonymizedName}\n");
sb.Append($"> **`Selected Collection: `** {CollectionManager.Active.Current.AnonymizedName}\n");
foreach (var (type, name, _) in CollectionTypeExtensions.Special)
{
var collection = CollectionManager.ByType(type);
var collection = CollectionManager.Active.ByType(type);
if (collection != null)
sb.Append($"> **`{name,-30}`** {collection.AnonymizedName}\n");
}
foreach (var (name, id, collection) in CollectionManager.Individuals.Assignments)
foreach (var (name, id, collection) in CollectionManager.Active.Individuals.Assignments)
sb.Append($"> **`{CharacterName(id[0], name),-30}`** {collection.AnonymizedName}\n");
foreach (var collection in CollectionManager.Where(c => c.HasCache))
foreach (var collection in CollectionManager.Caches.Active)
PrintCollection(collection);
return sb.ToString();

View file

@ -5,6 +5,7 @@ using OtterGui.Classes;
using OtterGui.Log;
using Penumbra.Api;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData;
using Penumbra.GameData.Data;
using Penumbra.Interop.ResourceLoading;
@ -85,7 +86,10 @@ public class PenumbraNew
.AddSingleton<Configuration>();
// Add Collection Services
services.AddTransient<IndividualCollections>()
services.AddSingleton<CollectionStorage>()
.AddSingleton<ActiveCollections>()
.AddSingleton<InheritanceManager>()
.AddSingleton<CollectionCacheManager>()
.AddSingleton<TempCollectionManager>()
.AddSingleton<CollectionManager>();

View file

@ -1,97 +1,207 @@
using System;
using System.IO;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Util;
namespace Penumbra.Services;
/// <summary>
/// Triggered whenever collection setup is changed.
/// <list type="number">
/// <item>Parameter is the type of the changed collection. (Inactive or Temporary for additions or deletions)</item>
/// <item>Parameter is the old collection, or null on additions.</item>
/// <item>Parameter is the new collection, or null on deletions.</item>
/// <item>Parameter is the display name for Individual collections or an empty string otherwise.</item>
/// </list> </summary>
public sealed class CollectionChange : EventWrapper<Action<CollectionType, ModCollection?, ModCollection?, string>>
{
public CollectionChange()
: base(nameof(CollectionChange))
{ }
public void Invoke(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string displayName)
=> Invoke(this, collectionType, oldCollection, newCollection, displayName);
}
/// <summary>
/// Triggered whenever a temporary mod for all collections is changed.
/// <list type="number">
/// <item>Parameter added, deleted or edited temporary mod.</item>
/// <item>Parameter is whether the mod was newly created.</item>
/// <item>Parameter is whether the mod was deleted.</item>
/// </list> </summary>
public sealed class TemporaryGlobalModChange : EventWrapper<Action<TemporaryMod, bool, bool>>
{
public TemporaryGlobalModChange()
: base(nameof(TemporaryGlobalModChange))
{ }
public void Invoke(TemporaryMod temporaryMod, bool newlyCreated, bool deleted)
=> Invoke(this, temporaryMod, newlyCreated, deleted);
}
/// <summary>
/// Triggered whenever a character base draw object is being created by the game.
/// <list type="number">
/// <item>Parameter is the game object for which a draw object is created. </item>
/// <item>Parameter is the name of the applied collection. </item>
/// <item>Parameter is a pointer to the model id (an uint). </item>
/// <item>Parameter is a pointer to the customize array. </item>
/// <item>Parameter is a pointer to the equip data array. </item>
/// </list> </summary>
public sealed class CreatingCharacterBase : EventWrapper<Action<nint, string, nint, nint, nint>>
{
public CreatingCharacterBase()
: base(nameof(CreatingCharacterBase))
{ }
public void Invoke(nint gameObject, string appliedCollectionName, nint modelIdAddress, nint customizeArrayAddress, nint equipDataAddress)
=> Invoke(this, gameObject, appliedCollectionName, modelIdAddress, customizeArrayAddress, equipDataAddress);
}
/// <summary> <list type="number">
/// <item>Parameter is the game object for which a draw object is created. </item>
/// <item>Parameter is the name of the applied collection. </item>
/// <item>Parameter is the created draw object. </item>
/// </list> </summary>
public sealed class CreatedCharacterBase : EventWrapper<Action<nint, string, nint>>
{
public CreatedCharacterBase()
: base(nameof(CreatedCharacterBase))
{ }
public void Invoke(nint gameObject, string appliedCollectionName, nint drawObject)
=> Invoke(this, gameObject, appliedCollectionName, drawObject);
}
/// <summary>
/// Triggered whenever mod meta data or local data is changed.
/// <list type="number">
/// <item>Parameter is the type of data change for the mod, which can be multiple flags. </item>
/// <item>Parameter is the changed mod. </item>
/// <item>Parameter is the old name of the mod in case of a name change, and null otherwise. </item>
/// </list> </summary>
public sealed class ModDataChanged : EventWrapper<Action<ModDataChangeType, Mod, string?>>
{
public ModDataChanged()
: base(nameof(ModDataChanged))
{ }
public void Invoke(ModDataChangeType changeType, Mod mod, string? oldName)
=> Invoke(this, changeType, mod, oldName);
}
/// <summary>
/// Triggered whenever an option of a mod is changed inside the mod.
/// <list type="number">
/// <item>Parameter is the type option change. </item>
/// <item>Parameter is the changed mod. </item>
/// <item>Parameter is the index of the changed group inside the mod. </item>
/// <item>Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. </item>
/// <item>Parameter is the index of the group an option was moved to. </item>
/// </list> </summary>
public sealed class ModOptionChanged : EventWrapper<Action<ModOptionChangeType, Mod, int, int, int>>
{
public ModOptionChanged()
: base(nameof(ModOptionChanged))
{ }
public void Invoke(ModOptionChangeType changeType, Mod mod, int groupIndex, int optionIndex, int moveToIndex)
=> Invoke(this, changeType, mod, groupIndex, optionIndex, moveToIndex);
}
/// <summary> Triggered whenever mods are prepared to be rediscovered. </summary>
public sealed class ModDiscoveryStarted : EventWrapper<Action>
{
public ModDiscoveryStarted()
: base(nameof(ModDiscoveryStarted))
{ }
public void Invoke()
=> EventWrapper<Action>.Invoke(this);
}
/// <summary> Triggered whenever a new mod discovery has finished. </summary>
public sealed class ModDiscoveryFinished : EventWrapper<Action>
{
public ModDiscoveryFinished()
: base(nameof(ModDiscoveryFinished))
{ }
public void Invoke()
=> Invoke(this);
}
/// <summary>
/// Triggered whenever the mod root directory changes.
/// <list type="number">
/// <item>Parameter is the full path of the new directory. </item>
/// <item>Parameter is whether the new directory is valid. </item>
/// </list>
/// </summary>
public sealed class ModDirectoryChanged : EventWrapper<Action<string, bool>>
{
public ModDirectoryChanged()
: base(nameof(ModDirectoryChanged))
{ }
public void Invoke(string newModDirectory, bool newDirectoryValid)
=> Invoke(this, newModDirectory, newDirectoryValid);
}
/// <summary>
/// Triggered whenever a mod is added, deleted, moved or reloaded.
/// <list type="number">
/// <item>Parameter is the type of change. </item>
/// <item>Parameter is the changed mod. </item>
/// <item>Parameter is the old directory on deletion, move or reload and null on addition. </item>
/// <item>Parameter is the new directory on addition, move or reload and null on deletion. </item>
/// </list>
/// </summary>
public sealed class ModPathChanged : EventWrapper<Action<ModPathChangeType, Mod, DirectoryInfo?, DirectoryInfo?>>
{
public ModPathChanged()
: base(nameof(ModPathChanged))
{ }
public void Invoke(ModPathChangeType changeType, Mod mod, DirectoryInfo? oldModDirectory, DirectoryInfo? newModDirectory)
=> Invoke(this, changeType, mod, oldModDirectory, newModDirectory);
}
public class CommunicatorService : IDisposable
{
/// <summary>
/// Triggered whenever collection setup is changed.
/// <list type="number">
/// <item>Parameter is the type of the changed collection. (Inactive or Temporary for additions or deletions)</item>
/// <item>Parameter is the old collection, or null on additions.</item>
/// <item>Parameter is the new collection, or null on deletions.</item>
/// <item>Parameter is the display name for Individual collections or an empty string otherwise.</item>
/// </list> </summary>
public readonly EventWrapper<CollectionType, ModCollection?, ModCollection?, string> CollectionChange = new(nameof(CollectionChange));
/// <inheritdoc cref="Services.CollectionChange"/>
public readonly CollectionChange CollectionChange = new();
/// <summary>
/// Triggered whenever a temporary mod for all collections is changed.
/// <list type="number">
/// <item>Parameter added, deleted or edited temporary mod.</item>
/// <item>Parameter is whether the mod was newly created.</item>
/// <item>Parameter is whether the mod was deleted.</item>
/// </list> </summary>
public readonly EventWrapper<TemporaryMod, bool, bool> TemporaryGlobalModChange = new(nameof(TemporaryGlobalModChange));
/// <inheritdoc cref="Services.TemporaryGlobalModChange"/>
public readonly TemporaryGlobalModChange TemporaryGlobalModChange = new();
/// <summary>
/// Triggered whenever a character base draw object is being created by the game.
/// <list type="number">
/// <item>Parameter is the game object for which a draw object is created. </item>
/// <item>Parameter is the name of the applied collection. </item>
/// <item>Parameter is a pointer to the model id (an uint). </item>
/// <item>Parameter is a pointer to the customize array. </item>
/// <item>Parameter is a pointer to the equip data array. </item>
/// </list> </summary>
public readonly EventWrapper<nint, string, nint, nint, nint> CreatingCharacterBase = new(nameof(CreatingCharacterBase));
/// <inheritdoc cref="Services.CreatingCharacterBase "/>
public readonly CreatingCharacterBase CreatingCharacterBase = new();
/// <summary> <list type="number">
/// <item>Parameter is the game object for which a draw object is created. </item>
/// <item>Parameter is the name of the applied collection. </item>
/// <item>Parameter is the created draw object. </item>
/// </list> </summary>
public readonly EventWrapper<nint, string, nint> CreatedCharacterBase = new(nameof(CreatedCharacterBase));
/// <inheritdoc cref="Services.CreatedCharacterBase "/>
public readonly CreatedCharacterBase CreatedCharacterBase = new();
/// <summary>
/// Triggered whenever mod meta data or local data is changed.
/// <list type="number">
/// <item>Parameter is the type of data change for the mod, which can be multiple flags. </item>
/// <item>Parameter is the changed mod. </item>
/// <item>Parameter is the old name of the mod in case of a name change, and null otherwise. </item>
/// </list> </summary>
public readonly EventWrapper<ModDataChangeType, Mod, string?> ModDataChanged = new(nameof(ModDataChanged));
/// <inheritdoc cref="Services.ModDataChanged "/>
public readonly ModDataChanged ModDataChanged = new();
/// <summary>
/// Triggered whenever an option of a mod is changed inside the mod.
/// <list type="number">
/// <item>Parameter is the type option change. </item>
/// <item>Parameter is the changed mod. </item>
/// <item>Parameter is the index of the changed group inside the mod. </item>
/// <item>Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. </item>
/// <item>Parameter is the index of the group an option was moved to. </item>
/// </list> </summary>
public readonly EventWrapper<ModOptionChangeType, Mod, int, int, int> ModOptionChanged = new(nameof(ModOptionChanged));
/// <inheritdoc cref="Services.ModOptionChanged "/>
public readonly ModOptionChanged ModOptionChanged = new();
/// <inheritdoc cref="Services.ModDiscoveryStarted "/>
public readonly ModDiscoveryStarted ModDiscoveryStarted = new();
/// <summary> Triggered whenever mods are prepared to be rediscovered. </summary>
public readonly EventWrapper ModDiscoveryStarted = new(nameof(ModDiscoveryStarted));
/// <inheritdoc cref="Services.ModDiscoveryFinished "/>
public readonly ModDiscoveryFinished ModDiscoveryFinished = new();
/// <summary> Triggered whenever a new mod discovery has finished. </summary>
public readonly EventWrapper ModDiscoveryFinished = new(nameof(ModDiscoveryFinished));
/// <inheritdoc cref="Services.ModDirectoryChanged "/>
public readonly ModDirectoryChanged ModDirectoryChanged = new();
/// <summary>
/// Triggered whenever the mod root directory changes.
/// <list type="number">
/// <item>Parameter is the full path of the new directory. </item>
/// <item>Parameter is whether the new directory is valid. </item>
/// </list>
/// </summary>
public readonly EventWrapper<string, bool> ModDirectoryChanged = new(nameof(ModDirectoryChanged));
/// <summary>
/// Triggered whenever a mod is added, deleted, moved or reloaded.
/// <list type="number">
/// <item>Parameter is the type of change. </item>
/// <item>Parameter is the changed mod. </item>
/// <item>Parameter is the old directory on deletion, move or reload and null on addition. </item>
/// <item>Parameter is the new directory on addition, move or reload and null on deletion. </item>
/// </list>
/// </summary>
public EventWrapper<ModPathChangeType, Mod, DirectoryInfo?, DirectoryInfo?> ModPathChanged = new(nameof(ModPathChanged));
/// <inheritdoc cref="Services.ModPathChanged "/>
public readonly ModPathChanged ModPathChanged = new();
public void Dispose()
{

View file

@ -7,6 +7,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Filesystem;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Mods;
using Penumbra.UI.Classes;
using SixLabors.ImageSharp;
@ -25,8 +26,8 @@ public class ConfigMigrationService
private Configuration _config = null!;
private JObject _data = null!;
public string CurrentCollection = ModCollection.DefaultCollection;
public string DefaultCollection = ModCollection.DefaultCollection;
public string CurrentCollection = ModCollection.DefaultCollectionName;
public string DefaultCollection = ModCollection.DefaultCollectionName;
public string ForcedCollection = string.Empty;
public Dictionary<string, string> CharacterCollections = new();
public Dictionary<string, string> ModSortOrder = new();
@ -87,7 +88,7 @@ public class ConfigMigrationService
if (_config.Version != 6)
return;
CollectionManager.MigrateUngenderedCollections(_fileNames);
ActiveCollectionMigration.MigrateUngenderedCollections(_fileNames);
_config.Version = 7;
}
@ -257,11 +258,11 @@ public class ConfigMigrationService
using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented;
j.WriteStartObject();
j.WritePropertyName(nameof(CollectionManager.Default));
j.WritePropertyName(nameof(ActiveCollections.Default));
j.WriteValue(def);
j.WritePropertyName(nameof(CollectionManager.Interface));
j.WritePropertyName(nameof(ActiveCollections.Interface));
j.WriteValue(ui);
j.WritePropertyName(nameof(CollectionManager.Current));
j.WritePropertyName(nameof(ActiveCollections.Current));
j.WriteValue(current);
foreach (var (type, collection) in special)
{
@ -305,7 +306,7 @@ public class ConfigMigrationService
if (!collectionJson.Exists)
return;
var defaultCollection = ModCollection.CreateNewEmpty(ModCollection.DefaultCollection);
var defaultCollection = ModCollection.CreateNewEmpty(ModCollection.DefaultCollectionName);
var defaultCollectionFile = new FileInfo(_fileNames.CollectionFile(defaultCollection));
if (defaultCollectionFile.Exists)
return;
@ -338,7 +339,7 @@ public class ConfigMigrationService
if (!InvertModListOrder)
dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority });
defaultCollection = ModCollection.MigrateFromV0(ModCollection.DefaultCollection, dict);
defaultCollection = ModCollection.MigrateFromV0(ModCollection.DefaultCollectionName, dict);
Penumbra.SaveService.ImmediateSave(defaultCollection);
}
catch (Exception e)

View file

@ -27,13 +27,13 @@ public class FilenameService
ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json");
}
/// <summary> Obtain the path of a collection file given its name. Returns an empty string if the collection is temporary. </summary>
/// <summary> Obtain the path of a collection file given its name.</summary>
public string CollectionFile(ModCollection collection)
=> collection.Index >= 0 ? Path.Combine(CollectionDirectory, $"{collection.Name.RemoveInvalidPathSymbols()}.json") : string.Empty;
=> CollectionFile(collection.Name);
/// <summary> Obtain the path of a collection file given its name. </summary>
public string CollectionFile(string collectionName)
=> Path.Combine(CollectionDirectory, $"{collectionName.RemoveInvalidPathSymbols()}.json");
=> Path.Combine(CollectionDirectory, $"{collectionName}.json");
/// <summary> Obtain the path of the local data file given a mod directory. Returns an empty string if the mod is temporary. </summary>

View file

@ -12,6 +12,7 @@ using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Mods;
@ -54,9 +55,9 @@ public class ItemSwapTab : IDisposable, ITab
// @formatter:on
};
_communicator.CollectionChange.Event += OnCollectionChange;
_collectionManager.Current.ModSettingChanged += OnSettingChange;
_communicator.ModOptionChanged.Event += OnModOptionChange;
_communicator.CollectionChange.Subscribe(OnCollectionChange);
_collectionManager.Active.Current.ModSettingChanged += OnSettingChange;
_communicator.ModOptionChanged.Subscribe(OnModOptionChange);
}
/// <summary> Update the currently selected mod or its settings. </summary>
@ -99,9 +100,9 @@ public class ItemSwapTab : IDisposable, ITab
public void Dispose()
{
_communicator.CollectionChange.Event -= OnCollectionChange;
_collectionManager.Current.ModSettingChanged -= OnSettingChange;
_communicator.ModOptionChanged.Event -= OnModOptionChange;
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
_collectionManager.Active.Current.ModSettingChanged -= OnSettingChange;
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
}
private enum SwapType
@ -199,7 +200,7 @@ public class ItemSwapTab : IDisposable, ITab
var values = _selectors[_lastTab];
if (values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null)
_affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2,
_useCurrentCollection ? _collectionManager.Current : null, _useRightRing, _useLeftRing);
_useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing);
break;
case SwapType.BetweenSlots:
@ -208,27 +209,27 @@ public class ItemSwapTab : IDisposable, ITab
if (selectorFrom.CurrentSelection.Item2 != null && selectorTo.CurrentSelection.Item2 != null)
_affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection.Item2, _slotFrom,
selectorFrom.CurrentSelection.Item2,
_useCurrentCollection ? _collectionManager.Current : null);
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
case SwapType.Hair when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? _collectionManager.Current : null);
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
case SwapType.Face when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? _collectionManager.Current : null);
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
case SwapType.Ears when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? _collectionManager.Current : null);
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
case SwapType.Tail when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? _collectionManager.Current : null);
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
case SwapType.Weapon: break;
}

View file

@ -52,7 +52,7 @@ public partial class ModEditWindow : Window, IDisposable
_modelTab.Reset();
_materialTab.Reset();
_shaderPackageTab.Reset();
_itemSwapTab.UpdateMod(mod, Penumbra.CollectionManager.Current[mod.Index].Settings);
_itemSwapTab.UpdateMod(mod, Penumbra.CollectionManager.Active.Current[mod.Index].Settings);
}
public void ChangeOption(SubMod? subMod)
@ -475,7 +475,7 @@ public partial class ModEditWindow : Window, IDisposable
/// </remarks>
private FullPath FindBestMatch(Utf8GamePath path)
{
var currentFile = Penumbra.CollectionManager.Current.ResolvePath(path);
var currentFile = Penumbra.CollectionManager.Active.Current.ResolvePath(path);
if (currentFile != null)
return currentFile.Value;

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using ImGuiNET;
using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
namespace Penumbra.UI.CollectionTab;
@ -17,16 +18,16 @@ public sealed class CollectionSelector : FilterComboCache<ModCollection>
public void Draw(string label, float width, int individualIdx)
{
var (_, collection) = _collectionManager.Individuals[individualIdx];
var (_, collection) = _collectionManager.Active.Individuals[individualIdx];
if (Draw(label, collection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null)
_collectionManager.SetCollection(CurrentSelection, CollectionType.Individual, individualIdx);
_collectionManager.Active.SetCollection(CurrentSelection, CollectionType.Individual, individualIdx);
}
public void Draw(string label, float width, CollectionType type)
{
var current = _collectionManager.ByType(type, ActorIdentifier.Invalid);
var current = _collectionManager.Active.ByType(type, ActorIdentifier.Invalid);
if (Draw(label, current?.Name ?? string.Empty, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null)
_collectionManager.SetCollection(CurrentSelection, type);
_collectionManager.Active.SetCollection(CurrentSelection, type);
}
protected override string ToString(ModCollection obj)

View file

@ -8,6 +8,7 @@ using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.Services;
@ -45,7 +46,7 @@ public class IndividualCollectionUi
+ $"More general {TutorialService.GroupAssignment} or the {TutorialService.DefaultCollection} do not apply if an Individual Collection takes effect.\n"
+ "Certain related actors - like the ones in cutscenes or preview windows - will try to use appropriate individual collections.");
ImGui.Separator();
for (var i = 0; i < _collectionManager.Individuals.Count; ++i)
for (var i = 0; i < _collectionManager.Active.Individuals.Count; ++i)
{
DrawIndividualAssignment(i);
}
@ -138,13 +139,13 @@ public class IndividualCollectionUi
/// <summary> Draw a single individual assignment. </summary>
private void DrawIndividualAssignment(int idx)
{
var (name, _) = _collectionManager.Individuals[idx];
var (name, _) = _collectionManager.Active.Individuals[idx];
using var id = ImRaii.PushId(idx);
_withEmpty.Draw("##IndividualCombo", UiHelpers.InputTextWidth.X, idx);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty,
false, true))
_collectionManager.RemoveIndividualCollection(idx);
_collectionManager.Active.RemoveIndividualCollection(idx);
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
@ -163,7 +164,7 @@ public class IndividualCollectionUi
return;
if (_individualDragDropIdx >= 0)
_collectionManager.MoveIndividualCollection(_individualDragDropIdx, idx);
_collectionManager.Active.MoveIndividualCollection(_individualDragDropIdx, idx);
_individualDragDropIdx = -1;
}
@ -178,7 +179,7 @@ public class IndividualCollectionUi
if (ImGuiUtil.DrawDisabledButton("Assign Player", buttonWidth, _newPlayerTooltip,
_newPlayerTooltip.Length > 0 || _newPlayerIdentifiers.Length == 0))
{
_collectionManager.CreateIndividualCollection(_newPlayerIdentifiers);
_collectionManager.Active.CreateIndividualCollection(_newPlayerIdentifiers);
change = true;
}
@ -196,7 +197,7 @@ public class IndividualCollectionUi
if (ImGuiUtil.DrawDisabledButton("Assign NPC", buttonWidth, _newNpcTooltip,
_newNpcIdentifiers.Length == 0 || _newNpcTooltip.Length > 0))
{
_collectionManager.CreateIndividualCollection(_newNpcIdentifiers);
_collectionManager.Active.CreateIndividualCollection(_newNpcIdentifiers);
change = true;
}
@ -209,7 +210,7 @@ public class IndividualCollectionUi
_newOwnedIdentifiers.Length == 0 || _newOwnedTooltip.Length > 0))
return false;
_collectionManager.CreateIndividualCollection(_newOwnedIdentifiers);
_collectionManager.Active.CreateIndividualCollection(_newOwnedIdentifiers);
return true;
}
@ -220,7 +221,7 @@ public class IndividualCollectionUi
_newRetainerIdentifiers.Length == 0 || _newRetainerTooltip.Length > 0))
return false;
_collectionManager.CreateIndividualCollection(_newRetainerIdentifiers);
_collectionManager.Active.CreateIndividualCollection(_newRetainerIdentifiers);
return true;
}
@ -264,7 +265,7 @@ public class IndividualCollectionUi
private bool DrawNewCurrentPlayerCollection(Vector2 width)
{
var player = _actorService.AwaitedService.GetCurrentPlayer();
var result = _collectionManager.Individuals.CanAdd(player);
var result = _collectionManager.Active.Individuals.CanAdd(player);
var tt = result switch
{
IndividualCollections.AddResult.Valid => $"Assign a collection to {player}.",
@ -277,7 +278,7 @@ public class IndividualCollectionUi
if (!ImGuiUtil.DrawDisabledButton("Assign Current Player", width, tt, result != IndividualCollections.AddResult.Valid))
return false;
_collectionManager.CreateIndividualCollection(player);
_collectionManager.Active.CreateIndividualCollection(player);
return true;
}
@ -285,7 +286,7 @@ public class IndividualCollectionUi
private bool DrawNewTargetCollection(Vector2 width)
{
var target = _actorService.AwaitedService.FromObject(DalamudServices.Targets.Target, false, true, true);
var result = _collectionManager.Individuals.CanAdd(target);
var result = _collectionManager.Active.Individuals.CanAdd(target);
var tt = result switch
{
IndividualCollections.AddResult.Valid => $"Assign a collection to {target}.",
@ -295,7 +296,7 @@ public class IndividualCollectionUi
};
if (ImGuiUtil.DrawDisabledButton("Assign Current Target", width, tt, result != IndividualCollections.AddResult.Valid))
{
_collectionManager.CreateIndividualCollection(_collectionManager.Individuals.GetGroup(target));
_collectionManager.Active.CreateIndividualCollection(_collectionManager.Active.Individuals.GetGroup(target));
return true;
}
@ -311,7 +312,7 @@ public class IndividualCollectionUi
private void UpdateIdentifiers()
{
var combo = GetNpcCombo(_newKind);
_newPlayerTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Player, _newCharacterName,
_newPlayerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Player, _newCharacterName,
_worldCombo.CurrentSelection.Key, ObjectKind.None,
Array.Empty<uint>(), out _newPlayerIdentifiers) switch
{
@ -320,7 +321,7 @@ public class IndividualCollectionUi
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty,
};
_newRetainerTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None,
_newRetainerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None,
Array.Empty<uint>(), out _newRetainerIdentifiers) switch
{
_ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty,
@ -330,13 +331,13 @@ public class IndividualCollectionUi
};
if (combo.CurrentSelection.Ids != null)
{
_newNpcTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind,
_newNpcTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind,
combo.CurrentSelection.Ids, out _newNpcIdentifiers) switch
{
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty,
};
_newOwnedTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Owned, _newCharacterName,
_newOwnedTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Owned, _newCharacterName,
_worldCombo.CurrentSelection.Key, _newKind,
combo.CurrentSelection.Ids, out _newOwnedIdentifiers) switch
{

View file

@ -7,6 +7,7 @@ using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.UI.Classes;
namespace Penumbra.UI.CollectionTab;
@ -116,8 +117,8 @@ public class InheritanceUi
return;
_seenInheritedCollections.Clear();
_seenInheritedCollections.Add(_collectionManager.Current);
foreach (var collection in _collectionManager.Current.Inheritance.ToList())
_seenInheritedCollections.Add(_collectionManager.Active.Current);
foreach (var collection in _collectionManager.Active.Current.Inheritance.ToList())
DrawInheritance(collection);
}
@ -135,7 +136,7 @@ public class InheritanceUi
using var target = ImRaii.DragDropTarget();
if (target.Success && ImGuiUtil.IsDropping(InheritanceDragDropLabel))
_inheritanceAction = (_collectionManager.Current.Inheritance.IndexOf(_movedInheritance!), -1);
_inheritanceAction = (_collectionManager.Active.Current.Inheritance.IndexOf(_movedInheritance!), -1);
}
/// <summary>
@ -146,7 +147,7 @@ public class InheritanceUi
{
if (_newCurrentCollection != null)
{
_collectionManager.SetCollection(_newCurrentCollection, CollectionType.Current);
_collectionManager.Active.SetCollection(_newCurrentCollection, CollectionType.Current);
_newCurrentCollection = null;
}
@ -156,9 +157,9 @@ public class InheritanceUi
if (_inheritanceAction.Value.Item1 >= 0)
{
if (_inheritanceAction.Value.Item2 == -1)
_collectionManager.Current.RemoveInheritance(_inheritanceAction.Value.Item1);
_collectionManager.Active.Current.RemoveInheritance(_inheritanceAction.Value.Item1);
else
_collectionManager.Current.MoveInheritance(_inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2);
_collectionManager.Active.Current.MoveInheritance(_inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2);
}
_inheritanceAction = null;
@ -172,7 +173,7 @@ public class InheritanceUi
{
DrawNewInheritanceCombo();
ImGui.SameLine();
var inheritance = _collectionManager.Current.CheckValidInheritance(_newInheritance);
var inheritance = _collectionManager.Active.Current.CheckValidInheritance(_newInheritance);
var tt = inheritance switch
{
ModCollection.ValidInheritance.Empty => "No valid collection to inherit from selected.",
@ -184,7 +185,7 @@ public class InheritanceUi
};
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, tt,
inheritance != ModCollection.ValidInheritance.Valid, true)
&& _collectionManager.Current.AddInheritance(_newInheritance!, true))
&& _collectionManager.Active.Current.AddInheritance(_newInheritance!, true))
_newInheritance = null;
if (inheritance != ModCollection.ValidInheritance.Valid)
@ -232,15 +233,15 @@ public class InheritanceUi
private void DrawNewInheritanceCombo()
{
ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton);
_newInheritance ??= _collectionManager.FirstOrDefault(c
=> c != _collectionManager.Current && !_collectionManager.Current.Inheritance.Contains(c))
_newInheritance ??= _collectionManager.Storage.FirstOrDefault(c
=> c != _collectionManager.Active.Current && !_collectionManager.Active.Current.Inheritance.Contains(c))
?? ModCollection.Empty;
using var combo = ImRaii.Combo("##newInheritance", _newInheritance.Name);
if (!combo)
return;
foreach (var collection in _collectionManager
.Where(c => _collectionManager.Current.CheckValidInheritance(c) == ModCollection.ValidInheritance.Valid)
foreach (var collection in _collectionManager.Storage
.Where(c => _collectionManager.Active.Current.CheckValidInheritance(c) == ModCollection.ValidInheritance.Valid)
.OrderBy(c => c.Name))
{
if (ImGui.Selectable(collection.Name, _newInheritance == collection))
@ -260,8 +261,8 @@ public class InheritanceUi
if (_movedInheritance != null)
{
var idx1 = _collectionManager.Current.Inheritance.IndexOf(_movedInheritance);
var idx2 = _collectionManager.Current.Inheritance.IndexOf(collection);
var idx1 = _collectionManager.Active.Current.Inheritance.IndexOf(_movedInheritance);
var idx2 = _collectionManager.Active.Current.Inheritance.IndexOf(collection);
if (idx1 >= 0 && idx2 >= 0)
_inheritanceAction = (idx1, idx2);
}
@ -291,7 +292,7 @@ public class InheritanceUi
if (ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
if (withDelete && ImGui.GetIO().KeyShift)
_inheritanceAction = (_collectionManager.Current.Inheritance.IndexOf(collection), -1);
_inheritanceAction = (_collectionManager.Active.Current.Inheritance.IndexOf(collection), -1);
else
_newCurrentCollection = collection;
}

View file

@ -2,6 +2,7 @@ using ImGuiNET;
using OtterGui.Classes;
using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
namespace Penumbra.UI.CollectionTab;
@ -37,6 +38,6 @@ public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, stri
protected override bool IsVisible(int globalIdx, LowerString filter)
{
var obj = Items[globalIdx];
return filter.IsContained(obj.Item2) && _collectionManager.ByType(obj.Item1) == null;
return filter.IsContained(obj.Item2) && _collectionManager.Active.ByType(obj.Item1) == null;
}
}

View file

@ -24,7 +24,7 @@ public class FileDialogService : IDisposable
{
_communicator = communicator;
_manager = SetupFileManager(config.ModDirectory);
_communicator.ModDirectoryChanged.Event += OnModDirectoryChange;
_communicator.ModDirectoryChanged.Subscribe(OnModDirectoryChange);
}
public void OpenFilePicker(string title, string filters, Action<bool, List<string>> callback, int selectionCountMax, string? startPath,
@ -71,7 +71,7 @@ public class FileDialogService : IDisposable
{
_startPaths.Clear();
_manager.Reset();
_communicator.ModDirectoryChanged.Event -= OnModDirectoryChange;
_communicator.ModDirectoryChanged.Unsubscribe(OnModDirectoryChange);
}
private string? GetStartPath(string title, string? startPath, bool forceStartPath)

View file

@ -14,10 +14,11 @@ using OtterGui.FileSystem.Selector;
using OtterGui.Raii;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Import;
using Penumbra.Import.Structs;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI.Classes;
using Penumbra.Util;
@ -79,25 +80,25 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
// @formatter:on
SetFilterTooltip();
SelectionChanged += OnSelectionChange;
_communicator.CollectionChange.Event += OnCollectionChange;
_collectionManager.Current.ModSettingChanged += OnSettingChange;
_collectionManager.Current.InheritanceChanged += OnInheritanceChange;
_communicator.ModDataChanged.Event += OnModDataChange;
_communicator.ModDiscoveryStarted.Event += StoreCurrentSelection;
_communicator.ModDiscoveryFinished.Event += RestoreLastSelection;
OnCollectionChange(CollectionType.Current, null, _collectionManager.Current, "");
SelectionChanged += OnSelectionChange;
_communicator.CollectionChange.Subscribe(OnCollectionChange);
_collectionManager.Active.Current.ModSettingChanged += OnSettingChange;
_collectionManager.Active.Current.InheritanceChanged += OnInheritanceChange;
_communicator.ModDataChanged.Subscribe(OnModDataChange);
_communicator.ModDiscoveryStarted.Subscribe(StoreCurrentSelection);
_communicator.ModDiscoveryFinished.Subscribe(RestoreLastSelection);
OnCollectionChange(CollectionType.Current, null, _collectionManager.Active.Current, "");
}
public override void Dispose()
{
base.Dispose();
_communicator.ModDiscoveryStarted.Event -= StoreCurrentSelection;
_communicator.ModDiscoveryFinished.Event -= RestoreLastSelection;
_communicator.ModDataChanged.Event -= OnModDataChange;
_collectionManager.Current.ModSettingChanged -= OnSettingChange;
_collectionManager.Current.InheritanceChanged -= OnInheritanceChange;
_communicator.CollectionChange.Event -= OnCollectionChange;
_communicator.ModDiscoveryStarted.Unsubscribe(StoreCurrentSelection);
_communicator.ModDiscoveryFinished.Unsubscribe(RestoreLastSelection);
_communicator.ModDataChanged.Unsubscribe(OnModDataChange);
_collectionManager.Active.Current.ModSettingChanged -= OnSettingChange;
_collectionManager.Active.Current.InheritanceChanged -= OnInheritanceChange;
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
_import?.Dispose();
_import = null;
}
@ -344,9 +345,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
});
if (inherit)
_collectionManager.Current.SetMultipleModInheritances(mods, enabled);
_collectionManager.Active.Current.SetMultipleModInheritances(mods, enabled);
else
_collectionManager.Current.SetMultipleModStates(mods, enabled);
_collectionManager.Active.Current.SetMultipleModStates(mods, enabled);
}
/// <summary>
@ -495,7 +496,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
}
else
{
(var settings, SelectedSettingCollection) = _collectionManager.Current[newSelection.Index];
(var settings, SelectedSettingCollection) = _collectionManager.Active.Current[newSelection.Index];
SelectedSettings = settings ?? ModSettings.Empty;
}
}
@ -628,11 +629,11 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
return ColorId.UndefinedMod;
if (!settings.Enabled)
return collection != _collectionManager.Current ? ColorId.InheritedDisabledMod : ColorId.DisabledMod;
return collection != _collectionManager.Active.Current ? ColorId.InheritedDisabledMod : ColorId.DisabledMod;
var conflicts = _collectionManager.Current.Conflicts(mod);
var conflicts = _collectionManager.Active.Current.Conflicts(mod);
if (conflicts.Count == 0)
return collection != _collectionManager.Current ? ColorId.InheritedMod : ColorId.EnabledMod;
return collection != _collectionManager.Active.Current ? ColorId.InheritedMod : ColorId.EnabledMod;
return conflicts.Any(c => !c.Solved)
? ColorId.ConflictingMod
@ -657,7 +658,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
return true;
// Handle Inheritance
if (collection == _collectionManager.Current)
if (collection == _collectionManager.Active.Current)
{
if (!_stateFilter.HasFlag(ModFilter.Uninherited))
return true;
@ -680,7 +681,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
}
else if (!settings.Enabled)
{
state.Color = collection == _collectionManager.Current ? ColorId.DisabledMod : ColorId.InheritedDisabledMod;
state.Color = collection == _collectionManager.Active.Current ? ColorId.DisabledMod : ColorId.InheritedDisabledMod;
if (!_stateFilter.HasFlag(ModFilter.Disabled)
|| !_stateFilter.HasFlag(ModFilter.NoConflict))
return true;
@ -691,7 +692,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
return true;
// Conflicts can only be relevant if the mod is enabled.
var conflicts = _collectionManager.Current.Conflicts(mod);
var conflicts = _collectionManager.Active.Current.Conflicts(mod);
if (conflicts.Count > 0)
{
if (conflicts.Any(c => !c.Solved))
@ -727,7 +728,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
{
state = new ModState { Color = ColorId.EnabledMod };
var mod = leaf.Value;
var (settings, collection) = _collectionManager.Current[mod.Index];
var (settings, collection) = _collectionManager.Active.Current[mod.Index];
if (ApplyStringFilters(leaf, mod))
return true;

View file

@ -3,7 +3,7 @@ using System.Numerics;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.String.Classes;
@ -26,7 +26,7 @@ public class ModPanelConflictsTab : ITab
=> "Conflicts"u8;
public bool IsVisible
=> _collectionManager.Current.Conflicts(_selector.Selected!).Count > 0;
=> _collectionManager.Active.Current.Conflicts(_selector.Selected!).Count > 0;
public void DrawContent()
{
@ -36,7 +36,7 @@ public class ModPanelConflictsTab : ITab
// Can not be null because otherwise the tab bar is never drawn.
var mod = _selector.Selected!;
foreach (var conflict in Penumbra.CollectionManager.Current.Conflicts(mod))
foreach (var conflict in _collectionManager.Active.Current.Conflicts(mod))
{
if (ImGui.Selectable(conflict.Mod2.Name) && conflict.Mod2 is Mod otherMod)
_selector.SelectByValue(otherMod);
@ -47,7 +47,7 @@ public class ModPanelConflictsTab : ITab
{
var priority = conflict.Mod2.Index < 0
? conflict.Mod2.Priority
: _collectionManager.Current[conflict.Mod2.Index].Settings!.Priority;
: _collectionManager.Active.Current[conflict.Mod2.Index].Settings!.Priority;
ImGui.TextUnformatted($"(Priority {priority})");
}

View file

@ -12,6 +12,7 @@ using Penumbra.Mods;
using Penumbra.UI.Classes;
using Dalamud.Interface.Components;
using Dalamud.Interface;
using Penumbra.Collections.Manager;
using Penumbra.Mods.Manager;
namespace Penumbra.UI.ModsTab;
@ -59,7 +60,7 @@ public class ModPanelSettingsTab : ITab
_settings = _selector.SelectedSettings;
_collection = _selector.SelectedSettingCollection;
_inherited = _collection != _collectionManager.Current;
_inherited = _collection != _collectionManager.Active.Current;
_empty = _settings == ModSettings.Empty;
DrawInheritedWarning();
@ -113,7 +114,7 @@ public class ModPanelSettingsTab : ITab
using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg);
var width = new Vector2(ImGui.GetContentRegionAvail().X, 0);
if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width))
_collectionManager.Current.SetModInheritance(_selector.Selected!.Index, false);
_collectionManager.Active.Current.SetModInheritance(_selector.Selected!.Index, false);
ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n"
+ "You can also just change any setting, which will copy the settings with the single setting changed to the current selection.");
@ -127,7 +128,7 @@ public class ModPanelSettingsTab : ITab
return;
_modManager.SetKnown(_selector.Selected!);
_collectionManager.Current.SetModState(_selector.Selected!.Index, enabled);
_collectionManager.Active.Current.SetModState(_selector.Selected!.Index, enabled);
}
/// <summary>
@ -145,7 +146,7 @@ public class ModPanelSettingsTab : ITab
if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue)
{
if (_currentPriority != _settings.Priority)
_collectionManager.Current.SetModPriority(_selector.Selected!.Index, _currentPriority.Value);
_collectionManager.Active.Current.SetModPriority(_selector.Selected!.Index, _currentPriority.Value);
_currentPriority = null;
}
@ -167,7 +168,7 @@ public class ModPanelSettingsTab : ITab
var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0;
ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll);
if (ImGui.Button(text))
_collectionManager.Current.SetModInheritance(_selector.Selected!.Index, true);
_collectionManager.Active.Current.SetModInheritance(_selector.Selected!.Index, true);
ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n"
+ "If no inherited collection has settings for this mod, it will be disabled.");
@ -190,7 +191,7 @@ public class ModPanelSettingsTab : ITab
id.Push(idx2);
var option = group[idx2];
if (ImGui.Selectable(option.Name, idx2 == selectedOption))
_collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx2);
_collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx2);
if (option.Description.Length > 0)
{
@ -235,7 +236,7 @@ public class ModPanelSettingsTab : ITab
using var i = ImRaii.PushId(idx);
var option = group[idx];
if (ImGui.RadioButton(option.Name, selectedOption == idx))
_collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx);
_collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx);
if (option.Description.Length <= 0)
continue;
@ -320,7 +321,7 @@ public class ModPanelSettingsTab : ITab
if (ImGui.Checkbox(option.Name, ref setting))
{
flags = setting ? flags | flag : flags & ~flag;
_collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags);
_collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags);
}
if (option.Description.Length > 0)
@ -348,10 +349,10 @@ public class ModPanelSettingsTab : ITab
if (ImGui.Selectable("Enable All"))
{
flags = group.Count == 32 ? uint.MaxValue : (1u << group.Count) - 1u;
_collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags);
_collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags);
}
if (ImGui.Selectable("Disable All"))
_collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, 0);
_collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, 0);
}
}

View file

@ -8,7 +8,7 @@ using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Api;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Mods;
using Penumbra.UI.Classes;
@ -17,7 +17,7 @@ namespace Penumbra.UI.Tabs;
public class ChangedItemsTab : ITab
{
private readonly CollectionManager _collectionManager;
private readonly PenumbraApi _api;
private readonly PenumbraApi _api;
public ChangedItemsTab(CollectionManager collectionManager, PenumbraApi api)
{
@ -49,7 +49,7 @@ public class ChangedItemsTab : ITab
ImGui.TableSetupColumn("mods", flags, varWidth - 120 * UiHelpers.Scale);
ImGui.TableSetupColumn("id", flags, 120 * UiHelpers.Scale);
var items = _collectionManager.Current.ChangedItems;
var items = _collectionManager.Active.Current.ChangedItems;
var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty
? ImGuiClip.ClippedDraw(items, skips, DrawChangedItemColumn, items.Count)
: ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn);

View file

@ -8,6 +8,7 @@ using OtterGui;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Services;
using Penumbra.UI.CollectionTab;
@ -35,12 +36,12 @@ public class CollectionsTab : IDisposable, ITab
_config = config;
_specialCollectionCombo = new SpecialCombo(_collectionManager, "##NewSpecial", 350);
_collectionsWithEmpty = new CollectionSelector(_collectionManager,
() => _collectionManager.OrderBy(c => c.Name).Prepend(ModCollection.Empty).ToList());
_collectionSelector = new CollectionSelector(_collectionManager, () => _collectionManager.OrderBy(c => c.Name).ToList());
() => _collectionManager.Storage.OrderBy(c => c.Name).Prepend(ModCollection.Empty).ToList());
_collectionSelector = new CollectionSelector(_collectionManager, () => _collectionManager.Storage.OrderBy(c => c.Name).ToList());
_inheritance = new InheritanceUi(_collectionManager);
_individualCollections = new IndividualCollectionUi(actorService, _collectionManager, _collectionsWithEmpty);
_communicator.CollectionChange.Event += _individualCollections.UpdateIdentifiers;
_communicator.CollectionChange.Subscribe(_individualCollections.UpdateIdentifiers);
}
public ReadOnlySpan<byte> Label
@ -51,7 +52,7 @@ public class CollectionsTab : IDisposable, ITab
=> (withEmpty ? _collectionsWithEmpty : _collectionSelector).Draw(label, width, collectionType);
public void Dispose()
=> _communicator.CollectionChange.Event -= _individualCollections.UpdateIdentifiers;
=> _communicator.CollectionChange.Unsubscribe(_individualCollections.UpdateIdentifiers);
/// <summary> Draw a tutorial step regardless of tab selection. </summary>
public void DrawHeader()
@ -79,22 +80,22 @@ public class CollectionsTab : IDisposable, ITab
/// </summary>
private void CreateNewCollection(bool duplicate)
{
if (_collectionManager.AddCollection(_newCollectionName, duplicate ? _collectionManager.Current : null))
if (_collectionManager.Storage.AddCollection(_newCollectionName, duplicate ? _collectionManager.Active.Current : null))
_newCollectionName = string.Empty;
}
/// <summary> Draw the Clean Unused Settings button if there are any. </summary>
private void DrawCleanCollectionButton(Vector2 width)
{
if (!_collectionManager.Current.HasUnusedSettings)
if (!_collectionManager.Active.Current.HasUnusedSettings)
return;
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(
$"Clean {_collectionManager.Current.NumUnusedSettings} Unused Settings###CleanSettings", width
$"Clean {_collectionManager.Active.Current.NumUnusedSettings} Unused Settings###CleanSettings", width
, "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk."
, false))
_collectionManager.Current.CleanUnavailableSettings();
_collectionManager.Active.Current.CleanUnavailableSettings();
}
/// <summary> Draw the new collection input as well as its buttons. </summary>
@ -103,7 +104,7 @@ public class CollectionsTab : IDisposable, ITab
// Input for new collection name. Also checks for validity when changed.
ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X);
if (ImGui.InputTextWithHint("##New Collection", "New Collection Name...", ref _newCollectionName, 64))
_canAddCollection = _collectionManager.CanAddCollection(_newCollectionName, out _);
_canAddCollection = _collectionManager.Storage.CanAddCollection(_newCollectionName, out _);
ImGui.SameLine();
ImGuiComponents.HelpMarker(
@ -161,14 +162,14 @@ public class CollectionsTab : IDisposable, ITab
"This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything.");
// Deletion conditions.
var deleteCondition = _collectionManager.Current.Name != ModCollection.DefaultCollection;
var deleteCondition = _collectionManager.Active.Current.Name != ModCollection.DefaultCollectionName;
var modifierHeld = Penumbra.Config.DeleteModModifier.IsActive();
var tt = deleteCondition
? modifierHeld ? string.Empty : $"Hold {_config.DeleteModModifier} while clicking to delete the collection."
: $"You can not delete the collection {ModCollection.DefaultCollection}.";
: $"You can not delete the collection {ModCollection.DefaultCollectionName}.";
if (ImGuiUtil.DrawDisabledButton($"Delete {TutorialService.SelectedCollection}", width, tt, !deleteCondition || !modifierHeld))
_collectionManager.RemoveCollection(_collectionManager.Current);
_collectionManager.Storage.RemoveCollection(_collectionManager.Active.Current);
DrawCleanCollectionButton(width);
}
@ -218,11 +219,11 @@ public class CollectionsTab : IDisposable, ITab
{
ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X);
if (_specialCollectionCombo.CurrentIdx == -1
|| _collectionManager.ByType(_specialCollectionCombo.CurrentType!.Value.Item1) != null)
|| _collectionManager.Active.ByType(_specialCollectionCombo.CurrentType!.Value.Item1) != null)
{
_specialCollectionCombo.ResetFilter();
_specialCollectionCombo.CurrentIdx = CollectionTypeExtensions.Special
.IndexOf(t => _collectionManager.ByType(t.Item1) == null);
.IndexOf(t => _collectionManager.Active.ByType(t.Item1) == null);
}
if (_specialCollectionCombo.CurrentType == null)
@ -238,7 +239,7 @@ public class CollectionsTab : IDisposable, ITab
if (!ImGuiUtil.DrawDisabledButton($"Assign {TutorialService.ConditionalGroup}", new Vector2(120 * UiHelpers.Scale, 0), tt, disabled))
return;
_collectionManager.CreateSpecialCollection(_specialCollectionCombo.CurrentType!.Value.Item1);
_collectionManager.Active.CreateSpecialCollection(_specialCollectionCombo.CurrentType!.Value.Item1);
_specialCollectionCombo.CurrentIdx = -1;
}
@ -274,7 +275,7 @@ public class CollectionsTab : IDisposable, ITab
{
foreach (var (type, name, desc) in CollectionTypeExtensions.Special)
{
var collection = _collectionManager.ByType(type);
var collection = _collectionManager.Active.ByType(type);
if (collection == null)
continue;
@ -284,7 +285,7 @@ public class CollectionsTab : IDisposable, ITab
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty,
false, true))
{
_collectionManager.RemoveSpecialCollection(type);
_collectionManager.Active.RemoveSpecialCollection(type);
_specialCollectionCombo.ResetFilter();
}

View file

@ -11,13 +11,12 @@ using ImGuiNET;
using OtterGui;
using OtterGui.Widgets;
using Penumbra.Api;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Files;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.PathResolving;
using Penumbra.Interop.Structs;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.String;
@ -35,8 +34,8 @@ public class DebugTab : ITab
private readonly StartTracker _timer;
private readonly PerformanceTracker _performance;
private readonly Configuration _config;
private readonly CollectionManager _collectionManager;
private readonly ModManager _modManager;
private readonly CollectionManager _collectionManager;
private readonly ModManager _modManager;
private readonly ValidityChecker _validityChecker;
private readonly HttpApi _httpApi;
private readonly ActorService _actorService;
@ -136,10 +135,10 @@ public class DebugTab : ITab
PrintValue("Penumbra Version", $"{_validityChecker.Version} {DebugVersionString}");
PrintValue("Git Commit Hash", _validityChecker.CommitHash);
PrintValue(TutorialService.SelectedCollection, _collectionManager.Current.Name);
PrintValue(" has Cache", _collectionManager.Current.HasCache.ToString());
PrintValue(TutorialService.DefaultCollection, _collectionManager.Default.Name);
PrintValue(" has Cache", _collectionManager.Default.HasCache.ToString());
PrintValue(TutorialService.SelectedCollection, _collectionManager.Active.Current.Name);
PrintValue(" has Cache", _collectionManager.Active.Current.HasCache.ToString());
PrintValue(TutorialService.DefaultCollection, _collectionManager.Active.Default.Name);
PrintValue(" has Cache", _collectionManager.Active.Default.HasCache.ToString());
PrintValue("Mod Manager BasePath", _modManager.BasePath.Name);
PrintValue("Mod Manager BasePath-Full", _modManager.BasePath.FullName);
PrintValue("Mod Manager BasePath IsRooted", Path.IsPathRooted(_config.ModDirectory).ToString());
@ -221,7 +220,7 @@ public class DebugTab : ITab
using var table = Table("###DrawObjectResolverTable", 6, ImGuiTableFlags.SizingFixedFit);
if (table)
foreach (var (drawObject, (gameObjectPtr, child)) in _drawObjectState
.OrderBy(kvp => ((GameObject*)kvp.Value.Item1)->ObjectIndex)
.OrderBy(kvp => ((GameObject*)kvp.Value.Item1)->ObjectIndex)
.ThenBy(kvp => kvp.Value.Item2)
.ThenBy(kvp => kvp.Key))
{
@ -299,7 +298,7 @@ public class DebugTab : ITab
{
using var table = Table("##PathCollectionsIdentifiedTable", 4, ImGuiTableFlags.SizingFixedFit);
if (table)
foreach (var (address, identifier, collection) in _identifiedCollectionCache
foreach (var (address, identifier, collection) in _identifiedCollectionCache
.OrderBy(kvp => ((GameObject*)kvp.Address)->ObjectIndex))
{
ImGuiUtil.DrawTableColumn($"{((GameObject*)address)->ObjectIndex}");

View file

@ -9,6 +9,7 @@ using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.String.Classes;
@ -43,7 +44,7 @@ public class EffectiveTab : ITab
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength);
ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength);
DrawEffectiveRows(_collectionManager.Current, skips, height,
DrawEffectiveRows(_collectionManager.Active.Current, skips, height,
_effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0);
}

View file

@ -16,7 +16,8 @@ using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI.ModsTab;
using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector;
using Penumbra.Collections.Manager;
namespace Penumbra.UI.Tabs;
public class ModsTab : ITab
@ -85,12 +86,12 @@ public class ModsTab : ITab
{
Penumbra.Log.Error($"Exception thrown during ModPanel Render:\n{e}");
Penumbra.Log.Error($"{_modManager.Count} Mods\n"
+ $"{_collectionManager.Current.AnonymizedName} Current Collection\n"
+ $"{_collectionManager.Current.Settings.Count} Settings\n"
+ $"{_collectionManager.Active.Current.AnonymizedName} Current Collection\n"
+ $"{_collectionManager.Active.Current.Settings.Count} Settings\n"
+ $"{_selector.SortMode.Name} Sort Mode\n"
+ $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n"
+ $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n"
+ $"{string.Join(", ", _collectionManager.Current.Inheritance.Select(c => c.AnonymizedName))} Inheritances\n"
+ $"{string.Join(", ", _collectionManager.Active.Current.Inheritance.Select(c => c.AnonymizedName))} Inheritances\n"
+ $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n");
}
}
@ -163,27 +164,27 @@ public class ModsTab : ITab
_tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors);
if (!_collectionManager.CurrentCollectionInUse)
if (!_collectionManager.Active.CurrentCollectionInUse)
ImGuiUtil.DrawTextButton("The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg);
}
private void DrawDefaultCollectionButton(Vector2 width)
{
var name = $"{TutorialService.DefaultCollection} ({_collectionManager.Default.Name})";
var isCurrent = _collectionManager.Default == _collectionManager.Current;
var isEmpty = _collectionManager.Default == ModCollection.Empty;
var name = $"{TutorialService.DefaultCollection} ({_collectionManager.Active.Default.Name})";
var isCurrent = _collectionManager.Active.Default == _collectionManager.Active.Current;
var isEmpty = _collectionManager.Active.Default == ModCollection.Empty;
var tt = isCurrent ? $"The current collection is already the configured {TutorialService.DefaultCollection}."
: isEmpty ? $"The {TutorialService.DefaultCollection} is configured to be empty."
: $"Set the {TutorialService.SelectedCollection} to the configured {TutorialService.DefaultCollection}.";
if (ImGuiUtil.DrawDisabledButton(name, width, tt, isCurrent || isEmpty))
_collectionManager.SetCollection(_collectionManager.Default, CollectionType.Current);
_collectionManager.Active.SetCollection(_collectionManager.Active.Default, CollectionType.Current);
}
private void DrawInheritedCollectionButton(Vector2 width)
{
var noModSelected = _selector.Selected == null;
var collection = _selector.SelectedSettingCollection;
var modInherited = collection != _collectionManager.Current;
var modInherited = collection != _collectionManager.Active.Current;
var (name, tt) = (noModSelected, modInherited) switch
{
(true, _) => ("Inherited Collection", "No mod selected."),
@ -192,7 +193,7 @@ public class ModsTab : ITab
(false, false) => ("Not Inherited", "The selected mod does not inherit its settings."),
};
if (ImGuiUtil.DrawDisabledButton(name, width, tt, noModSelected || !modInherited))
_collectionManager.SetCollection(collection, CollectionType.Current);
_collectionManager.Active.SetCollection(collection, CollectionType.Current);
}
/// <summary> Get the correct size for the mod selector based on current config. </summary>

View file

@ -90,7 +90,7 @@ public class TutorialService
+ "In here, we can create new collections, delete collections, or make them inherit from each other.")
.Register($"Initial Setup, Step 5: {SelectedCollection}",
$"The {SelectedCollection} is the one we are currently editing. Any changes we make in our mod settings later in the next tab will edit this collection."
+ $"We should already have a collection named {ModCollection.DefaultCollection} selected, and for our simple setup, we do not need to do anything here.\n\n")
+ $"We should already have a collection named {ModCollection.DefaultCollectionName} selected, and for our simple setup, we do not need to do anything here.\n\n")
.Register("Inheritance",
"This is a more advanced feature. Click the help button for more information, but we will ignore this for now.")
.Register($"Initial Setup, Step 6: {ActiveCollections}",
@ -99,7 +99,7 @@ public class TutorialService
+ $"The {SelectedCollection} is also active for technical reasons, while not necessarily being assigned to anything.\n\n"
+ "Open this now to continue.")
.Register($"Initial Setup, Step 7: {DefaultCollection}",
$"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollection} - is the main one.\n\n"
$"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollectionName} - is the main one.\n\n"
+ $"As long as no more specific conditions apply to an object in the game, the mods from the {DefaultCollection} will be used.\n\n"
+ "This is also the collection you need to use for all mods that are not directly associated with any character in the game or the user interface, like music mods.")
.Register("Interface Collection",

View file

@ -4,32 +4,14 @@ using System.Linq;
namespace Penumbra.Util;
public readonly struct EventWrapper : IDisposable
public abstract class EventWrapper<T> : IDisposable where T : Delegate
{
private readonly string _name;
private readonly List<Action> _event = new();
private readonly string _name;
private readonly List<(object Subscriber, int Priority)> _event = new();
public EventWrapper(string name)
protected EventWrapper(string name)
=> _name = name;
public void Invoke()
{
lock (_event)
{
foreach (var action in _event)
{
try
{
action.Invoke();
}
catch (Exception ex)
{
Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}");
}
}
}
}
public void Dispose()
{
lock (_event)
@ -38,345 +20,165 @@ public readonly struct EventWrapper : IDisposable
}
}
public event Action Event
{
add
{
lock (_event)
{
if (_event.All(a => a != value))
_event.Add(value);
}
}
remove
{
lock (_event)
{
_event.Remove(value);
}
}
}
}
public readonly struct EventWrapper<T1> : IDisposable
{
private readonly string _name;
private readonly List<Action<T1>> _event = new();
public EventWrapper(string name)
=> _name = name;
public void Invoke(T1 arg1)
public void Subscribe(T subscriber, int priority = 0)
{
lock (_event)
{
foreach (var action in _event)
var existingIdx = _event.FindIndex(p => (T) p.Subscriber == subscriber);
var idx = _event.FindIndex(p => p.Priority > priority);
if (idx == existingIdx)
{
if (idx < 0)
_event.Add((subscriber, priority));
else
_event[idx] = (subscriber, priority);
}
else
{
if (idx < 0)
_event.Add((subscriber, priority));
else
_event.Insert(idx, (subscriber, priority));
if (existingIdx >= 0)
_event.RemoveAt(existingIdx < idx ? existingIdx : existingIdx + 1);
}
}
}
public void Unsubscribe(T subscriber)
{
lock (_event)
{
var idx = _event.FindIndex(p => (T) p.Subscriber == subscriber);
if (idx >= 0)
_event.RemoveAt(idx);
}
}
protected static void Invoke(EventWrapper<T> wrapper)
{
lock (wrapper._event)
{
foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse())
{
try
{
action.Invoke(arg1);
((Action)action).Invoke();
}
catch (Exception ex)
{
Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}");
Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}");
}
}
}
}
public void Dispose()
protected static void Invoke<T1>(EventWrapper<T> wrapper, T1 a)
{
lock (_event)
lock (wrapper._event)
{
_event.Clear();
}
}
public event Action<T1> Event
{
add
{
lock (_event)
{
if (_event.All(a => a != value))
_event.Add(value);
}
}
remove
{
lock (_event)
{
_event.Remove(value);
}
}
}
}
public readonly struct EventWrapper<T1, T2> : IDisposable
{
private readonly string _name;
private readonly List<Action<T1, T2>> _event = new();
public EventWrapper(string name)
=> _name = name;
public void Invoke(T1 arg1, T2 arg2)
{
lock (_event)
{
foreach (var action in _event)
foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse())
{
try
{
action.Invoke(arg1, arg2);
((Action<T1>)action).Invoke(a);
}
catch (Exception ex)
{
Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}");
Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}");
}
}
}
}
public void Dispose()
protected static void Invoke<T1, T2>(EventWrapper<T> wrapper, T1 a, T2 b)
{
lock (_event)
lock (wrapper._event)
{
_event.Clear();
}
}
public event Action<T1, T2> Event
{
add
{
lock (_event)
{
if (_event.All(a => a != value))
_event.Add(value);
}
}
remove
{
lock (_event)
{
_event.Remove(value);
}
}
}
}
public readonly struct EventWrapper<T1, T2, T3> : IDisposable
{
private readonly string _name;
private readonly List<Action<T1, T2, T3>> _event = new();
public EventWrapper(string name)
=> _name = name;
public void Invoke(T1 arg1, T2 arg2, T3 arg3)
{
lock (_event)
{
foreach (var action in _event)
foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse())
{
try
{
action.Invoke(arg1, arg2, arg3);
((Action<T1, T2>)action).Invoke(a, b);
}
catch (Exception ex)
{
Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}");
Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}");
}
}
}
}
public void Dispose()
protected static void Invoke<T1, T2, T3>(EventWrapper<T> wrapper, T1 a, T2 b, T3 c)
{
lock (_event)
lock (wrapper._event)
{
_event.Clear();
}
}
public event Action<T1, T2, T3> Event
{
add
{
lock (_event)
{
if (_event.All(a => a != value))
_event.Add(value);
}
}
remove
{
lock (_event)
{
_event.Remove(value);
}
}
}
}
public readonly struct EventWrapper<T1, T2, T3, T4> : IDisposable
{
private readonly string _name;
private readonly List<Action<T1, T2, T3, T4>> _event = new();
public EventWrapper(string name)
=> _name = name;
public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
{
lock (_event)
{
foreach (var action in _event)
foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse())
{
try
{
action.Invoke(arg1, arg2, arg3, arg4);
((Action<T1, T2, T3>)action).Invoke(a, b, c);
}
catch (Exception ex)
{
Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}");
Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}");
}
}
}
}
public void Dispose()
protected static void Invoke<T1, T2, T3, T4>(EventWrapper<T> wrapper, T1 a, T2 b, T3 c, T4 d)
{
lock (_event)
lock (wrapper._event)
{
_event.Clear();
}
}
public event Action<T1, T2, T3, T4> Event
{
add
{
lock (_event)
{
if (_event.All(a => a != value))
_event.Add(value);
}
}
remove
{
lock (_event)
{
_event.Remove(value);
}
}
}
}
public readonly struct EventWrapper<T1, T2, T3, T4, T5> : IDisposable
{
private readonly string _name;
private readonly List<Action<T1, T2, T3, T4, T5>> _event = new();
public EventWrapper(string name)
=> _name = name;
public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
{
lock (_event)
{
foreach (var action in _event)
foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse())
{
try
{
action.Invoke(arg1, arg2, arg3, arg4, arg5);
((Action<T1, T2, T3, T4>)action).Invoke(a, b, c, d);
}
catch (Exception ex)
{
Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}");
Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}");
}
}
}
}
public void Dispose()
protected static void Invoke<T1, T2, T3, T4, T5>(EventWrapper<T> wrapper, T1 a, T2 b, T3 c, T4 d, T5 e)
{
lock (_event)
lock (wrapper._event)
{
_event.Clear();
}
}
public event Action<T1, T2, T3, T4, T5> Event
{
add
{
lock (_event)
{
if (_event.All(a => a != value))
_event.Add(value);
}
}
remove
{
lock (_event)
{
_event.Remove(value);
}
}
}
}
public readonly struct EventWrapper<T1, T2, T3, T4, T5, T6> : IDisposable
{
private readonly string _name;
private readonly List<Action<T1, T2, T3, T4, T5, T6>> _event = new();
public EventWrapper(string name)
=> _name = name;
public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)
{
lock (_event)
{
foreach (var action in _event)
foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse())
{
try
{
action.Invoke(arg1, arg2, arg3, arg4, arg5, arg6);
((Action<T1, T2, T3, T4, T5>)action).Invoke(a, b, c, d, e);
}
catch (Exception ex)
{
Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}");
Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}");
}
}
}
}
public void Dispose()
protected static void Invoke<T1, T2, T3, T4, T5, T6>(EventWrapper<T> wrapper, T1 a, T2 b, T3 c, T4 d, T5 e, T6 f)
{
lock (_event)
lock (wrapper._event)
{
_event.Clear();
}
}
public event Action<T1, T2, T3, T4, T5, T6> Event
{
add
{
lock (_event)
foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse())
{
if (_event.All(a => a != value))
_event.Add(value);
}
}
remove
{
lock (_event)
{
_event.Remove(value);
try
{
((Action<T1, T2, T3, T4, T5, T6>)action).Invoke(a, b, c, d, e, f);
}
catch (Exception ex)
{
Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}");
}
}
}
}