mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-23 16:27:47 +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
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,
|
||||
};
|
||||
}
|
||||
173
Penumbra/Collections/Manager/IndividualCollections.Access.cs
Normal file
173
Penumbra/Collections/Manager/IndividualCollections.Access.cs
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace Penumbra.Collections.Manager;
|
||||
|
||||
public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) >
|
||||
{
|
||||
public IEnumerator< (string DisplayName, ModCollection Collection) > GetEnumerator()
|
||||
=> _assignments.Select( t => ( t.DisplayName, t.Collection ) ).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public int Count
|
||||
=> _assignments.Count;
|
||||
|
||||
public (string DisplayName, ModCollection Collection) this[ int index ]
|
||||
=> ( _assignments[ index ].DisplayName, _assignments[ index ].Collection );
|
||||
|
||||
public bool TryGetCollection( ActorIdentifier identifier, [NotNullWhen( true )] out ModCollection? collection )
|
||||
{
|
||||
if( Count == 0 )
|
||||
{
|
||||
collection = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
switch( identifier.Type )
|
||||
{
|
||||
case IdentifierType.Player: return CheckWorlds( identifier, out collection );
|
||||
case IdentifierType.Retainer:
|
||||
{
|
||||
if( _individuals.TryGetValue( identifier, out collection ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if( identifier.Retainer is not ActorIdentifier.RetainerType.Mannequin && Penumbra.Config.UseOwnerNameForCharacterCollection )
|
||||
{
|
||||
return CheckWorlds( _actorManager.GetCurrentPlayer(), out collection );
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case IdentifierType.Owned:
|
||||
{
|
||||
if( CheckWorlds( identifier, out collection! ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle generic NPC
|
||||
var npcIdentifier = _actorManager.CreateIndividualUnchecked( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, identifier.Kind, identifier.DataId );
|
||||
if( npcIdentifier.IsValid && _individuals.TryGetValue( npcIdentifier, out collection ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle Ownership.
|
||||
if( Penumbra.Config.UseOwnerNameForCharacterCollection )
|
||||
{
|
||||
identifier = _actorManager.CreateIndividualUnchecked( IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, ObjectKind.None, uint.MaxValue );
|
||||
return CheckWorlds( identifier, out collection );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
case IdentifierType.Npc: return _individuals.TryGetValue( identifier, out collection );
|
||||
case IdentifierType.Special: return CheckWorlds( ConvertSpecialIdentifier( identifier ).Item1, out collection );
|
||||
}
|
||||
|
||||
collection = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public enum SpecialResult
|
||||
{
|
||||
PartyBanner,
|
||||
PvPBanner,
|
||||
Mahjong,
|
||||
CharacterScreen,
|
||||
FittingRoom,
|
||||
DyePreview,
|
||||
Portrait,
|
||||
Inspect,
|
||||
Card,
|
||||
Glamour,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
public (ActorIdentifier, SpecialResult) ConvertSpecialIdentifier( ActorIdentifier identifier )
|
||||
{
|
||||
if( identifier.Type != IdentifierType.Special )
|
||||
{
|
||||
return ( identifier, SpecialResult.Invalid );
|
||||
}
|
||||
|
||||
if( _actorManager.ResolvePartyBannerPlayer( identifier.Special, out var id ) )
|
||||
{
|
||||
return ( id, SpecialResult.PartyBanner );
|
||||
}
|
||||
|
||||
if( _actorManager.ResolvePvPBannerPlayer( identifier.Special, out id ) )
|
||||
{
|
||||
return ( id, SpecialResult.PvPBanner );
|
||||
}
|
||||
|
||||
if( _actorManager.ResolveMahjongPlayer( identifier.Special, out id ) )
|
||||
{
|
||||
return ( id, SpecialResult.Mahjong );
|
||||
}
|
||||
|
||||
switch( identifier.Special )
|
||||
{
|
||||
case ScreenActor.CharacterScreen when Penumbra.Config.UseCharacterCollectionInMainWindow: return ( _actorManager.GetCurrentPlayer(), SpecialResult.CharacterScreen );
|
||||
case ScreenActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn: return ( _actorManager.GetCurrentPlayer(), SpecialResult.FittingRoom );
|
||||
case ScreenActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn: return ( _actorManager.GetCurrentPlayer(), SpecialResult.DyePreview );
|
||||
case ScreenActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards: return ( _actorManager.GetCurrentPlayer(), SpecialResult.Portrait );
|
||||
case ScreenActor.ExamineScreen:
|
||||
{
|
||||
identifier = _actorManager.GetInspectPlayer();
|
||||
if( identifier.IsValid )
|
||||
{
|
||||
return ( Penumbra.Config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Inspect );
|
||||
}
|
||||
|
||||
identifier = _actorManager.GetCardPlayer();
|
||||
if( identifier.IsValid )
|
||||
{
|
||||
return ( Penumbra.Config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Card );
|
||||
}
|
||||
|
||||
return Penumbra.Config.UseCharacterCollectionInTryOn ? ( _actorManager.GetGlamourPlayer(), SpecialResult.Glamour ) : ( identifier, SpecialResult.Invalid );
|
||||
}
|
||||
default: return ( identifier, SpecialResult.Invalid );
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection )
|
||||
=> TryGetCollection( _actorManager.FromObject( gameObject, true, false, false ), out collection );
|
||||
|
||||
public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection )
|
||||
=> TryGetCollection( _actorManager.FromObject( gameObject, out _, true, false, false ), out collection );
|
||||
|
||||
private bool CheckWorlds( ActorIdentifier identifier, out ModCollection? collection )
|
||||
{
|
||||
if( !identifier.IsValid )
|
||||
{
|
||||
collection = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if( _individuals.TryGetValue( identifier, out collection ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
identifier = _actorManager.CreateIndividualUnchecked( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId );
|
||||
if( identifier.IsValid && _individuals.TryGetValue( identifier, out collection ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
collection = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
248
Penumbra/Collections/Manager/IndividualCollections.cs
Normal file
248
Penumbra/Collections/Manager/IndividualCollections.cs
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using OtterGui.Filesystem;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace Penumbra.Collections.Manager;
|
||||
|
||||
public sealed partial class IndividualCollections
|
||||
{
|
||||
private readonly ActorManager _actorManager;
|
||||
private readonly List<(string DisplayName, IReadOnlyList<ActorIdentifier> Identifiers, ModCollection Collection)> _assignments = new();
|
||||
private readonly Dictionary<ActorIdentifier, ModCollection> _individuals = new();
|
||||
|
||||
public IReadOnlyList<(string DisplayName, IReadOnlyList<ActorIdentifier> Identifiers, ModCollection Collection)> Assignments
|
||||
=> _assignments;
|
||||
|
||||
// TODO
|
||||
public IndividualCollections(ActorService actorManager)
|
||||
=> _actorManager = actorManager.AwaitedService;
|
||||
|
||||
public IndividualCollections(ActorManager actorManager)
|
||||
=> _actorManager = actorManager;
|
||||
|
||||
public enum AddResult
|
||||
{
|
||||
Valid,
|
||||
AlreadySet,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
public bool TryGetValue(ActorIdentifier identifier, [NotNullWhen(true)] out ModCollection? collection)
|
||||
{
|
||||
lock (_individuals)
|
||||
{
|
||||
return _individuals.TryGetValue(identifier, out collection);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ContainsKey(ActorIdentifier identifier)
|
||||
{
|
||||
lock (_individuals)
|
||||
{
|
||||
return _individuals.ContainsKey(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
public AddResult CanAdd(params ActorIdentifier[] identifiers)
|
||||
{
|
||||
if (identifiers.Length == 0)
|
||||
return AddResult.Invalid;
|
||||
|
||||
if (identifiers.Any(i => !i.IsValid))
|
||||
return AddResult.Invalid;
|
||||
|
||||
bool set;
|
||||
lock (_individuals)
|
||||
{
|
||||
set = identifiers.Any(_individuals.ContainsKey);
|
||||
}
|
||||
|
||||
return set ? AddResult.AlreadySet : AddResult.Valid;
|
||||
}
|
||||
|
||||
public AddResult CanAdd(IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IEnumerable<uint> dataIds,
|
||||
out ActorIdentifier[] identifiers)
|
||||
{
|
||||
identifiers = Array.Empty<ActorIdentifier>();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case IdentifierType.Player:
|
||||
if (!ByteString.FromString(name, out var playerName))
|
||||
return AddResult.Invalid;
|
||||
|
||||
identifiers = new[]
|
||||
{
|
||||
_actorManager.CreatePlayer(playerName, homeWorld),
|
||||
};
|
||||
break;
|
||||
case IdentifierType.Retainer:
|
||||
if (!ByteString.FromString(name, out var retainerName))
|
||||
return AddResult.Invalid;
|
||||
|
||||
identifiers = new[]
|
||||
{
|
||||
_actorManager.CreateRetainer(retainerName, 0),
|
||||
};
|
||||
break;
|
||||
case IdentifierType.Owned:
|
||||
if (!ByteString.FromString(name, out var ownerName))
|
||||
return AddResult.Invalid;
|
||||
|
||||
identifiers = dataIds.Select(id => _actorManager.CreateOwned(ownerName, homeWorld, kind, id)).ToArray();
|
||||
break;
|
||||
case IdentifierType.Npc:
|
||||
identifiers = dataIds
|
||||
.Select(id => _actorManager.CreateIndividual(IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, id)).ToArray();
|
||||
break;
|
||||
default:
|
||||
identifiers = Array.Empty<ActorIdentifier>();
|
||||
break;
|
||||
}
|
||||
|
||||
return CanAdd(identifiers);
|
||||
}
|
||||
|
||||
public ActorIdentifier[] GetGroup(ActorIdentifier identifier)
|
||||
{
|
||||
if (!identifier.IsValid)
|
||||
return Array.Empty<ActorIdentifier>();
|
||||
|
||||
static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier)
|
||||
{
|
||||
var name = manager.Data.ToName(identifier.Kind, identifier.DataId);
|
||||
var table = identifier.Kind switch
|
||||
{
|
||||
ObjectKind.BattleNpc => manager.Data.BNpcs,
|
||||
ObjectKind.EventNpc => manager.Data.ENpcs,
|
||||
ObjectKind.Companion => manager.Data.Companions,
|
||||
ObjectKind.MountType => manager.Data.Mounts,
|
||||
(ObjectKind)15 => manager.Data.Ornaments,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
return table.Where(kvp => kvp.Value == name)
|
||||
.Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld, identifier.Kind,
|
||||
kvp.Key)).ToArray();
|
||||
}
|
||||
|
||||
return identifier.Type switch
|
||||
{
|
||||
IdentifierType.Player => new[]
|
||||
{
|
||||
identifier.CreatePermanent(),
|
||||
},
|
||||
IdentifierType.Special => new[]
|
||||
{
|
||||
identifier,
|
||||
},
|
||||
IdentifierType.Retainer => new[]
|
||||
{
|
||||
identifier.CreatePermanent(),
|
||||
},
|
||||
IdentifierType.Owned => CreateNpcs(_actorManager, identifier.CreatePermanent()),
|
||||
IdentifierType.Npc => CreateNpcs(_actorManager, identifier),
|
||||
_ => Array.Empty<ActorIdentifier>(),
|
||||
};
|
||||
}
|
||||
|
||||
internal bool Add(ActorIdentifier[] identifiers, ModCollection collection)
|
||||
{
|
||||
if (identifiers.Length == 0 || !identifiers[0].IsValid)
|
||||
return false;
|
||||
|
||||
var name = DisplayString(identifiers[0]);
|
||||
return Add(name, identifiers, collection);
|
||||
}
|
||||
|
||||
private bool Add(string displayName, ActorIdentifier[] identifiers, ModCollection collection)
|
||||
{
|
||||
if (CanAdd(identifiers) != AddResult.Valid
|
||||
|| displayName.Length == 0
|
||||
|| _assignments.Any(a => a.DisplayName.Equals(displayName, StringComparison.OrdinalIgnoreCase)))
|
||||
return false;
|
||||
|
||||
for (var i = 0; i < identifiers.Length; ++i)
|
||||
{
|
||||
identifiers[i] = identifiers[i].CreatePermanent();
|
||||
lock (_individuals)
|
||||
{
|
||||
_individuals.Add(identifiers[i], collection);
|
||||
}
|
||||
}
|
||||
|
||||
_assignments.Add((displayName, identifiers, collection));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool ChangeCollection(ActorIdentifier identifier, ModCollection newCollection)
|
||||
=> ChangeCollection(DisplayString(identifier), newCollection);
|
||||
|
||||
internal bool ChangeCollection(string displayName, ModCollection newCollection)
|
||||
=> ChangeCollection(_assignments.FindIndex(t => t.DisplayName.Equals(displayName, StringComparison.OrdinalIgnoreCase)), newCollection);
|
||||
|
||||
internal bool ChangeCollection(int displayIndex, ModCollection newCollection)
|
||||
{
|
||||
if (displayIndex < 0 || displayIndex >= _assignments.Count || _assignments[displayIndex].Collection == newCollection)
|
||||
return false;
|
||||
|
||||
_assignments[displayIndex] = _assignments[displayIndex] with { Collection = newCollection };
|
||||
lock (_individuals)
|
||||
{
|
||||
foreach (var identifier in _assignments[displayIndex].Identifiers)
|
||||
_individuals[identifier] = newCollection;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool Delete(ActorIdentifier identifier)
|
||||
=> Delete(Index(identifier));
|
||||
|
||||
internal bool Delete(string displayName)
|
||||
=> Delete(Index(displayName));
|
||||
|
||||
internal bool Delete(int displayIndex)
|
||||
{
|
||||
if (displayIndex < 0 || displayIndex >= _assignments.Count)
|
||||
return false;
|
||||
|
||||
var (name, identifiers, _) = _assignments[displayIndex];
|
||||
_assignments.RemoveAt(displayIndex);
|
||||
lock (_individuals)
|
||||
{
|
||||
foreach (var identifier in identifiers)
|
||||
_individuals.Remove(identifier);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool Move(int from, int to)
|
||||
=> _assignments.Move(from, to);
|
||||
|
||||
internal int Index(string displayName)
|
||||
=> _assignments.FindIndex(t => t.DisplayName.Equals(displayName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
internal int Index(ActorIdentifier identifier)
|
||||
=> identifier.IsValid ? Index(DisplayString(identifier)) : -1;
|
||||
|
||||
private string DisplayString(ActorIdentifier identifier)
|
||||
{
|
||||
return identifier.Type switch
|
||||
{
|
||||
IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})",
|
||||
IdentifierType.Retainer => $"{identifier.PlayerName} (Retainer)",
|
||||
IdentifierType.Owned =>
|
||||
$"{identifier.PlayerName} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})'s {_actorManager.Data.ToName(identifier.Kind, identifier.DataId)}",
|
||||
IdentifierType.Npc => $"{_actorManager.Data.ToName(identifier.Kind, identifier.DataId)} ({identifier.Kind.ToName()})",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
121
Penumbra/Collections/Manager/TempCollectionManager.cs
Normal file
121
Penumbra/Collections/Manager/TempCollectionManager.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Penumbra.Api;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace Penumbra.Collections.Manager;
|
||||
|
||||
public class TempCollectionManager : IDisposable
|
||||
{
|
||||
public int GlobalChangeCounter { get; private set; } = 0;
|
||||
public readonly IndividualCollections Collections;
|
||||
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly CollectionStorage _storage;
|
||||
private readonly Dictionary<string, ModCollection> _customCollections = new();
|
||||
|
||||
public TempCollectionManager(CommunicatorService communicator, ActorService actors, CollectionStorage storage)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_storage = storage;
|
||||
Collections = new IndividualCollections(actors);
|
||||
|
||||
_communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.TemporaryGlobalModChange.Unsubscribe(OnGlobalModChange);
|
||||
}
|
||||
|
||||
private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed)
|
||||
=> TempModManager.OnGlobalModChange(_customCollections.Values, mod, created, removed);
|
||||
|
||||
public int Count
|
||||
=> _customCollections.Count;
|
||||
|
||||
public IEnumerable<ModCollection> Values
|
||||
=> _customCollections.Values;
|
||||
|
||||
public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection)
|
||||
=> _customCollections.TryGetValue(name.ToLowerInvariant(), out collection);
|
||||
|
||||
public string CreateTemporaryCollection(string name)
|
||||
{
|
||||
if (_storage.ByName(name, out _))
|
||||
return string.Empty;
|
||||
|
||||
if (GlobalChangeCounter == int.MaxValue)
|
||||
GlobalChangeCounter = 0;
|
||||
var collection = ModCollection.CreateNewTemporary(name, GlobalChangeCounter++);
|
||||
if (_customCollections.TryAdd(collection.Name.ToLowerInvariant(), collection))
|
||||
return collection.Name;
|
||||
|
||||
collection.ClearCache();
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public bool RemoveTemporaryCollection(string collectionName)
|
||||
{
|
||||
if (!_customCollections.Remove(collectionName.ToLowerInvariant(), out var collection))
|
||||
return false;
|
||||
|
||||
GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0);
|
||||
collection.ClearCache();
|
||||
for (var i = 0; i < Collections.Count; ++i)
|
||||
{
|
||||
if (Collections[i].Collection != collection)
|
||||
continue;
|
||||
|
||||
_communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName);
|
||||
Collections.Delete(i--);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool AddIdentifier(ModCollection collection, params ActorIdentifier[] identifiers)
|
||||
{
|
||||
if (Collections.Add(identifiers, collection))
|
||||
{
|
||||
_communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, Collections.Last().DisplayName);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool AddIdentifier(string collectionName, params ActorIdentifier[] identifiers)
|
||||
{
|
||||
if (!_customCollections.TryGetValue(collectionName.ToLowerInvariant(), out var collection))
|
||||
return false;
|
||||
|
||||
return AddIdentifier(collection, identifiers);
|
||||
}
|
||||
|
||||
public bool AddIdentifier(string collectionName, string characterName, ushort worldId = ushort.MaxValue)
|
||||
{
|
||||
if (!ByteString.FromString(characterName, out var byteString, false))
|
||||
return false;
|
||||
|
||||
var identifier = Penumbra.Actors.CreatePlayer(byteString, worldId);
|
||||
if (!identifier.IsValid)
|
||||
return false;
|
||||
|
||||
return AddIdentifier(collectionName, identifier);
|
||||
}
|
||||
|
||||
internal bool RemoveByCharacterName(string characterName, ushort worldId = ushort.MaxValue)
|
||||
{
|
||||
if (!ByteString.FromString(characterName, out var byteString, false))
|
||||
return false;
|
||||
|
||||
var identifier = Penumbra.Actors.CreatePlayer(byteString, worldId);
|
||||
return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Name);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue