mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-14 20:54:16 +01:00
Now that's a collection manager.
This commit is contained in:
parent
5a817db069
commit
f85fc46fb7
55 changed files with 2433 additions and 2317 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit d87dfa44ff6efcf4fe576d8a877c78f4ac0dc893
|
||||
Subproject commit 6c6533ac60ee6e5e401bb9a65b31ad843d1757cd
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Penumbra/Collections/Manager/ActiveCollectionMigration.cs
Normal file
67
Penumbra/Collections/Manager/ActiveCollectionMigration.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
469
Penumbra/Collections/Manager/ActiveCollections.cs
Normal file
469
Penumbra/Collections/Manager/ActiveCollections.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
173
Penumbra/Collections/Manager/CollectionCacheManager.cs
Normal file
173
Penumbra/Collections/Manager/CollectionCacheManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Penumbra/Collections/Manager/CollectionManager.cs
Normal file
20
Penumbra/Collections/Manager/CollectionManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
306
Penumbra/Collections/Manager/CollectionStorage.cs
Normal file
306
Penumbra/Collections/Manager/CollectionStorage.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
583
Penumbra/Collections/Manager/CollectionType.cs
Normal file
583
Penumbra/Collections/Manager/CollectionType.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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) >
|
||||
{
|
||||
140
Penumbra/Collections/Manager/IndividualCollections.Files.cs
Normal file
140
Penumbra/Collections/Manager/IndividualCollections.Files.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)})",
|
||||
68
Penumbra/Collections/Manager/InheritanceManager.cs
Normal file
68
Penumbra/Collections/Manager/InheritanceManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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})");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue