Fix some bugs and start work on new collections tab.

This commit is contained in:
Ottermandias 2023-04-18 18:44:53 +02:00
parent e9fc57022e
commit fba5bc6820
26 changed files with 1346 additions and 717 deletions

View file

@ -221,6 +221,16 @@ public static class RaceEnumExtensions
}; };
} }
public static string ToShortName(this SubRace subRace)
{
return subRace switch
{
SubRace.SeekerOfTheSun => "Sunseeker",
SubRace.KeeperOfTheMoon => "Moonkeeper",
_ => subRace.ToName(),
};
}
public static bool FitsRace(this SubRace subRace, Race race) public static bool FitsRace(this SubRace subRace, Race race)
=> subRace.ToRace() == race; => subRace.ToRace() == race;

View file

@ -43,7 +43,7 @@ public class CollectionCacheManager : IDisposable
MetaFileManager = metaFileManager; MetaFileManager = metaFileManager;
_active = active; _active = active;
_communicator.CollectionChange.Subscribe(OnCollectionChange); _communicator.CollectionChange.Subscribe(OnCollectionChange, -100);
_communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100); _communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100);
_communicator.ModPathChanged.Subscribe(OnModChangeRemoval, 100); _communicator.ModPathChanged.Subscribe(OnModChangeRemoval, 100);
_communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange); _communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange);

View file

@ -13,24 +13,35 @@ using Penumbra.Util;
namespace Penumbra.Collections.Manager; namespace Penumbra.Collections.Manager;
public class ActiveCollectionData
{
public ModCollection Current { get; internal set; } = ModCollection.Empty;
public ModCollection Default { get; internal set; } = ModCollection.Empty;
public ModCollection Interface { get; internal set; } = ModCollection.Empty;
public readonly ModCollection?[] SpecialCollections = new ModCollection?[Enum.GetValues<Api.Enums.ApiCollectionType>().Length - 3];
}
public class ActiveCollections : ISavable, IDisposable public class ActiveCollections : ISavable, IDisposable
{ {
public const int Version = 1; public const int Version = 1;
private readonly CollectionStorage _storage; private readonly CollectionStorage _storage;
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
private readonly SaveService _saveService; private readonly SaveService _saveService;
private readonly ActiveCollectionData _data;
public ActiveCollections(Configuration config, CollectionStorage storage, ActorService actors, CommunicatorService communicator, SaveService saveService) public ActiveCollections(Configuration config, CollectionStorage storage, ActorService actors, CommunicatorService communicator, SaveService saveService, ActiveCollectionData data)
{ {
_storage = storage; _storage = storage;
_communicator = communicator; _communicator = communicator;
_saveService = saveService; _saveService = saveService;
_data = data;
Current = storage.DefaultNamed; Current = storage.DefaultNamed;
Default = storage.DefaultNamed; Default = storage.DefaultNamed;
Interface = storage.DefaultNamed; Interface = storage.DefaultNamed;
Individuals = new IndividualCollections(actors.AwaitedService, config); Individuals = new IndividualCollections(actors.AwaitedService, config);
_communicator.CollectionChange.Subscribe(OnCollectionChange); _communicator.CollectionChange.Subscribe(OnCollectionChange, -100);
LoadCollections(); LoadCollections();
UpdateCurrentCollectionInUse(); UpdateCurrentCollectionInUse();
} }
@ -39,16 +50,28 @@ public class ActiveCollections : ISavable, IDisposable
=> _communicator.CollectionChange.Unsubscribe(OnCollectionChange); => _communicator.CollectionChange.Unsubscribe(OnCollectionChange);
/// <summary> The collection currently selected for changing settings. </summary> /// <summary> The collection currently selected for changing settings. </summary>
public ModCollection Current { get; private set; } public ModCollection Current
{
get => _data.Current;
private set => _data.Current = value;
}
/// <summary> Whether the currently selected collection is used either directly via assignment or via inheritance. </summary> /// <summary> Whether the currently selected collection is used either directly via assignment or via inheritance. </summary>
public bool CurrentCollectionInUse { get; private set; } public bool CurrentCollectionInUse { get; private set; }
/// <summary> The collection used for general file redirections and all characters not specifically named. </summary> /// <summary> The collection used for general file redirections and all characters not specifically named. </summary>
public ModCollection Default { get; private set; } public ModCollection Default
{
get => _data.Default;
private set => _data.Default = value;
}
/// <summary> The collection used for all files categorized as UI files. </summary> /// <summary> The collection used for all files categorized as UI files. </summary>
public ModCollection Interface { get; private set; } public ModCollection Interface
{
get => _data.Interface;
private set => _data.Interface = value;
}
/// <summary> The list of individual assignments. </summary> /// <summary> The list of individual assignments. </summary>
public readonly IndividualCollections Individuals; public readonly IndividualCollections Individuals;
@ -58,16 +81,17 @@ public class ActiveCollections : ISavable, IDisposable
=> Individuals.TryGetCollection(identifier, out var c) ? c : Default; => Individuals.TryGetCollection(identifier, out var c) ? c : Default;
/// <summary> The list of group assignments. </summary> /// <summary> The list of group assignments. </summary>
private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues<Api.Enums.ApiCollectionType>().Length - 3]; private ModCollection?[] SpecialCollections
=> _data.SpecialCollections;
/// <summary> Return all actually assigned group assignments. </summary> /// <summary> Return all actually assigned group assignments. </summary>
public IEnumerable<KeyValuePair<CollectionType, ModCollection>> SpecialAssignments public IEnumerable<KeyValuePair<CollectionType, ModCollection>> SpecialAssignments
{ {
get get
{ {
for (var i = 0; i < _specialCollections.Length; ++i) for (var i = 0; i < SpecialCollections.Length; ++i)
{ {
var collection = _specialCollections[i]; var collection = SpecialCollections[i];
if (collection != null) if (collection != null)
yield return new KeyValuePair<CollectionType, ModCollection>((CollectionType)i, collection); yield return new KeyValuePair<CollectionType, ModCollection>((CollectionType)i, collection);
} }
@ -82,7 +106,7 @@ public class ActiveCollections : ISavable, IDisposable
public ModCollection? ByType(CollectionType type, ActorIdentifier identifier) public ModCollection? ByType(CollectionType type, ActorIdentifier identifier)
{ {
if (type.IsSpecial()) if (type.IsSpecial())
return _specialCollections[(int)type]; return SpecialCollections[(int)type];
return type switch return type switch
{ {
@ -94,13 +118,13 @@ public class ActiveCollections : ISavable, IDisposable
}; };
} }
/// <summary> Create a special collection if it does not exist and set it to Empty. </summary> /// <summary> Create a special collection if it does not exist and set it to the current default. </summary>
public bool CreateSpecialCollection(CollectionType collectionType) public bool CreateSpecialCollection(CollectionType collectionType)
{ {
if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null) if (!collectionType.IsSpecial() || SpecialCollections[(int)collectionType] != null)
return false; return false;
_specialCollections[(int)collectionType] = Default; SpecialCollections[(int)collectionType] = Default;
_communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty); _communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty);
return true; return true;
} }
@ -111,11 +135,11 @@ public class ActiveCollections : ISavable, IDisposable
if (!collectionType.IsSpecial()) if (!collectionType.IsSpecial())
return; return;
var old = _specialCollections[(int)collectionType]; var old = SpecialCollections[(int)collectionType];
if (old == null) if (old == null)
return; return;
_specialCollections[(int)collectionType] = null; SpecialCollections[(int)collectionType] = null;
_communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty); _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty);
} }
@ -144,7 +168,38 @@ public class ActiveCollections : ISavable, IDisposable
_saveService.QueueSave(this); _saveService.QueueSave(this);
} }
/// <summary> Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. </summary> /// <summary> Set and create an active collection, can be used to set Default, Current, Interface, Special, or Individual collections. </summary>
public void SetCollection(ModCollection? collection, CollectionType collectionType, ActorIdentifier[] identifiers)
{
if (collectionType is CollectionType.Individual && identifiers.Length > 0 && identifiers[0].IsValid)
{
var idx = Individuals.Index(identifiers[0]);
if (idx >= 0)
{
if (collection == null)
RemoveIndividualCollection(idx);
else
SetCollection(collection, collectionType, idx);
}
else if (collection != null)
{
CreateIndividualCollection(identifiers);
SetCollection(collection, CollectionType.Individual, Individuals.Count - 1);
}
}
else
{
if (collection == null)
RemoveSpecialCollection(collectionType);
else
{
CreateSpecialCollection(collectionType);
SetCollection(collection, collectionType);
}
}
}
/// <summary> Set an 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) public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1)
{ {
var oldCollection = collectionType switch var oldCollection = collectionType switch
@ -154,7 +209,7 @@ public class ActiveCollections : ISavable, IDisposable
CollectionType.Current => Current, CollectionType.Current => Current,
CollectionType.Individual when individualIndex >= 0 && individualIndex < Individuals.Count => Individuals[individualIndex].Collection, CollectionType.Individual when individualIndex >= 0 && individualIndex < Individuals.Count => Individuals[individualIndex].Collection,
CollectionType.Individual => null, CollectionType.Individual => null,
_ when collectionType.IsSpecial() => _specialCollections[(int)collectionType] ?? Default, _ when collectionType.IsSpecial() => SpecialCollections[(int)collectionType] ?? Default,
_ => null, _ => null,
}; };
@ -178,7 +233,7 @@ public class ActiveCollections : ISavable, IDisposable
break; break;
default: default:
_specialCollections[(int)collectionType] = collection; SpecialCollections[(int)collectionType] = collection;
break; break;
} }
@ -205,7 +260,7 @@ public class ActiveCollections : ISavable, IDisposable
{ nameof(Interface), Interface.Name }, { nameof(Interface), Interface.Name },
{ nameof(Current), Current.Name }, { nameof(Current), Current.Name },
}; };
foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) foreach (var (type, collection) in SpecialCollections.WithIndex().Where(p => p.Value != null)
.Select(p => ((CollectionType)p.Index, p.Value!))) .Select(p => ((CollectionType)p.Index, p.Value!)))
jObj.Add(type.ToString(), collection.Name); jObj.Add(type.ToString(), collection.Name);
@ -215,7 +270,7 @@ public class ActiveCollections : ISavable, IDisposable
} }
private void UpdateCurrentCollectionInUse() private void UpdateCurrentCollectionInUse()
=> CurrentCollectionInUse = _specialCollections => CurrentCollectionInUse = SpecialCollections
.OfType<ModCollection>() .OfType<ModCollection>()
.Prepend(Interface) .Prepend(Interface)
.Prepend(Default) .Prepend(Default)
@ -240,9 +295,9 @@ public class ActiveCollections : ISavable, IDisposable
if (oldCollection == Current) if (oldCollection == Current)
SetCollection(Default.Index > ModCollection.Empty.Index ? Default : _storage.DefaultNamed, CollectionType.Current); SetCollection(Default.Index > ModCollection.Empty.Index ? Default : _storage.DefaultNamed, CollectionType.Current);
for (var i = 0; i < _specialCollections.Length; ++i) for (var i = 0; i < SpecialCollections.Length; ++i)
{ {
if (oldCollection == _specialCollections[i]) if (oldCollection == SpecialCollections[i])
SetCollection(ModCollection.Empty, (CollectionType)i); SetCollection(ModCollection.Empty, (CollectionType)i);
} }
@ -329,7 +384,7 @@ public class ActiveCollections : ISavable, IDisposable
} }
else else
{ {
_specialCollections[(int)type] = typeCollection; SpecialCollections[(int)type] = typeCollection;
} }
} }
} }
@ -398,24 +453,6 @@ public class ActiveCollections : ISavable, IDisposable
: string.Empty; : 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; break;
// Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default. // 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.NonPlayerChild:

View file

@ -113,7 +113,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
return false; return false;
} }
var newCollection = duplicate?.Duplicate(name, _collections.Count) ?? ModCollection.CreateEmpty(name, _collections.Count); var newCollection = duplicate?.Duplicate(name, _collections.Count) ?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count);
_collections.Add(newCollection); _collections.Add(newCollection);
_saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection));
@ -200,7 +200,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
/// Does not check for uniqueness. /// Does not check for uniqueness.
/// </summary> /// </summary>
private static bool IsValidName(string name) private static bool IsValidName(string name)
=> name.Length > 0 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath()); => name.Length is > 0 and < 32 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath());
/// <summary> /// <summary>
/// Read all collection files in the Collection Directory. /// Read all collection files in the Collection Directory.

View file

@ -10,96 +10,96 @@ public enum CollectionType : byte
// Special Collections // Special Collections
Yourself = Api.Enums.ApiCollectionType.Yourself, Yourself = Api.Enums.ApiCollectionType.Yourself,
MalePlayerCharacter = Api.Enums.ApiCollectionType.MalePlayerCharacter, MalePlayerCharacter = Api.Enums.ApiCollectionType.MalePlayerCharacter,
FemalePlayerCharacter = Api.Enums.ApiCollectionType.FemalePlayerCharacter, FemalePlayerCharacter = Api.Enums.ApiCollectionType.FemalePlayerCharacter,
MaleNonPlayerCharacter = Api.Enums.ApiCollectionType.MaleNonPlayerCharacter, MaleNonPlayerCharacter = Api.Enums.ApiCollectionType.MaleNonPlayerCharacter,
FemaleNonPlayerCharacter = Api.Enums.ApiCollectionType.FemaleNonPlayerCharacter, FemaleNonPlayerCharacter = Api.Enums.ApiCollectionType.FemaleNonPlayerCharacter,
NonPlayerChild = Api.Enums.ApiCollectionType.NonPlayerChild, NonPlayerChild = Api.Enums.ApiCollectionType.NonPlayerChild,
NonPlayerElderly = Api.Enums.ApiCollectionType.NonPlayerElderly, NonPlayerElderly = Api.Enums.ApiCollectionType.NonPlayerElderly,
MaleMidlander = Api.Enums.ApiCollectionType.MaleMidlander, MaleMidlander = Api.Enums.ApiCollectionType.MaleMidlander,
FemaleMidlander = Api.Enums.ApiCollectionType.FemaleMidlander, FemaleMidlander = Api.Enums.ApiCollectionType.FemaleMidlander,
MaleHighlander = Api.Enums.ApiCollectionType.MaleHighlander, MaleHighlander = Api.Enums.ApiCollectionType.MaleHighlander,
FemaleHighlander = Api.Enums.ApiCollectionType.FemaleHighlander, FemaleHighlander = Api.Enums.ApiCollectionType.FemaleHighlander,
MaleWildwood = Api.Enums.ApiCollectionType.MaleWildwood, MaleWildwood = Api.Enums.ApiCollectionType.MaleWildwood,
FemaleWildwood = Api.Enums.ApiCollectionType.FemaleWildwood, FemaleWildwood = Api.Enums.ApiCollectionType.FemaleWildwood,
MaleDuskwight = Api.Enums.ApiCollectionType.MaleDuskwight, MaleDuskwight = Api.Enums.ApiCollectionType.MaleDuskwight,
FemaleDuskwight = Api.Enums.ApiCollectionType.FemaleDuskwight, FemaleDuskwight = Api.Enums.ApiCollectionType.FemaleDuskwight,
MalePlainsfolk = Api.Enums.ApiCollectionType.MalePlainsfolk, MalePlainsfolk = Api.Enums.ApiCollectionType.MalePlainsfolk,
FemalePlainsfolk = Api.Enums.ApiCollectionType.FemalePlainsfolk, FemalePlainsfolk = Api.Enums.ApiCollectionType.FemalePlainsfolk,
MaleDunesfolk = Api.Enums.ApiCollectionType.MaleDunesfolk, MaleDunesfolk = Api.Enums.ApiCollectionType.MaleDunesfolk,
FemaleDunesfolk = Api.Enums.ApiCollectionType.FemaleDunesfolk, FemaleDunesfolk = Api.Enums.ApiCollectionType.FemaleDunesfolk,
MaleSeekerOfTheSun = Api.Enums.ApiCollectionType.MaleSeekerOfTheSun, MaleSeekerOfTheSun = Api.Enums.ApiCollectionType.MaleSeekerOfTheSun,
FemaleSeekerOfTheSun = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSun, FemaleSeekerOfTheSun = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSun,
MaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoon, MaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoon,
FemaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoon, FemaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoon,
MaleSeawolf = Api.Enums.ApiCollectionType.MaleSeawolf, MaleSeawolf = Api.Enums.ApiCollectionType.MaleSeawolf,
FemaleSeawolf = Api.Enums.ApiCollectionType.FemaleSeawolf, FemaleSeawolf = Api.Enums.ApiCollectionType.FemaleSeawolf,
MaleHellsguard = Api.Enums.ApiCollectionType.MaleHellsguard, MaleHellsguard = Api.Enums.ApiCollectionType.MaleHellsguard,
FemaleHellsguard = Api.Enums.ApiCollectionType.FemaleHellsguard, FemaleHellsguard = Api.Enums.ApiCollectionType.FemaleHellsguard,
MaleRaen = Api.Enums.ApiCollectionType.MaleRaen, MaleRaen = Api.Enums.ApiCollectionType.MaleRaen,
FemaleRaen = Api.Enums.ApiCollectionType.FemaleRaen, FemaleRaen = Api.Enums.ApiCollectionType.FemaleRaen,
MaleXaela = Api.Enums.ApiCollectionType.MaleXaela, MaleXaela = Api.Enums.ApiCollectionType.MaleXaela,
FemaleXaela = Api.Enums.ApiCollectionType.FemaleXaela, FemaleXaela = Api.Enums.ApiCollectionType.FemaleXaela,
MaleHelion = Api.Enums.ApiCollectionType.MaleHelion, MaleHelion = Api.Enums.ApiCollectionType.MaleHelion,
FemaleHelion = Api.Enums.ApiCollectionType.FemaleHelion, FemaleHelion = Api.Enums.ApiCollectionType.FemaleHelion,
MaleLost = Api.Enums.ApiCollectionType.MaleLost, MaleLost = Api.Enums.ApiCollectionType.MaleLost,
FemaleLost = Api.Enums.ApiCollectionType.FemaleLost, FemaleLost = Api.Enums.ApiCollectionType.FemaleLost,
MaleRava = Api.Enums.ApiCollectionType.MaleRava, MaleRava = Api.Enums.ApiCollectionType.MaleRava,
FemaleRava = Api.Enums.ApiCollectionType.FemaleRava, FemaleRava = Api.Enums.ApiCollectionType.FemaleRava,
MaleVeena = Api.Enums.ApiCollectionType.MaleVeena, MaleVeena = Api.Enums.ApiCollectionType.MaleVeena,
FemaleVeena = Api.Enums.ApiCollectionType.FemaleVeena, FemaleVeena = Api.Enums.ApiCollectionType.FemaleVeena,
MaleMidlanderNpc = Api.Enums.ApiCollectionType.MaleMidlanderNpc, MaleMidlanderNpc = Api.Enums.ApiCollectionType.MaleMidlanderNpc,
FemaleMidlanderNpc = Api.Enums.ApiCollectionType.FemaleMidlanderNpc, FemaleMidlanderNpc = Api.Enums.ApiCollectionType.FemaleMidlanderNpc,
MaleHighlanderNpc = Api.Enums.ApiCollectionType.MaleHighlanderNpc, MaleHighlanderNpc = Api.Enums.ApiCollectionType.MaleHighlanderNpc,
FemaleHighlanderNpc = Api.Enums.ApiCollectionType.FemaleHighlanderNpc, FemaleHighlanderNpc = Api.Enums.ApiCollectionType.FemaleHighlanderNpc,
MaleWildwoodNpc = Api.Enums.ApiCollectionType.MaleWildwoodNpc, MaleWildwoodNpc = Api.Enums.ApiCollectionType.MaleWildwoodNpc,
FemaleWildwoodNpc = Api.Enums.ApiCollectionType.FemaleWildwoodNpc, FemaleWildwoodNpc = Api.Enums.ApiCollectionType.FemaleWildwoodNpc,
MaleDuskwightNpc = Api.Enums.ApiCollectionType.MaleDuskwightNpc, MaleDuskwightNpc = Api.Enums.ApiCollectionType.MaleDuskwightNpc,
FemaleDuskwightNpc = Api.Enums.ApiCollectionType.FemaleDuskwightNpc, FemaleDuskwightNpc = Api.Enums.ApiCollectionType.FemaleDuskwightNpc,
MalePlainsfolkNpc = Api.Enums.ApiCollectionType.MalePlainsfolkNpc, MalePlainsfolkNpc = Api.Enums.ApiCollectionType.MalePlainsfolkNpc,
FemalePlainsfolkNpc = Api.Enums.ApiCollectionType.FemalePlainsfolkNpc, FemalePlainsfolkNpc = Api.Enums.ApiCollectionType.FemalePlainsfolkNpc,
MaleDunesfolkNpc = Api.Enums.ApiCollectionType.MaleDunesfolkNpc, MaleDunesfolkNpc = Api.Enums.ApiCollectionType.MaleDunesfolkNpc,
FemaleDunesfolkNpc = Api.Enums.ApiCollectionType.FemaleDunesfolkNpc, FemaleDunesfolkNpc = Api.Enums.ApiCollectionType.FemaleDunesfolkNpc,
MaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.MaleSeekerOfTheSunNpc, MaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.MaleSeekerOfTheSunNpc,
FemaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSunNpc, FemaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSunNpc,
MaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoonNpc, MaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoonNpc,
FemaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoonNpc, FemaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoonNpc,
MaleSeawolfNpc = Api.Enums.ApiCollectionType.MaleSeawolfNpc, MaleSeawolfNpc = Api.Enums.ApiCollectionType.MaleSeawolfNpc,
FemaleSeawolfNpc = Api.Enums.ApiCollectionType.FemaleSeawolfNpc, FemaleSeawolfNpc = Api.Enums.ApiCollectionType.FemaleSeawolfNpc,
MaleHellsguardNpc = Api.Enums.ApiCollectionType.MaleHellsguardNpc, MaleHellsguardNpc = Api.Enums.ApiCollectionType.MaleHellsguardNpc,
FemaleHellsguardNpc = Api.Enums.ApiCollectionType.FemaleHellsguardNpc, FemaleHellsguardNpc = Api.Enums.ApiCollectionType.FemaleHellsguardNpc,
MaleRaenNpc = Api.Enums.ApiCollectionType.MaleRaenNpc, MaleRaenNpc = Api.Enums.ApiCollectionType.MaleRaenNpc,
FemaleRaenNpc = Api.Enums.ApiCollectionType.FemaleRaenNpc, FemaleRaenNpc = Api.Enums.ApiCollectionType.FemaleRaenNpc,
MaleXaelaNpc = Api.Enums.ApiCollectionType.MaleXaelaNpc, MaleXaelaNpc = Api.Enums.ApiCollectionType.MaleXaelaNpc,
FemaleXaelaNpc = Api.Enums.ApiCollectionType.FemaleXaelaNpc, FemaleXaelaNpc = Api.Enums.ApiCollectionType.FemaleXaelaNpc,
MaleHelionNpc = Api.Enums.ApiCollectionType.MaleHelionNpc, MaleHelionNpc = Api.Enums.ApiCollectionType.MaleHelionNpc,
FemaleHelionNpc = Api.Enums.ApiCollectionType.FemaleHelionNpc, FemaleHelionNpc = Api.Enums.ApiCollectionType.FemaleHelionNpc,
MaleLostNpc = Api.Enums.ApiCollectionType.MaleLostNpc, MaleLostNpc = Api.Enums.ApiCollectionType.MaleLostNpc,
FemaleLostNpc = Api.Enums.ApiCollectionType.FemaleLostNpc, FemaleLostNpc = Api.Enums.ApiCollectionType.FemaleLostNpc,
MaleRavaNpc = Api.Enums.ApiCollectionType.MaleRavaNpc, MaleRavaNpc = Api.Enums.ApiCollectionType.MaleRavaNpc,
FemaleRavaNpc = Api.Enums.ApiCollectionType.FemaleRavaNpc, FemaleRavaNpc = Api.Enums.ApiCollectionType.FemaleRavaNpc,
MaleVeenaNpc = Api.Enums.ApiCollectionType.MaleVeenaNpc, MaleVeenaNpc = Api.Enums.ApiCollectionType.MaleVeenaNpc,
FemaleVeenaNpc = Api.Enums.ApiCollectionType.FemaleVeenaNpc, FemaleVeenaNpc = Api.Enums.ApiCollectionType.FemaleVeenaNpc,
Default = Api.Enums.ApiCollectionType.Default, // The default collection was changed Default = Api.Enums.ApiCollectionType.Default, // The default collection was changed
Interface = Api.Enums.ApiCollectionType.Interface, // The ui collection was changed Interface = Api.Enums.ApiCollectionType.Interface, // The ui collection was changed
Current = Api.Enums.ApiCollectionType.Current, // The current collection was changed Current = Api.Enums.ApiCollectionType.Current, // The current collection was changed
Individual, // An individual collection was changed Individual, // An individual collection was changed
Inactive, // A collection was added or removed Inactive, // A collection was added or removed
Temporary, // A temporary collections was set or deleted via IPC Temporary, // A temporary collections was set or deleted via IPC
@ -111,202 +111,200 @@ public static class CollectionTypeExtensions
=> collectionType < CollectionType.Default; => collectionType < CollectionType.Default;
public static readonly (CollectionType, string, string)[] Special = Enum.GetValues<CollectionType>() public static readonly (CollectionType, string, string)[] Special = Enum.GetValues<CollectionType>()
.Where(IsSpecial) .Where(IsSpecial)
.Select(s => (s, s.ToName(), s.ToDescription())) .Select(s => (s, s.ToName(), s.ToDescription()))
.ToArray(); .ToArray();
public static CollectionType FromParts(Gender gender, bool npc) public static CollectionType FromParts(Gender gender, bool npc)
{ {
gender = gender switch gender = gender switch
{ {
Gender.MaleNpc => Gender.Male, Gender.MaleNpc => Gender.Male,
Gender.FemaleNpc => Gender.Female, Gender.FemaleNpc => Gender.Female,
_ => gender, _ => gender,
}; };
return (gender, npc) switch return (gender, npc) switch
{ {
(Gender.Male, false) => CollectionType.MalePlayerCharacter, (Gender.Male, false) => CollectionType.MalePlayerCharacter,
(Gender.Female, false) => CollectionType.FemalePlayerCharacter, (Gender.Female, false) => CollectionType.FemalePlayerCharacter,
(Gender.Male, true) => CollectionType.MaleNonPlayerCharacter, (Gender.Male, true) => CollectionType.MaleNonPlayerCharacter,
(Gender.Female, true) => CollectionType.FemaleNonPlayerCharacter, (Gender.Female, true) => CollectionType.FemaleNonPlayerCharacter,
_ => CollectionType.Inactive, _ => CollectionType.Inactive,
}; };
} }
// @formatter:off // @formatter:off
private static readonly IReadOnlyList<CollectionType> DefaultList = new[] { CollectionType.Default }; 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> MalePlayerList = new[] { CollectionType.MalePlayerCharacter, CollectionType.Default };
private static readonly IReadOnlyList<CollectionType> FemalePlayerList = new[] { CollectionType.FemalePlayerCharacter, 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> MaleNpcList = new[] { CollectionType.MaleNonPlayerCharacter, CollectionType.Default };
private static readonly IReadOnlyList<CollectionType> FemaleNpcList = new[] { CollectionType.FemaleNonPlayerCharacter, CollectionType.Default }; private static readonly IReadOnlyList<CollectionType> FemaleNpcList = new[] { CollectionType.FemaleNonPlayerCharacter, CollectionType.Default };
// @formatter:on // @formatter:on
/// <summary> A list of definite redundancy possibilities. </summary> /// <summary> A list of definite redundancy possibilities. </summary>
public static IReadOnlyList<CollectionType> InheritanceOrder(this CollectionType collectionType) public static IReadOnlyList<CollectionType> InheritanceOrder(this CollectionType collectionType)
=> collectionType switch => collectionType switch
{ {
CollectionType.Yourself => DefaultList, CollectionType.Yourself => DefaultList,
CollectionType.MalePlayerCharacter => DefaultList, CollectionType.MalePlayerCharacter => DefaultList,
CollectionType.FemalePlayerCharacter => DefaultList, CollectionType.FemalePlayerCharacter => DefaultList,
CollectionType.MaleNonPlayerCharacter => DefaultList, CollectionType.MaleNonPlayerCharacter => DefaultList,
CollectionType.FemaleNonPlayerCharacter => DefaultList, CollectionType.FemaleNonPlayerCharacter => DefaultList,
CollectionType.MaleMidlander => MalePlayerList, CollectionType.MaleMidlander => MalePlayerList,
CollectionType.FemaleMidlander => FemalePlayerList, CollectionType.FemaleMidlander => FemalePlayerList,
CollectionType.MaleHighlander => MalePlayerList, CollectionType.MaleHighlander => MalePlayerList,
CollectionType.FemaleHighlander => FemalePlayerList, CollectionType.FemaleHighlander => FemalePlayerList,
CollectionType.MaleWildwood => MalePlayerList, CollectionType.MaleWildwood => MalePlayerList,
CollectionType.FemaleWildwood => FemalePlayerList, CollectionType.FemaleWildwood => FemalePlayerList,
CollectionType.MaleDuskwight => MalePlayerList, CollectionType.MaleDuskwight => MalePlayerList,
CollectionType.FemaleDuskwight => FemalePlayerList, CollectionType.FemaleDuskwight => FemalePlayerList,
CollectionType.MalePlainsfolk => MalePlayerList, CollectionType.MalePlainsfolk => MalePlayerList,
CollectionType.FemalePlainsfolk => FemalePlayerList, CollectionType.FemalePlainsfolk => FemalePlayerList,
CollectionType.MaleDunesfolk => MalePlayerList, CollectionType.MaleDunesfolk => MalePlayerList,
CollectionType.FemaleDunesfolk => FemalePlayerList, CollectionType.FemaleDunesfolk => FemalePlayerList,
CollectionType.MaleSeekerOfTheSun => MalePlayerList, CollectionType.MaleSeekerOfTheSun => MalePlayerList,
CollectionType.FemaleSeekerOfTheSun => FemalePlayerList, CollectionType.FemaleSeekerOfTheSun => FemalePlayerList,
CollectionType.MaleKeeperOfTheMoon => MalePlayerList, CollectionType.MaleKeeperOfTheMoon => MalePlayerList,
CollectionType.FemaleKeeperOfTheMoon => FemalePlayerList, CollectionType.FemaleKeeperOfTheMoon => FemalePlayerList,
CollectionType.MaleSeawolf => MalePlayerList, CollectionType.MaleSeawolf => MalePlayerList,
CollectionType.FemaleSeawolf => FemalePlayerList, CollectionType.FemaleSeawolf => FemalePlayerList,
CollectionType.MaleHellsguard => MalePlayerList, CollectionType.MaleHellsguard => MalePlayerList,
CollectionType.FemaleHellsguard => FemalePlayerList, CollectionType.FemaleHellsguard => FemalePlayerList,
CollectionType.MaleRaen => MalePlayerList, CollectionType.MaleRaen => MalePlayerList,
CollectionType.FemaleRaen => FemalePlayerList, CollectionType.FemaleRaen => FemalePlayerList,
CollectionType.MaleXaela => MalePlayerList, CollectionType.MaleXaela => MalePlayerList,
CollectionType.FemaleXaela => FemalePlayerList, CollectionType.FemaleXaela => FemalePlayerList,
CollectionType.MaleHelion => MalePlayerList, CollectionType.MaleHelion => MalePlayerList,
CollectionType.FemaleHelion => FemalePlayerList, CollectionType.FemaleHelion => FemalePlayerList,
CollectionType.MaleLost => MalePlayerList, CollectionType.MaleLost => MalePlayerList,
CollectionType.FemaleLost => FemalePlayerList, CollectionType.FemaleLost => FemalePlayerList,
CollectionType.MaleRava => MalePlayerList, CollectionType.MaleRava => MalePlayerList,
CollectionType.FemaleRava => FemalePlayerList, CollectionType.FemaleRava => FemalePlayerList,
CollectionType.MaleVeena => MalePlayerList, CollectionType.MaleVeena => MalePlayerList,
CollectionType.FemaleVeena => FemalePlayerList, CollectionType.FemaleVeena => FemalePlayerList,
CollectionType.MaleMidlanderNpc => MaleNpcList, CollectionType.MaleMidlanderNpc => MaleNpcList,
CollectionType.FemaleMidlanderNpc => FemaleNpcList, CollectionType.FemaleMidlanderNpc => FemaleNpcList,
CollectionType.MaleHighlanderNpc => MaleNpcList, CollectionType.MaleHighlanderNpc => MaleNpcList,
CollectionType.FemaleHighlanderNpc => FemaleNpcList, CollectionType.FemaleHighlanderNpc => FemaleNpcList,
CollectionType.MaleWildwoodNpc => MaleNpcList, CollectionType.MaleWildwoodNpc => MaleNpcList,
CollectionType.FemaleWildwoodNpc => FemaleNpcList, CollectionType.FemaleWildwoodNpc => FemaleNpcList,
CollectionType.MaleDuskwightNpc => MaleNpcList, CollectionType.MaleDuskwightNpc => MaleNpcList,
CollectionType.FemaleDuskwightNpc => FemaleNpcList, CollectionType.FemaleDuskwightNpc => FemaleNpcList,
CollectionType.MalePlainsfolkNpc => MaleNpcList, CollectionType.MalePlainsfolkNpc => MaleNpcList,
CollectionType.FemalePlainsfolkNpc => FemaleNpcList, CollectionType.FemalePlainsfolkNpc => FemaleNpcList,
CollectionType.MaleDunesfolkNpc => MaleNpcList, CollectionType.MaleDunesfolkNpc => MaleNpcList,
CollectionType.FemaleDunesfolkNpc => FemaleNpcList, CollectionType.FemaleDunesfolkNpc => FemaleNpcList,
CollectionType.MaleSeekerOfTheSunNpc => MaleNpcList, CollectionType.MaleSeekerOfTheSunNpc => MaleNpcList,
CollectionType.FemaleSeekerOfTheSunNpc => FemaleNpcList, CollectionType.FemaleSeekerOfTheSunNpc => FemaleNpcList,
CollectionType.MaleKeeperOfTheMoonNpc => MaleNpcList, CollectionType.MaleKeeperOfTheMoonNpc => MaleNpcList,
CollectionType.FemaleKeeperOfTheMoonNpc => FemaleNpcList, CollectionType.FemaleKeeperOfTheMoonNpc => FemaleNpcList,
CollectionType.MaleSeawolfNpc => MaleNpcList, CollectionType.MaleSeawolfNpc => MaleNpcList,
CollectionType.FemaleSeawolfNpc => FemaleNpcList, CollectionType.FemaleSeawolfNpc => FemaleNpcList,
CollectionType.MaleHellsguardNpc => MaleNpcList, CollectionType.MaleHellsguardNpc => MaleNpcList,
CollectionType.FemaleHellsguardNpc => FemaleNpcList, CollectionType.FemaleHellsguardNpc => FemaleNpcList,
CollectionType.MaleRaenNpc => MaleNpcList, CollectionType.MaleRaenNpc => MaleNpcList,
CollectionType.FemaleRaenNpc => FemaleNpcList, CollectionType.FemaleRaenNpc => FemaleNpcList,
CollectionType.MaleXaelaNpc => MaleNpcList, CollectionType.MaleXaelaNpc => MaleNpcList,
CollectionType.FemaleXaelaNpc => FemaleNpcList, CollectionType.FemaleXaelaNpc => FemaleNpcList,
CollectionType.MaleHelionNpc => MaleNpcList, CollectionType.MaleHelionNpc => MaleNpcList,
CollectionType.FemaleHelionNpc => FemaleNpcList, CollectionType.FemaleHelionNpc => FemaleNpcList,
CollectionType.MaleLostNpc => MaleNpcList, CollectionType.MaleLostNpc => MaleNpcList,
CollectionType.FemaleLostNpc => FemaleNpcList, CollectionType.FemaleLostNpc => FemaleNpcList,
CollectionType.MaleRavaNpc => MaleNpcList, CollectionType.MaleRavaNpc => MaleNpcList,
CollectionType.FemaleRavaNpc => FemaleNpcList, CollectionType.FemaleRavaNpc => FemaleNpcList,
CollectionType.MaleVeenaNpc => MaleNpcList, CollectionType.MaleVeenaNpc => MaleNpcList,
CollectionType.FemaleVeenaNpc => FemaleNpcList, CollectionType.FemaleVeenaNpc => FemaleNpcList,
CollectionType.Individual => DefaultList, CollectionType.Individual => DefaultList,
_ => Array.Empty<CollectionType>(), _ => Array.Empty<CollectionType>(),
}; };
public static CollectionType FromParts(SubRace race, Gender gender, bool npc) public static CollectionType FromParts(SubRace race, Gender gender, bool npc)
{ {
gender = gender switch gender = gender switch
{ {
Gender.MaleNpc => Gender.Male, Gender.MaleNpc => Gender.Male,
Gender.FemaleNpc => Gender.Female, Gender.FemaleNpc => Gender.Female,
_ => gender, _ => gender,
}; };
return (race, gender, npc) switch return (race, gender, npc) switch
{ {
(SubRace.Midlander, Gender.Male, false) => CollectionType.MaleMidlander, (SubRace.Midlander, Gender.Male, false) => CollectionType.MaleMidlander,
(SubRace.Highlander, Gender.Male, false) => CollectionType.MaleHighlander, (SubRace.Highlander, Gender.Male, false) => CollectionType.MaleHighlander,
(SubRace.Wildwood, Gender.Male, false) => CollectionType.MaleWildwood, (SubRace.Wildwood, Gender.Male, false) => CollectionType.MaleWildwood,
(SubRace.Duskwight, Gender.Male, false) => CollectionType.MaleDuskwight, (SubRace.Duskwight, Gender.Male, false) => CollectionType.MaleDuskwight,
(SubRace.Plainsfolk, Gender.Male, false) => CollectionType.MalePlainsfolk, (SubRace.Plainsfolk, Gender.Male, false) => CollectionType.MalePlainsfolk,
(SubRace.Dunesfolk, Gender.Male, false) => CollectionType.MaleDunesfolk, (SubRace.Dunesfolk, Gender.Male, false) => CollectionType.MaleDunesfolk,
(SubRace.SeekerOfTheSun, Gender.Male, false) => CollectionType.MaleSeekerOfTheSun, (SubRace.SeekerOfTheSun, Gender.Male, false) => CollectionType.MaleSeekerOfTheSun,
(SubRace.KeeperOfTheMoon, Gender.Male, false) => CollectionType.MaleKeeperOfTheMoon, (SubRace.KeeperOfTheMoon, Gender.Male, false) => CollectionType.MaleKeeperOfTheMoon,
(SubRace.Seawolf, Gender.Male, false) => CollectionType.MaleSeawolf, (SubRace.Seawolf, Gender.Male, false) => CollectionType.MaleSeawolf,
(SubRace.Hellsguard, Gender.Male, false) => CollectionType.MaleHellsguard, (SubRace.Hellsguard, Gender.Male, false) => CollectionType.MaleHellsguard,
(SubRace.Raen, Gender.Male, false) => CollectionType.MaleRaen, (SubRace.Raen, Gender.Male, false) => CollectionType.MaleRaen,
(SubRace.Xaela, Gender.Male, false) => CollectionType.MaleXaela, (SubRace.Xaela, Gender.Male, false) => CollectionType.MaleXaela,
(SubRace.Helion, Gender.Male, false) => CollectionType.MaleHelion, (SubRace.Helion, Gender.Male, false) => CollectionType.MaleHelion,
(SubRace.Lost, Gender.Male, false) => CollectionType.MaleLost, (SubRace.Lost, Gender.Male, false) => CollectionType.MaleLost,
(SubRace.Rava, Gender.Male, false) => CollectionType.MaleRava, (SubRace.Rava, Gender.Male, false) => CollectionType.MaleRava,
(SubRace.Veena, Gender.Male, false) => CollectionType.MaleVeena, (SubRace.Veena, Gender.Male, false) => CollectionType.MaleVeena,
(SubRace.Midlander, Gender.Female, false) => CollectionType.FemaleMidlander, (SubRace.Midlander, Gender.Female, false) => CollectionType.FemaleMidlander,
(SubRace.Highlander, Gender.Female, false) => CollectionType.FemaleHighlander, (SubRace.Highlander, Gender.Female, false) => CollectionType.FemaleHighlander,
(SubRace.Wildwood, Gender.Female, false) => CollectionType.FemaleWildwood, (SubRace.Wildwood, Gender.Female, false) => CollectionType.FemaleWildwood,
(SubRace.Duskwight, Gender.Female, false) => CollectionType.FemaleDuskwight, (SubRace.Duskwight, Gender.Female, false) => CollectionType.FemaleDuskwight,
(SubRace.Plainsfolk, Gender.Female, false) => CollectionType.FemalePlainsfolk, (SubRace.Plainsfolk, Gender.Female, false) => CollectionType.FemalePlainsfolk,
(SubRace.Dunesfolk, Gender.Female, false) => CollectionType.FemaleDunesfolk, (SubRace.Dunesfolk, Gender.Female, false) => CollectionType.FemaleDunesfolk,
(SubRace.SeekerOfTheSun, Gender.Female, false) => CollectionType.FemaleSeekerOfTheSun, (SubRace.SeekerOfTheSun, Gender.Female, false) => CollectionType.FemaleSeekerOfTheSun,
(SubRace.KeeperOfTheMoon, Gender.Female, false) => CollectionType.FemaleKeeperOfTheMoon, (SubRace.KeeperOfTheMoon, Gender.Female, false) => CollectionType.FemaleKeeperOfTheMoon,
(SubRace.Seawolf, Gender.Female, false) => CollectionType.FemaleSeawolf, (SubRace.Seawolf, Gender.Female, false) => CollectionType.FemaleSeawolf,
(SubRace.Hellsguard, Gender.Female, false) => CollectionType.FemaleHellsguard, (SubRace.Hellsguard, Gender.Female, false) => CollectionType.FemaleHellsguard,
(SubRace.Raen, Gender.Female, false) => CollectionType.FemaleRaen, (SubRace.Raen, Gender.Female, false) => CollectionType.FemaleRaen,
(SubRace.Xaela, Gender.Female, false) => CollectionType.FemaleXaela, (SubRace.Xaela, Gender.Female, false) => CollectionType.FemaleXaela,
(SubRace.Helion, Gender.Female, false) => CollectionType.FemaleHelion, (SubRace.Helion, Gender.Female, false) => CollectionType.FemaleHelion,
(SubRace.Lost, Gender.Female, false) => CollectionType.FemaleLost, (SubRace.Lost, Gender.Female, false) => CollectionType.FemaleLost,
(SubRace.Rava, Gender.Female, false) => CollectionType.FemaleRava, (SubRace.Rava, Gender.Female, false) => CollectionType.FemaleRava,
(SubRace.Veena, Gender.Female, false) => CollectionType.FemaleVeena, (SubRace.Veena, Gender.Female, false) => CollectionType.FemaleVeena,
(SubRace.Midlander, Gender.Male, true) => CollectionType.MaleMidlanderNpc, (SubRace.Midlander, Gender.Male, true) => CollectionType.MaleMidlanderNpc,
(SubRace.Highlander, Gender.Male, true) => CollectionType.MaleHighlanderNpc, (SubRace.Highlander, Gender.Male, true) => CollectionType.MaleHighlanderNpc,
(SubRace.Wildwood, Gender.Male, true) => CollectionType.MaleWildwoodNpc, (SubRace.Wildwood, Gender.Male, true) => CollectionType.MaleWildwoodNpc,
(SubRace.Duskwight, Gender.Male, true) => CollectionType.MaleDuskwightNpc, (SubRace.Duskwight, Gender.Male, true) => CollectionType.MaleDuskwightNpc,
(SubRace.Plainsfolk, Gender.Male, true) => CollectionType.MalePlainsfolkNpc, (SubRace.Plainsfolk, Gender.Male, true) => CollectionType.MalePlainsfolkNpc,
(SubRace.Dunesfolk, Gender.Male, true) => CollectionType.MaleDunesfolkNpc, (SubRace.Dunesfolk, Gender.Male, true) => CollectionType.MaleDunesfolkNpc,
(SubRace.SeekerOfTheSun, Gender.Male, true) => CollectionType.MaleSeekerOfTheSunNpc, (SubRace.SeekerOfTheSun, Gender.Male, true) => CollectionType.MaleSeekerOfTheSunNpc,
(SubRace.KeeperOfTheMoon, Gender.Male, true) => CollectionType.MaleKeeperOfTheMoonNpc, (SubRace.KeeperOfTheMoon, Gender.Male, true) => CollectionType.MaleKeeperOfTheMoonNpc,
(SubRace.Seawolf, Gender.Male, true) => CollectionType.MaleSeawolfNpc, (SubRace.Seawolf, Gender.Male, true) => CollectionType.MaleSeawolfNpc,
(SubRace.Hellsguard, Gender.Male, true) => CollectionType.MaleHellsguardNpc, (SubRace.Hellsguard, Gender.Male, true) => CollectionType.MaleHellsguardNpc,
(SubRace.Raen, Gender.Male, true) => CollectionType.MaleRaenNpc, (SubRace.Raen, Gender.Male, true) => CollectionType.MaleRaenNpc,
(SubRace.Xaela, Gender.Male, true) => CollectionType.MaleXaelaNpc, (SubRace.Xaela, Gender.Male, true) => CollectionType.MaleXaelaNpc,
(SubRace.Helion, Gender.Male, true) => CollectionType.MaleHelionNpc, (SubRace.Helion, Gender.Male, true) => CollectionType.MaleHelionNpc,
(SubRace.Lost, Gender.Male, true) => CollectionType.MaleLostNpc, (SubRace.Lost, Gender.Male, true) => CollectionType.MaleLostNpc,
(SubRace.Rava, Gender.Male, true) => CollectionType.MaleRavaNpc, (SubRace.Rava, Gender.Male, true) => CollectionType.MaleRavaNpc,
(SubRace.Veena, Gender.Male, true) => CollectionType.MaleVeenaNpc, (SubRace.Veena, Gender.Male, true) => CollectionType.MaleVeenaNpc,
(SubRace.Midlander, Gender.Female, true) => CollectionType.FemaleMidlanderNpc, (SubRace.Midlander, Gender.Female, true) => CollectionType.FemaleMidlanderNpc,
(SubRace.Highlander, Gender.Female, true) => CollectionType.FemaleHighlanderNpc, (SubRace.Highlander, Gender.Female, true) => CollectionType.FemaleHighlanderNpc,
(SubRace.Wildwood, Gender.Female, true) => CollectionType.FemaleWildwoodNpc, (SubRace.Wildwood, Gender.Female, true) => CollectionType.FemaleWildwoodNpc,
(SubRace.Duskwight, Gender.Female, true) => CollectionType.FemaleDuskwightNpc, (SubRace.Duskwight, Gender.Female, true) => CollectionType.FemaleDuskwightNpc,
(SubRace.Plainsfolk, Gender.Female, true) => CollectionType.FemalePlainsfolkNpc, (SubRace.Plainsfolk, Gender.Female, true) => CollectionType.FemalePlainsfolkNpc,
(SubRace.Dunesfolk, Gender.Female, true) => CollectionType.FemaleDunesfolkNpc, (SubRace.Dunesfolk, Gender.Female, true) => CollectionType.FemaleDunesfolkNpc,
(SubRace.SeekerOfTheSun, Gender.Female, true) => CollectionType.FemaleSeekerOfTheSunNpc, (SubRace.SeekerOfTheSun, Gender.Female, true) => CollectionType.FemaleSeekerOfTheSunNpc,
(SubRace.KeeperOfTheMoon, Gender.Female, true) => CollectionType.FemaleKeeperOfTheMoonNpc, (SubRace.KeeperOfTheMoon, Gender.Female, true) => CollectionType.FemaleKeeperOfTheMoonNpc,
(SubRace.Seawolf, Gender.Female, true) => CollectionType.FemaleSeawolfNpc, (SubRace.Seawolf, Gender.Female, true) => CollectionType.FemaleSeawolfNpc,
(SubRace.Hellsguard, Gender.Female, true) => CollectionType.FemaleHellsguardNpc, (SubRace.Hellsguard, Gender.Female, true) => CollectionType.FemaleHellsguardNpc,
(SubRace.Raen, Gender.Female, true) => CollectionType.FemaleRaenNpc, (SubRace.Raen, Gender.Female, true) => CollectionType.FemaleRaenNpc,
(SubRace.Xaela, Gender.Female, true) => CollectionType.FemaleXaelaNpc, (SubRace.Xaela, Gender.Female, true) => CollectionType.FemaleXaelaNpc,
(SubRace.Helion, Gender.Female, true) => CollectionType.FemaleHelionNpc, (SubRace.Helion, Gender.Female, true) => CollectionType.FemaleHelionNpc,
(SubRace.Lost, Gender.Female, true) => CollectionType.FemaleLostNpc, (SubRace.Lost, Gender.Female, true) => CollectionType.FemaleLostNpc,
(SubRace.Rava, Gender.Female, true) => CollectionType.FemaleRavaNpc, (SubRace.Rava, Gender.Female, true) => CollectionType.FemaleRavaNpc,
(SubRace.Veena, Gender.Female, true) => CollectionType.FemaleVeenaNpc, (SubRace.Veena, Gender.Female, true) => CollectionType.FemaleVeenaNpc,
_ => CollectionType.Inactive, _ => CollectionType.Inactive,
}; };
} }
public static bool TryParse(string text, out CollectionType type) public static bool TryParse(string text, out CollectionType type)
{ {
if (Enum.TryParse(text, true, out type)) if (Enum.TryParse(text, true, out type))
{
return type is not CollectionType.Inactive and not CollectionType.Temporary; return type is not CollectionType.Inactive and not CollectionType.Temporary;
}
if (string.Equals(text, "character", StringComparison.OrdinalIgnoreCase)) if (string.Equals(text, "character", StringComparison.OrdinalIgnoreCase))
{ {
@ -335,9 +333,7 @@ public static class CollectionTypeExtensions
foreach (var t in Enum.GetValues<CollectionType>()) foreach (var t in Enum.GetValues<CollectionType>())
{ {
if (t is CollectionType.Inactive or CollectionType.Temporary) if (t is CollectionType.Inactive or CollectionType.Temporary)
{
continue; continue;
}
if (string.Equals(text, t.ToName(), StringComparison.OrdinalIgnoreCase)) if (string.Equals(text, t.ToName(), StringComparison.OrdinalIgnoreCase))
{ {
@ -352,83 +348,83 @@ public static class CollectionTypeExtensions
public static string ToName(this CollectionType collectionType) public static string ToName(this CollectionType collectionType)
=> collectionType switch => collectionType switch
{ {
CollectionType.Yourself => "Your Character", CollectionType.Yourself => "Your Character",
CollectionType.NonPlayerChild => "Non-Player Children", CollectionType.NonPlayerChild => "Non-Player Children",
CollectionType.NonPlayerElderly => "Non-Player Elderly", CollectionType.NonPlayerElderly => "Non-Player Elderly",
CollectionType.MalePlayerCharacter => "Male Player Characters", CollectionType.MalePlayerCharacter => "Male Player Characters",
CollectionType.MaleNonPlayerCharacter => "Male Non-Player Characters", CollectionType.MaleNonPlayerCharacter => "Male Non-Player Characters",
CollectionType.MaleMidlander => $"Male {SubRace.Midlander.ToName()}", CollectionType.MaleMidlander => $"Male {SubRace.Midlander.ToName()}",
CollectionType.MaleHighlander => $"Male {SubRace.Highlander.ToName()}", CollectionType.MaleHighlander => $"Male {SubRace.Highlander.ToName()}",
CollectionType.MaleWildwood => $"Male {SubRace.Wildwood.ToName()}", CollectionType.MaleWildwood => $"Male {SubRace.Wildwood.ToName()}",
CollectionType.MaleDuskwight => $"Male {SubRace.Duskwight.ToName()}", CollectionType.MaleDuskwight => $"Male {SubRace.Duskwight.ToName()}",
CollectionType.MalePlainsfolk => $"Male {SubRace.Plainsfolk.ToName()}", CollectionType.MalePlainsfolk => $"Male {SubRace.Plainsfolk.ToName()}",
CollectionType.MaleDunesfolk => $"Male {SubRace.Dunesfolk.ToName()}", CollectionType.MaleDunesfolk => $"Male {SubRace.Dunesfolk.ToName()}",
CollectionType.MaleSeekerOfTheSun => $"Male {SubRace.SeekerOfTheSun.ToName()}", CollectionType.MaleSeekerOfTheSun => $"Male {SubRace.SeekerOfTheSun.ToName()}",
CollectionType.MaleKeeperOfTheMoon => $"Male {SubRace.KeeperOfTheMoon.ToName()}", CollectionType.MaleKeeperOfTheMoon => $"Male {SubRace.KeeperOfTheMoon.ToName()}",
CollectionType.MaleSeawolf => $"Male {SubRace.Seawolf.ToName()}", CollectionType.MaleSeawolf => $"Male {SubRace.Seawolf.ToName()}",
CollectionType.MaleHellsguard => $"Male {SubRace.Hellsguard.ToName()}", CollectionType.MaleHellsguard => $"Male {SubRace.Hellsguard.ToName()}",
CollectionType.MaleRaen => $"Male {SubRace.Raen.ToName()}", CollectionType.MaleRaen => $"Male {SubRace.Raen.ToName()}",
CollectionType.MaleXaela => $"Male {SubRace.Xaela.ToName()}", CollectionType.MaleXaela => $"Male {SubRace.Xaela.ToName()}",
CollectionType.MaleHelion => $"Male {SubRace.Helion.ToName()}", CollectionType.MaleHelion => $"Male {SubRace.Helion.ToName()}",
CollectionType.MaleLost => $"Male {SubRace.Lost.ToName()}", CollectionType.MaleLost => $"Male {SubRace.Lost.ToName()}",
CollectionType.MaleRava => $"Male {SubRace.Rava.ToName()}", CollectionType.MaleRava => $"Male {SubRace.Rava.ToName()}",
CollectionType.MaleVeena => $"Male {SubRace.Veena.ToName()}", CollectionType.MaleVeena => $"Male {SubRace.Veena.ToName()}",
CollectionType.MaleMidlanderNpc => $"Male {SubRace.Midlander.ToName()} (NPC)", CollectionType.MaleMidlanderNpc => $"Male {SubRace.Midlander.ToName()} (NPC)",
CollectionType.MaleHighlanderNpc => $"Male {SubRace.Highlander.ToName()} (NPC)", CollectionType.MaleHighlanderNpc => $"Male {SubRace.Highlander.ToName()} (NPC)",
CollectionType.MaleWildwoodNpc => $"Male {SubRace.Wildwood.ToName()} (NPC)", CollectionType.MaleWildwoodNpc => $"Male {SubRace.Wildwood.ToName()} (NPC)",
CollectionType.MaleDuskwightNpc => $"Male {SubRace.Duskwight.ToName()} (NPC)", CollectionType.MaleDuskwightNpc => $"Male {SubRace.Duskwight.ToName()} (NPC)",
CollectionType.MalePlainsfolkNpc => $"Male {SubRace.Plainsfolk.ToName()} (NPC)", CollectionType.MalePlainsfolkNpc => $"Male {SubRace.Plainsfolk.ToName()} (NPC)",
CollectionType.MaleDunesfolkNpc => $"Male {SubRace.Dunesfolk.ToName()} (NPC)", CollectionType.MaleDunesfolkNpc => $"Male {SubRace.Dunesfolk.ToName()} (NPC)",
CollectionType.MaleSeekerOfTheSunNpc => $"Male {SubRace.SeekerOfTheSun.ToName()} (NPC)", CollectionType.MaleSeekerOfTheSunNpc => $"Male {SubRace.SeekerOfTheSun.ToName()} (NPC)",
CollectionType.MaleKeeperOfTheMoonNpc => $"Male {SubRace.KeeperOfTheMoon.ToName()} (NPC)", CollectionType.MaleKeeperOfTheMoonNpc => $"Male {SubRace.KeeperOfTheMoon.ToName()} (NPC)",
CollectionType.MaleSeawolfNpc => $"Male {SubRace.Seawolf.ToName()} (NPC)", CollectionType.MaleSeawolfNpc => $"Male {SubRace.Seawolf.ToName()} (NPC)",
CollectionType.MaleHellsguardNpc => $"Male {SubRace.Hellsguard.ToName()} (NPC)", CollectionType.MaleHellsguardNpc => $"Male {SubRace.Hellsguard.ToName()} (NPC)",
CollectionType.MaleRaenNpc => $"Male {SubRace.Raen.ToName()} (NPC)", CollectionType.MaleRaenNpc => $"Male {SubRace.Raen.ToName()} (NPC)",
CollectionType.MaleXaelaNpc => $"Male {SubRace.Xaela.ToName()} (NPC)", CollectionType.MaleXaelaNpc => $"Male {SubRace.Xaela.ToName()} (NPC)",
CollectionType.MaleHelionNpc => $"Male {SubRace.Helion.ToName()} (NPC)", CollectionType.MaleHelionNpc => $"Male {SubRace.Helion.ToName()} (NPC)",
CollectionType.MaleLostNpc => $"Male {SubRace.Lost.ToName()} (NPC)", CollectionType.MaleLostNpc => $"Male {SubRace.Lost.ToName()} (NPC)",
CollectionType.MaleRavaNpc => $"Male {SubRace.Rava.ToName()} (NPC)", CollectionType.MaleRavaNpc => $"Male {SubRace.Rava.ToName()} (NPC)",
CollectionType.MaleVeenaNpc => $"Male {SubRace.Veena.ToName()} (NPC)", CollectionType.MaleVeenaNpc => $"Male {SubRace.Veena.ToName()} (NPC)",
CollectionType.FemalePlayerCharacter => "Female Player Characters", CollectionType.FemalePlayerCharacter => "Female Player Characters",
CollectionType.FemaleNonPlayerCharacter => "Female Non-Player Characters", CollectionType.FemaleNonPlayerCharacter => "Female Non-Player Characters",
CollectionType.FemaleMidlander => $"Female {SubRace.Midlander.ToName()}", CollectionType.FemaleMidlander => $"Female {SubRace.Midlander.ToName()}",
CollectionType.FemaleHighlander => $"Female {SubRace.Highlander.ToName()}", CollectionType.FemaleHighlander => $"Female {SubRace.Highlander.ToName()}",
CollectionType.FemaleWildwood => $"Female {SubRace.Wildwood.ToName()}", CollectionType.FemaleWildwood => $"Female {SubRace.Wildwood.ToName()}",
CollectionType.FemaleDuskwight => $"Female {SubRace.Duskwight.ToName()}", CollectionType.FemaleDuskwight => $"Female {SubRace.Duskwight.ToName()}",
CollectionType.FemalePlainsfolk => $"Female {SubRace.Plainsfolk.ToName()}", CollectionType.FemalePlainsfolk => $"Female {SubRace.Plainsfolk.ToName()}",
CollectionType.FemaleDunesfolk => $"Female {SubRace.Dunesfolk.ToName()}", CollectionType.FemaleDunesfolk => $"Female {SubRace.Dunesfolk.ToName()}",
CollectionType.FemaleSeekerOfTheSun => $"Female {SubRace.SeekerOfTheSun.ToName()}", CollectionType.FemaleSeekerOfTheSun => $"Female {SubRace.SeekerOfTheSun.ToName()}",
CollectionType.FemaleKeeperOfTheMoon => $"Female {SubRace.KeeperOfTheMoon.ToName()}", CollectionType.FemaleKeeperOfTheMoon => $"Female {SubRace.KeeperOfTheMoon.ToName()}",
CollectionType.FemaleSeawolf => $"Female {SubRace.Seawolf.ToName()}", CollectionType.FemaleSeawolf => $"Female {SubRace.Seawolf.ToName()}",
CollectionType.FemaleHellsguard => $"Female {SubRace.Hellsguard.ToName()}", CollectionType.FemaleHellsguard => $"Female {SubRace.Hellsguard.ToName()}",
CollectionType.FemaleRaen => $"Female {SubRace.Raen.ToName()}", CollectionType.FemaleRaen => $"Female {SubRace.Raen.ToName()}",
CollectionType.FemaleXaela => $"Female {SubRace.Xaela.ToName()}", CollectionType.FemaleXaela => $"Female {SubRace.Xaela.ToName()}",
CollectionType.FemaleHelion => $"Female {SubRace.Helion.ToName()}", CollectionType.FemaleHelion => $"Female {SubRace.Helion.ToName()}",
CollectionType.FemaleLost => $"Female {SubRace.Lost.ToName()}", CollectionType.FemaleLost => $"Female {SubRace.Lost.ToName()}",
CollectionType.FemaleRava => $"Female {SubRace.Rava.ToName()}", CollectionType.FemaleRava => $"Female {SubRace.Rava.ToName()}",
CollectionType.FemaleVeena => $"Female {SubRace.Veena.ToName()}", CollectionType.FemaleVeena => $"Female {SubRace.Veena.ToName()}",
CollectionType.FemaleMidlanderNpc => $"Female {SubRace.Midlander.ToName()} (NPC)", CollectionType.FemaleMidlanderNpc => $"Female {SubRace.Midlander.ToName()} (NPC)",
CollectionType.FemaleHighlanderNpc => $"Female {SubRace.Highlander.ToName()} (NPC)", CollectionType.FemaleHighlanderNpc => $"Female {SubRace.Highlander.ToName()} (NPC)",
CollectionType.FemaleWildwoodNpc => $"Female {SubRace.Wildwood.ToName()} (NPC)", CollectionType.FemaleWildwoodNpc => $"Female {SubRace.Wildwood.ToName()} (NPC)",
CollectionType.FemaleDuskwightNpc => $"Female {SubRace.Duskwight.ToName()} (NPC)", CollectionType.FemaleDuskwightNpc => $"Female {SubRace.Duskwight.ToName()} (NPC)",
CollectionType.FemalePlainsfolkNpc => $"Female {SubRace.Plainsfolk.ToName()} (NPC)", CollectionType.FemalePlainsfolkNpc => $"Female {SubRace.Plainsfolk.ToName()} (NPC)",
CollectionType.FemaleDunesfolkNpc => $"Female {SubRace.Dunesfolk.ToName()} (NPC)", CollectionType.FemaleDunesfolkNpc => $"Female {SubRace.Dunesfolk.ToName()} (NPC)",
CollectionType.FemaleSeekerOfTheSunNpc => $"Female {SubRace.SeekerOfTheSun.ToName()} (NPC)", CollectionType.FemaleSeekerOfTheSunNpc => $"Female {SubRace.SeekerOfTheSun.ToName()} (NPC)",
CollectionType.FemaleKeeperOfTheMoonNpc => $"Female {SubRace.KeeperOfTheMoon.ToName()} (NPC)", CollectionType.FemaleKeeperOfTheMoonNpc => $"Female {SubRace.KeeperOfTheMoon.ToName()} (NPC)",
CollectionType.FemaleSeawolfNpc => $"Female {SubRace.Seawolf.ToName()} (NPC)", CollectionType.FemaleSeawolfNpc => $"Female {SubRace.Seawolf.ToName()} (NPC)",
CollectionType.FemaleHellsguardNpc => $"Female {SubRace.Hellsguard.ToName()} (NPC)", CollectionType.FemaleHellsguardNpc => $"Female {SubRace.Hellsguard.ToName()} (NPC)",
CollectionType.FemaleRaenNpc => $"Female {SubRace.Raen.ToName()} (NPC)", CollectionType.FemaleRaenNpc => $"Female {SubRace.Raen.ToName()} (NPC)",
CollectionType.FemaleXaelaNpc => $"Female {SubRace.Xaela.ToName()} (NPC)", CollectionType.FemaleXaelaNpc => $"Female {SubRace.Xaela.ToName()} (NPC)",
CollectionType.FemaleHelionNpc => $"Female {SubRace.Helion.ToName()} (NPC)", CollectionType.FemaleHelionNpc => $"Female {SubRace.Helion.ToName()} (NPC)",
CollectionType.FemaleLostNpc => $"Female {SubRace.Lost.ToName()} (NPC)", CollectionType.FemaleLostNpc => $"Female {SubRace.Lost.ToName()} (NPC)",
CollectionType.FemaleRavaNpc => $"Female {SubRace.Rava.ToName()} (NPC)", CollectionType.FemaleRavaNpc => $"Female {SubRace.Rava.ToName()} (NPC)",
CollectionType.FemaleVeenaNpc => $"Female {SubRace.Veena.ToName()} (NPC)", CollectionType.FemaleVeenaNpc => $"Female {SubRace.Veena.ToName()} (NPC)",
CollectionType.Inactive => "Collection", CollectionType.Inactive => "Collection",
CollectionType.Default => "Default", CollectionType.Default => "Base",
CollectionType.Interface => "Interface", CollectionType.Interface => "Interface",
CollectionType.Individual => "Individual", CollectionType.Individual => "Individual",
CollectionType.Current => "Current", CollectionType.Current => "Current",
_ => string.Empty, _ => string.Empty,
}; };
public static string ToDescription(this CollectionType collectionType) public static string ToDescription(this CollectionType collectionType)
@ -580,4 +576,4 @@ public static class CollectionTypeExtensions
"This collection applies to all female non-player character Veena Viera that do not have a more specific character collection associated.", "This collection applies to all female non-player character Veena Viera that do not have a more specific character collection associated.",
_ => string.Empty, _ => string.Empty,
}; };
} }

View file

@ -28,7 +28,7 @@ public partial class ModCollection
/// Create the always available Empty Collection that will always sit at index 0, /// Create the always available Empty Collection that will always sit at index 0,
/// can not be deleted and does never create a cache. /// can not be deleted and does never create a cache.
/// </summary> /// </summary>
public static readonly ModCollection Empty = CreateEmpty(EmptyCollectionName, 0); public static readonly ModCollection Empty = CreateEmpty(EmptyCollectionName, 0, 0);
/// <summary> The name of a collection can not contain characters invalid in a path. </summary> /// <summary> The name of a collection can not contain characters invalid in a path. </summary>
public string Name { get; internal init; } public string Name { get; internal init; }
@ -133,10 +133,10 @@ public partial class ModCollection
} }
/// <summary> Constructor for empty collections. </summary> /// <summary> Constructor for empty collections. </summary>
public static ModCollection CreateEmpty(string name, int index) public static ModCollection CreateEmpty(string name, int index, int modCount)
{ {
Debug.Assert(index >= 0, "Empty collection created with negative index."); Debug.Assert(index >= 0, "Empty collection created with negative index.");
return new ModCollection(name, index, 0, CurrentVersion, new List<ModSettings?>(), new List<ModCollection>(), return new ModCollection(name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?) null, modCount).ToList(), new List<ModCollection>(),
new Dictionary<string, ModSettings.SavedSettings>()); new Dictionary<string, ModSettings.SavedSettings>());
} }

View file

@ -1,12 +1,12 @@
using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using Penumbra.GameData; using Penumbra.GameData;
namespace Penumbra.Interop.Services; namespace Penumbra.Interop.Services;
/// <summary> /// <summary>
/// Handle font reloading via game functions. /// Handle font reloading via game functions.
/// May cause a interface flicker while reloading. /// May cause a interface flicker while reloading.
/// </summary> /// </summary>
public unsafe class FontReloader public unsafe class FontReloader
{ {
@ -21,24 +21,27 @@ public unsafe class FontReloader
Penumbra.Log.Error("Could not reload fonts, function could not be found."); Penumbra.Log.Error("Could not reload fonts, function could not be found.");
} }
private readonly AtkModule* _atkModule = null!; private AtkModule* _atkModule = null!;
private readonly delegate* unmanaged<AtkModule*, bool, bool, void> _reloadFontsFunc = null!; private delegate* unmanaged<AtkModule*, bool, bool, void> _reloadFontsFunc = null!;
public FontReloader() public FontReloader(Dalamud.Game.Framework dFramework)
{ {
var framework = Framework.Instance(); dFramework.RunOnFrameworkThread(() =>
if (framework == null) {
return; var framework = Framework.Instance();
if (framework == null)
return;
var uiModule = framework->GetUiModule(); var uiModule = framework->GetUiModule();
if (uiModule == null) if (uiModule == null)
return; return;
var atkModule = uiModule->GetRaptureAtkModule(); var atkModule = uiModule->GetRaptureAtkModule();
if (atkModule == null) if (atkModule == null)
return; return;
_atkModule = &atkModule->AtkModule; _atkModule = &atkModule->AtkModule;
_reloadFontsFunc = ((delegate* unmanaged<AtkModule*, bool, bool, void>*)_atkModule->vtbl)[Offsets.ReloadFontsVfunc]; _reloadFontsFunc = ((delegate* unmanaged<AtkModule*, bool, bool, void>*)_atkModule->vtbl)[Offsets.ReloadFontsVfunc];
});
} }
} }

View file

@ -178,7 +178,7 @@ public unsafe class ImcFile : MetaBaseFile
public void Replace(ResourceHandle* resource) public void Replace(ResourceHandle* resource)
{ {
var (data, length) = resource->GetData(); var (data, length) = resource->GetData();
var newData = Penumbra.MetaFileManager.AllocateDefaultMemory(ActualLength, 8); var newData = Manager.AllocateDefaultMemory(ActualLength, 8);
if (newData == null) if (newData == null)
{ {
Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong)resource:X}, allocation failed."); Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong)resource:X}, allocation failed.");
@ -187,7 +187,7 @@ public unsafe class ImcFile : MetaBaseFile
MemoryUtility.MemCpyUnchecked(newData, Data, ActualLength); MemoryUtility.MemCpyUnchecked(newData, Data, ActualLength);
Penumbra.MetaFileManager.Free(data, length); Manager.Free(data, length);
resource->SetData((IntPtr)newData, ActualLength); resource->SetData((IntPtr)newData, ActualLength);
} }
} }

View file

@ -20,12 +20,12 @@ public unsafe class MetaFileManager
internal readonly CharacterUtility CharacterUtility; internal readonly CharacterUtility CharacterUtility;
internal readonly ResidentResourceManager ResidentResources; internal readonly ResidentResourceManager ResidentResources;
internal readonly DataManager GameData; internal readonly DataManager GameData;
internal readonly ActiveCollections ActiveCollections; internal readonly ActiveCollectionData ActiveCollections;
internal readonly ValidityChecker ValidityChecker; internal readonly ValidityChecker ValidityChecker;
internal readonly IdentifierService Identifier; internal readonly IdentifierService Identifier;
public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, DataManager gameData, public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, DataManager gameData,
ActiveCollections activeCollections, Configuration config, ValidityChecker validityChecker, IdentifierService identifier) ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, IdentifierService identifier)
{ {
CharacterUtility = characterUtility; CharacterUtility = characterUtility;
ResidentResources = residentResources; ResidentResources = residentResources;

View file

@ -8,16 +8,16 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Penumbra.GameData;
using Penumbra.Meta; using Penumbra.Meta;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Services;
namespace Penumbra.Mods.ItemSwap; namespace Penumbra.Mods.ItemSwap;
public class ItemSwapContainer public class ItemSwapContainer
{ {
private readonly MetaFileManager _manager; private readonly MetaFileManager _manager;
private readonly IObjectIdentifier _identifier; private readonly IdentifierService _identifier;
private Dictionary< Utf8GamePath, FullPath > _modRedirections = new(); private Dictionary< Utf8GamePath, FullPath > _modRedirections = new();
private HashSet< MetaManipulation > _modManipulations = new(); private HashSet< MetaManipulation > _modManipulations = new();
@ -114,7 +114,7 @@ public class ItemSwapContainer
} }
} }
public ItemSwapContainer(MetaFileManager manager, IObjectIdentifier identifier) public ItemSwapContainer(MetaFileManager manager, IdentifierService identifier)
{ {
_manager = manager; _manager = manager;
_identifier = identifier; _identifier = identifier;
@ -136,7 +136,7 @@ public class ItemSwapContainer
{ {
Swaps.Clear(); Swaps.Clear();
Loaded = false; Loaded = false;
var ret = EquipmentSwap.CreateItemSwap( _manager, _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), from, to, useRightRing, useLeftRing ); var ret = EquipmentSwap.CreateItemSwap( _manager, _identifier.AwaitedService, Swaps, PathResolver( collection ), MetaResolver( collection ), from, to, useRightRing, useLeftRing );
Loaded = true; Loaded = true;
return ret; return ret;
} }
@ -145,7 +145,7 @@ public class ItemSwapContainer
{ {
Swaps.Clear(); Swaps.Clear();
Loaded = false; Loaded = false;
var ret = EquipmentSwap.CreateTypeSwap( _manager, _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), slotFrom, from, slotTo, to ); var ret = EquipmentSwap.CreateTypeSwap( _manager, _identifier.AwaitedService, Swaps, PathResolver( collection ), MetaResolver( collection ), slotFrom, from, slotTo, to );
Loaded = true; Loaded = true;
return ret; return ret;
} }

View file

@ -38,12 +38,6 @@ public class ModDataEditor
_communicatorService = communicatorService; _communicatorService = communicatorService;
} }
public string MetaFile(Mod mod)
=> _saveService.FileNames.ModMetaPath(mod);
public string DataFile(Mod mod)
=> _saveService.FileNames.LocalDataFile(mod);
/// <summary> Create the file containing the meta information about a mod from scratch. </summary> /// <summary> Create the file containing the meta information about a mod from scratch. </summary>
public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version, public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version,
string? website) string? website)

View file

@ -220,7 +220,7 @@ public partial class ModCreator
if (!file.Exists) if (!file.Exists)
continue; continue;
var rgsp = TexToolsMeta.FromRgspFile(Penumbra.MetaFileManager, file.FullName, File.ReadAllBytes(file.FullName), var rgsp = TexToolsMeta.FromRgspFile(_metaFileManager, file.FullName, File.ReadAllBytes(file.FullName),
_config.KeepDefaultMetaChanges); _config.KeepDefaultMetaChanges);
Penumbra.Log.Verbose( Penumbra.Log.Verbose(
$"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}");

View file

@ -41,7 +41,6 @@ public class Penumbra : IDalamudPlugin
public static Configuration Config { get; private set; } = null!; public static Configuration Config { get; private set; } = null!;
public static CharacterUtility CharacterUtility { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!;
public static MetaFileManager MetaFileManager { get; private set; } = null!;
public static ModManager ModManager { get; private set; } = null!; public static ModManager ModManager { get; private set; } = null!;
public static ModCacheManager ModCaches { get; private set; } = null!; public static ModCacheManager ModCaches { get; private set; } = null!;
public static CollectionManager CollectionManager { get; private set; } = null!; public static CollectionManager CollectionManager { get; private set; } = null!;
@ -72,7 +71,6 @@ public class Penumbra : IDalamudPlugin
_tmp.Services.GetRequiredService<BackupService>(); _tmp.Services.GetRequiredService<BackupService>();
Config = _tmp.Services.GetRequiredService<Configuration>(); Config = _tmp.Services.GetRequiredService<Configuration>();
CharacterUtility = _tmp.Services.GetRequiredService<CharacterUtility>(); CharacterUtility = _tmp.Services.GetRequiredService<CharacterUtility>();
MetaFileManager = _tmp.Services.GetRequiredService<MetaFileManager>();
Actors = _tmp.Services.GetRequiredService<ActorService>().AwaitedService; Actors = _tmp.Services.GetRequiredService<ActorService>().AwaitedService;
_tempMods = _tmp.Services.GetRequiredService<TempModManager>(); _tempMods = _tmp.Services.GetRequiredService<TempModManager>();
_residentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>(); _residentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>();
@ -114,8 +112,6 @@ public class Penumbra : IDalamudPlugin
var api = _tmp.Services.GetRequiredService<IPenumbraApi>(); var api = _tmp.Services.GetRequiredService<IPenumbraApi>();
HttpApi = _tmp.Services.GetRequiredService<HttpApi>(); HttpApi = _tmp.Services.GetRequiredService<HttpApi>();
_tmp.Services.GetRequiredService<PenumbraIpcProviders>(); _tmp.Services.GetRequiredService<PenumbraIpcProviders>();
if (Config.EnableHttpApi)
HttpApi.CreateWebServer();
api.ChangedItemTooltip += it => api.ChangedItemTooltip += it =>
{ {
if (it is Item) if (it is Item)

View file

@ -84,6 +84,7 @@ public class PenumbraNew
// Add Collection Services // Add Collection Services
services.AddSingleton<CollectionStorage>() services.AddSingleton<CollectionStorage>()
.AddSingleton<ActiveCollectionData>()
.AddSingleton<ActiveCollections>() .AddSingleton<ActiveCollections>()
.AddSingleton<InheritanceManager>() .AddSingleton<InheritanceManager>()
.AddSingleton<CollectionCacheManager>() .AddSingleton<CollectionCacheManager>()
@ -139,6 +140,7 @@ public class PenumbraNew
.AddSingleton<ModPanelEditTab>() .AddSingleton<ModPanelEditTab>()
.AddSingleton<ModPanelChangedItemsTab>() .AddSingleton<ModPanelChangedItemsTab>()
.AddSingleton<ModPanelConflictsTab>() .AddSingleton<ModPanelConflictsTab>()
.AddSingleton<ModPanelCollectionsTab>()
.AddSingleton<ModPanelTabBar>() .AddSingleton<ModPanelTabBar>()
.AddSingleton<ModFileSystemSelector>() .AddSingleton<ModFileSystemSelector>()
.AddSingleton<CollectionsTab>() .AddSingleton<CollectionsTab>()

View file

@ -252,11 +252,11 @@ public class ConfigMigrationService
using var j = new JsonTextWriter(writer); using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented; j.Formatting = Formatting.Indented;
j.WriteStartObject(); j.WriteStartObject();
j.WritePropertyName(nameof(ActiveCollections.Default)); j.WritePropertyName(nameof(ActiveCollectionData.Default));
j.WriteValue(def); j.WriteValue(def);
j.WritePropertyName(nameof(ActiveCollections.Interface)); j.WritePropertyName(nameof(ActiveCollectionData.Interface));
j.WriteValue(ui); j.WriteValue(ui);
j.WritePropertyName(nameof(ActiveCollections.Current)); j.WritePropertyName(nameof(ActiveCollectionData.Current));
j.WriteValue(current); j.WriteValue(current);
foreach (var (type, collection) in special) foreach (var (type, collection) in special)
{ {

View file

@ -30,19 +30,17 @@ public class ItemSwapTab : IDisposable, ITab
private readonly ItemService _itemService; private readonly ItemService _itemService;
private readonly CollectionManager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly ModManager _modManager; private readonly ModManager _modManager;
private readonly Configuration _config;
private readonly MetaFileManager _metaFileManager; private readonly MetaFileManager _metaFileManager;
public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager, public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager,
ModManager modManager, Configuration config, IdentifierService identifier, MetaFileManager metaFileManager) ModManager modManager, IdentifierService identifier, MetaFileManager metaFileManager)
{ {
_communicator = communicator; _communicator = communicator;
_itemService = itemService; _itemService = itemService;
_collectionManager = collectionManager; _collectionManager = collectionManager;
_modManager = modManager; _modManager = modManager;
_config = config;
_metaFileManager = metaFileManager; _metaFileManager = metaFileManager;
_swapData = new ItemSwapContainer(metaFileManager, identifier.AwaitedService); _swapData = new ItemSwapContainer(metaFileManager, identifier);
_selectors = new Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo)> _selectors = new Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo)>
{ {

View file

@ -27,6 +27,8 @@ public static class Colors
public const uint MetaInfoText = 0xAAFFFFFF; public const uint MetaInfoText = 0xAAFFFFFF;
public const uint RedTableBgTint = 0x40000080; public const uint RedTableBgTint = 0x40000080;
public const uint DiscordColor = 0xFFDA8972; public const uint DiscordColor = 0xFFDA8972;
public const uint SelectedColor = 0x6069C056;
public const uint RedundantColor = 0x6050D0D0;
public const uint FilterActive = 0x807070FF; public const uint FilterActive = 0x807070FF;
public const uint TutorialMarker = 0xFF20FFFF; public const uint TutorialMarker = 0xFF20FFFF;
public const uint TutorialBorder = 0xD00000FF; public const uint TutorialBorder = 0xD00000FF;

View file

@ -8,11 +8,11 @@ using Penumbra.GameData.Actors;
namespace Penumbra.UI.CollectionTab; namespace Penumbra.UI.CollectionTab;
public sealed class CollectionSelector : FilterComboCache<ModCollection> public sealed class CollectionCombo : FilterComboCache<ModCollection>
{ {
private readonly CollectionManager _collectionManager; private readonly CollectionManager _collectionManager;
public CollectionSelector(CollectionManager manager, Func<IReadOnlyList<ModCollection>> items) public CollectionCombo(CollectionManager manager, Func<IReadOnlyList<ModCollection>> items)
: base(items) : base(items)
=> _collectionManager = manager; => _collectionManager = manager;

View file

@ -18,9 +18,9 @@ public class IndividualCollectionUi
{ {
private readonly ActorService _actorService; private readonly ActorService _actorService;
private readonly CollectionManager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly CollectionSelector _withEmpty; private readonly CollectionCombo _withEmpty;
public IndividualCollectionUi(ActorService actors, CollectionManager collectionManager, CollectionSelector withEmpty) public IndividualCollectionUi(ActorService actors, CollectionManager collectionManager, CollectionCombo withEmpty)
{ {
_actorService = actors; _actorService = actors;
_collectionManager = collectionManager; _collectionManager = collectionManager;

View file

@ -1,7 +1,6 @@
using ImGuiNET; using ImGuiNET;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
namespace Penumbra.UI.CollectionTab; namespace Penumbra.UI.CollectionTab;

View file

@ -1,299 +1,592 @@
using System; using System;
using System.Linq; using System.Collections.Generic;
using System.Numerics; using System.Linq;
using Dalamud.Interface; using System.Numerics;
using Dalamud.Interface.Components; using System.Text;
using ImGuiNET; using Dalamud.Game.ClientState.Objects;
using OtterGui; using Dalamud.Interface;
using OtterGui.Raii; using ImGuiNET;
using OtterGui.Widgets; using OtterGui;
using Penumbra.Collections; using OtterGui.Raii;
using Penumbra.Collections.Manager; using OtterGui.Widgets;
using Penumbra.Services; using Penumbra.Collections;
using Penumbra.UI.CollectionTab; using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
namespace Penumbra.UI.Tabs; using Penumbra.GameData.Enums;
using Penumbra.Mods.Manager;
public class CollectionsTab : IDisposable, ITab using Penumbra.Services;
{ using Penumbra.UI.Classes;
private readonly CommunicatorService _communicator; using Penumbra.UI.CollectionTab;
private readonly Configuration _config;
private readonly CollectionManager _collectionManager; namespace Penumbra.UI.Tabs;
private readonly TutorialService _tutorial;
private readonly SpecialCombo _specialCollectionCombo; public sealed class CollectionTree
{
private readonly CollectionSelector _collectionsWithEmpty; private readonly CollectionStorage _collections;
private readonly CollectionSelector _collectionSelector; private readonly ActiveCollections _active;
private readonly InheritanceUi _inheritance; private readonly CollectionSelector2 _selector;
private readonly IndividualCollectionUi _individualCollections; private readonly ActorService _actors;
private readonly TargetManager _targets;
public CollectionsTab(ActorService actorService, CommunicatorService communicator, CollectionManager collectionManager,
TutorialService tutorial, Configuration config) private static readonly IReadOnlyList<(string Name, uint Border)> Buttons = CreateButtons();
{ private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree();
_communicator = communicator;
_collectionManager = collectionManager; public CollectionTree(CollectionManager manager, CollectionSelector2 selector, ActorService actors,
_tutorial = tutorial; TargetManager targets)
_config = config; {
_specialCollectionCombo = new SpecialCombo(_collectionManager, "##NewSpecial", 350); _collections = manager.Storage;
_collectionsWithEmpty = new CollectionSelector(_collectionManager, _active = manager.Active;
() => _collectionManager.Storage.OrderBy(c => c.Name).Prepend(ModCollection.Empty).ToList()); _selector = selector;
_collectionSelector = new CollectionSelector(_collectionManager, () => _collectionManager.Storage.OrderBy(c => c.Name).ToList()); _actors = actors;
_inheritance = new InheritanceUi(_collectionManager); _targets = targets;
_individualCollections = new IndividualCollectionUi(actorService, _collectionManager, _collectionsWithEmpty); }
_communicator.CollectionChange.Subscribe(_individualCollections.UpdateIdentifiers); public void DrawSimple()
} {
var buttonWidth = new Vector2(200 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing());
public ReadOnlySpan<byte> Label using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero)
=> "Collections"u8; .Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale);
DrawSimpleCollectionButton(CollectionType.Default, buttonWidth);
/// <summary> Draw a collection selector of a certain width for a certain type. </summary> DrawSimpleCollectionButton(CollectionType.Interface, buttonWidth);
public void DrawCollectionSelector(string label, float width, CollectionType collectionType, bool withEmpty) DrawSimpleCollectionButton(CollectionType.Yourself, buttonWidth);
=> (withEmpty ? _collectionsWithEmpty : _collectionSelector).Draw(label, width, collectionType); DrawSimpleCollectionButton(CollectionType.MalePlayerCharacter, buttonWidth);
DrawSimpleCollectionButton(CollectionType.FemalePlayerCharacter, buttonWidth);
public void Dispose() DrawSimpleCollectionButton(CollectionType.MaleNonPlayerCharacter, buttonWidth);
=> _communicator.CollectionChange.Unsubscribe(_individualCollections.UpdateIdentifiers); DrawSimpleCollectionButton(CollectionType.FemaleNonPlayerCharacter, buttonWidth);
/// <summary> Draw a tutorial step regardless of tab selection. </summary> var specialWidth = buttonWidth with { X = 275 * ImGuiHelpers.GlobalScale };
public void DrawHeader() var player = _actors.AwaitedService.GetCurrentPlayer();
=> _tutorial.OpenTutorial(BasicTutorialSteps.Collections); DrawButton($"Current Character ({(player.IsValid ? player.ToString() : "Unavailable")})", CollectionType.Individual, specialWidth, 0,
player);
public void DrawContent() ImGui.SameLine();
{
using var child = ImRaii.Child("##collections", -Vector2.One); var target = _actors.AwaitedService.FromObject(_targets.Target, false, true, true);
if (child) DrawButton($"Current Target ({(target.IsValid ? target.ToString() : "Unavailable")})", CollectionType.Individual, specialWidth, 0, target);
{ if (_active.Individuals.Count > 0)
DrawActiveCollectionSelectors(); {
DrawMainSelectors(); ImGui.TextUnformatted("Currently Active Individual Assignments");
} for (var i = 0; i < _active.Individuals.Count; ++i)
} {
var (name, ids, coll) = _active.Individuals.Assignments[i];
#region New Collections DrawButton(name, CollectionType.Individual, buttonWidth, 0, ids[0], coll);
// Input text fields. ImGui.SameLine();
private string _newCollectionName = string.Empty; if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X
private bool _canAddCollection; && i < _active.Individuals.Count - 1)
ImGui.NewLine();
/// <summary> }
/// Create a new collection that is either empty or a duplicate of the current collection.
/// Resets the new collection name. ImGui.NewLine();
/// </summary> }
private void CreateNewCollection(bool duplicate)
{ var first = true;
if (_collectionManager.Storage.AddCollection(_newCollectionName, duplicate ? _collectionManager.Active.Current : null))
_newCollectionName = string.Empty; void Button(CollectionType type)
} {
var (name, border) = Buttons[(int)type];
/// <summary> Draw the Clean Unused Settings button if there are any. </summary> var collection = _active.ByType(type);
private void DrawCleanCollectionButton(Vector2 width) if (collection == null)
{ return;
if (_collectionManager.Active.Current.UnusedSettings.Count == 0)
return; if (first)
{
ImGui.SameLine(); ImGui.Separator();
if (ImGuiUtil.DrawDisabledButton( ImGui.TextUnformatted("Currently Active Advanced Assignments");
$"Clean {_collectionManager.Active.Current.UnusedSettings.Count} Unused Settings###CleanSettings", width first = false;
, "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk." }
, false)) DrawButton(name, type, buttonWidth, border, ActorIdentifier.Invalid, collection);
_collectionManager.Storage.CleanUnavailableSettings(_collectionManager.Active.Current); ImGui.SameLine();
} if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X)
ImGui.NewLine();
/// <summary> Draw the new collection input as well as its buttons. </summary> }
private void DrawNewCollectionInput(Vector2 width)
{ Button(CollectionType.NonPlayerChild);
// Input for new collection name. Also checks for validity when changed. Button(CollectionType.NonPlayerElderly);
ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); foreach (var race in Enum.GetValues<SubRace>().Skip(1))
if (ImGui.InputTextWithHint("##New Collection", "New Collection Name...", ref _newCollectionName, 64)) {
_canAddCollection = _collectionManager.Storage.CanAddCollection(_newCollectionName, out _); Button(CollectionTypeExtensions.FromParts(race, Gender.Male, false));
Button(CollectionTypeExtensions.FromParts(race, Gender.Female, false));
ImGui.SameLine(); Button(CollectionTypeExtensions.FromParts(race, Gender.Male, true));
ImGuiComponents.HelpMarker( Button(CollectionTypeExtensions.FromParts(race, Gender.Female, true));
"A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n" }
+ "You can use multiple collections to quickly switch between sets of enabled mods."); }
// Creation buttons. public void DrawAdvanced()
var tt = _canAddCollection {
? string.Empty using var table = ImRaii.Table("##advanced", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
: "Please enter a unique name only consisting of symbols valid in a path but no '|' before creating a collection."; if (!table)
if (ImGuiUtil.DrawDisabledButton("Create Empty Collection", width, tt, !_canAddCollection)) return;
CreateNewCollection(false);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, Vector2.Zero)
ImGui.SameLine(); .Push(ImGuiStyleVar.FrameBorderSize, 1 * ImGuiHelpers.GlobalScale);
if (ImGuiUtil.DrawDisabledButton($"Duplicate {TutorialService.SelectedCollection}", width, tt, !_canAddCollection))
CreateNewCollection(true); var buttonWidth = new Vector2(150 * ImGuiHelpers.GlobalScale, 2 * ImGui.GetTextLineHeightWithSpacing());
} var dummy = new Vector2(1, 0);
#endregion foreach (var (type, pre, post, name, border) in AdvancedTree)
{
#region Collection Selection ImGui.TableNextColumn();
if (type is CollectionType.Inactive)
/// <summary> Draw all collection assignment selections. </summary> continue;
private void DrawActiveCollectionSelectors()
{ if (pre)
UiHelpers.DefaultLineSpace(); ImGui.Dummy(dummy);
var open = ImGui.CollapsingHeader(TutorialService.ActiveCollections, ImGuiTreeNodeFlags.DefaultOpen); DrawAssignmentButton(type, buttonWidth, name, border);
_tutorial.OpenTutorial(BasicTutorialSteps.ActiveCollections); if (post)
if (!open) ImGui.Dummy(dummy);
return; }
}
UiHelpers.DefaultLineSpace();
private void DrawContext(bool open, ModCollection? collection, CollectionType type, ActorIdentifier identifier, char suffix = 'i')
DrawDefaultCollectionSelector(); {
_tutorial.OpenTutorial(BasicTutorialSteps.DefaultCollection); var label = $"{type}{identifier}{suffix}";
DrawInterfaceCollectionSelector(); if (open)
_tutorial.OpenTutorial(BasicTutorialSteps.InterfaceCollection); ImGui.OpenPopup(label);
UiHelpers.DefaultLineSpace();
using var context = ImRaii.Popup(label);
DrawSpecialAssignments(); if (context)
_tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections1); {
UiHelpers.DefaultLineSpace(); using (var color = ImRaii.PushColor(ImGuiCol.Text, Colors.DiscordColor))
{
_individualCollections.Draw(); if (ImGui.MenuItem("Use no mods."))
_tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections2); _active.SetCollection(ModCollection.Empty, type, _active.Individuals.GetGroup(identifier));
UiHelpers.DefaultLineSpace(); }
}
if (collection != null)
private void DrawCurrentCollectionSelector(Vector2 width) {
{ using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder);
using var group = ImRaii.Group(); if (ImGui.MenuItem("Remove this assignment."))
DrawCollectionSelector("##current", UiHelpers.InputTextWidth.X, CollectionType.Current, false); _active.SetCollection(null, type, _active.Individuals.GetGroup(identifier));
ImGui.SameLine(); }
ImGuiUtil.LabeledHelpMarker(TutorialService.SelectedCollection,
"This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything."); foreach (var coll in _collections)
{
// Deletion conditions. if (coll != collection && ImGui.MenuItem($"Use {coll.Name}."))
var deleteCondition = _collectionManager.Active.Current.Name != ModCollection.DefaultCollectionName; _active.SetCollection(coll, type, _active.Individuals.GetGroup(identifier));
var modifierHeld = Penumbra.Config.DeleteModModifier.IsActive(); }
var tt = deleteCondition }
? modifierHeld ? string.Empty : $"Hold {_config.DeleteModModifier} while clicking to delete the collection." }
: $"You can not delete the collection {ModCollection.DefaultCollectionName}.";
private bool DrawButton(string text, CollectionType type, Vector2 width, uint borderColor, ActorIdentifier id, ModCollection? collection = null)
if (ImGuiUtil.DrawDisabledButton($"Delete {TutorialService.SelectedCollection}", width, tt, !deleteCondition || !modifierHeld)) {
_collectionManager.Storage.RemoveCollection(_collectionManager.Active.Current); using var group = ImRaii.Group();
var invalid = type == CollectionType.Individual && !id.IsValid;
DrawCleanCollectionButton(width); var redundancy = _active.RedundancyCheck(type, id);
} collection ??= _active.ByType(type, id);
using var color = ImRaii.PushColor(ImGuiCol.Button,
/// <summary> Draw the selector for the default collection assignment. </summary> collection == null
private void DrawDefaultCollectionSelector() ? 0
{ : redundancy.Length > 0
using var group = ImRaii.Group(); ? Colors.RedundantColor
DrawCollectionSelector("##default", UiHelpers.InputTextWidth.X, CollectionType.Default, true); : collection == _active.Current
ImGui.SameLine(); ? Colors.SelectedColor
ImGuiUtil.LabeledHelpMarker(TutorialService.DefaultCollection, : collection == ModCollection.Empty
$"Mods in the {TutorialService.DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game," ? Colors.RedTableBgTint
+ "as well as any character for whom no more specific conditions from below apply."); : ImGui.GetColorU32(ImGuiCol.Button), !invalid)
} .Push(ImGuiCol.Border, borderColor == 0 ? ImGui.GetColorU32(ImGuiCol.TextDisabled) : borderColor);
using var disabled = ImRaii.Disabled(invalid);
/// <summary> Draw the selector for the interface collection assignment. </summary> var button = ImGui.Button(text, width) || ImGui.IsItemClicked(ImGuiMouseButton.Right);
private void DrawInterfaceCollectionSelector() var hovered = redundancy.Length > 0 && ImGui.IsItemHovered();
{ if (!invalid)
using var group = ImRaii.Group(); {
DrawCollectionSelector("##interface", UiHelpers.InputTextWidth.X, CollectionType.Interface, true); _selector.DragTarget(type, id);
ImGui.SameLine(); var name = collection == ModCollection.Empty ? "Use No Mods" : collection?.Name ?? "Unassigned";
ImGuiUtil.LabeledHelpMarker(TutorialService.InterfaceCollection, var size = ImGui.CalcTextSize(name);
$"Mods in the {TutorialService.InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves."); var textPos = ImGui.GetItemRectMax() - size - ImGui.GetStyle().FramePadding;
} ImGui.GetWindowDrawList().AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), name);
DrawContext(button, collection, type, id);
/// <summary> Description for character groups used in multiple help markers. </summary> }
private const string CharacterGroupDescription =
$"{TutorialService.CharacterGroups} apply to certain types of characters based on a condition.\n" if (hovered)
+ $"All of them take precedence before the {TutorialService.DefaultCollection},\n" ImGui.SetTooltip(redundancy);
+ $"but all {TutorialService.IndividualAssignments} take precedence before them.";
return button;
/// <summary> Draw the entire group assignment section. </summary> }
private void DrawSpecialAssignments()
{ private void DrawSimpleCollectionButton(CollectionType type, Vector2 width)
using var _ = ImRaii.Group(); {
ImGui.AlignTextToFramePadding(); DrawButton(type.ToName(), type, width, 0, ActorIdentifier.Invalid);
ImGui.TextUnformatted(TutorialService.CharacterGroups); ImGui.SameLine();
ImGuiComponents.HelpMarker(CharacterGroupDescription); var secondLine = string.Empty;
ImGui.Separator(); foreach (var parent in type.InheritanceOrder())
DrawSpecialCollections(); {
ImGui.Dummy(Vector2.Zero); var coll = _active.ByType(parent);
DrawNewSpecialCollection(); if (coll == null)
} continue;
/// <summary> Draw a new combo to select special collections as well as button to create it. </summary> secondLine = $"\nWill behave as {parent.ToName()} ({coll.Name}) while unassigned.";
private void DrawNewSpecialCollection() break;
{ }
ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X);
if (_specialCollectionCombo.CurrentIdx == -1 ImGui.TextUnformatted(type.ToDescription() + secondLine);
|| _collectionManager.Active.ByType(_specialCollectionCombo.CurrentType!.Value.Item1) != null) ImGui.Separator();
{ }
_specialCollectionCombo.ResetFilter();
_specialCollectionCombo.CurrentIdx = CollectionTypeExtensions.Special private void DrawAssignmentButton(CollectionType type, Vector2 width, string name, uint color)
.IndexOf(t => _collectionManager.Active.ByType(t.Item1) == null); => DrawButton(name, type, width, color, ActorIdentifier.Invalid, _active.ByType(type));
}
private static IReadOnlyList<(string Name, uint Border)> CreateButtons()
if (_specialCollectionCombo.CurrentType == null) {
return; var ret = Enum.GetValues<CollectionType>().Select(t => (t.ToName(), 0u)).ToArray();
_specialCollectionCombo.Draw(); foreach (var race in Enum.GetValues<SubRace>().Skip(1))
ImGui.SameLine(); {
var disabled = _specialCollectionCombo.CurrentType == null; var color = race switch
var tt = disabled {
? $"Please select a condition for a {TutorialService.GroupAssignment} before creating the collection.\n\n" SubRace.Midlander => 0xAA5C9FE4u,
+ CharacterGroupDescription SubRace.Highlander => 0xAA5C9FE4u,
: CharacterGroupDescription; SubRace.Wildwood => 0xAA5C9F49u,
if (!ImGuiUtil.DrawDisabledButton($"Assign {TutorialService.ConditionalGroup}", new Vector2(120 * UiHelpers.Scale, 0), tt, disabled)) SubRace.Duskwight => 0xAA5C9F49u,
return; SubRace.Plainsfolk => 0xAAEF8CB6u,
SubRace.Dunesfolk => 0xAAEF8CB6u,
_collectionManager.Active.CreateSpecialCollection(_specialCollectionCombo.CurrentType!.Value.Item1); SubRace.SeekerOfTheSun => 0xAA8CEFECu,
_specialCollectionCombo.CurrentIdx = -1; SubRace.KeeperOfTheMoon => 0xAA8CEFECu,
} SubRace.Seawolf => 0xAAEFE68Cu,
SubRace.Hellsguard => 0xAAEFE68Cu,
#endregion SubRace.Raen => 0xAAB5EF8Cu,
SubRace.Xaela => 0xAAB5EF8Cu,
#region Current Collection Editing SubRace.Helion => 0xAAFFFFFFu,
SubRace.Lost => 0xAAFFFFFFu,
/// <summary> Draw the current collection selection, the creation of new collections and the inheritance block. </summary> SubRace.Rava => 0xAA607FA7u,
private void DrawMainSelectors() SubRace.Veena => 0xAA607FA7u,
{ _ => 0u,
UiHelpers.DefaultLineSpace(); };
var open = ImGui.CollapsingHeader("Collection Settings", ImGuiTreeNodeFlags.DefaultOpen);
_tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections); ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Male, false)] = ($"♂ {race.ToShortName()}", color);
if (!open) ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Female, false)] = ($"♀ {race.ToShortName()}", color);
return; ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Male, true)] = ($"♂ {race.ToShortName()} (NPC)", color);
ret[(int)CollectionTypeExtensions.FromParts(race, Gender.Female, true)] = ($"♀ {race.ToShortName()} (NPC)", color);
var width = new Vector2((UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); }
UiHelpers.DefaultLineSpace();
ret[(int)CollectionType.MalePlayerCharacter] = ("♂ Player", 0);
DrawCurrentCollectionSelector(width); ret[(int)CollectionType.FemalePlayerCharacter] = ("♀ Player", 0);
_tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection); ret[(int)CollectionType.MaleNonPlayerCharacter] = ("♂ NPC", 0);
UiHelpers.DefaultLineSpace(); ret[(int)CollectionType.FemaleNonPlayerCharacter] = ("♀ NPC", 0);
return ret;
DrawNewCollectionInput(width); }
UiHelpers.DefaultLineSpace();
private static IReadOnlyList<(CollectionType, bool, bool, string, uint)> CreateTree()
_inheritance.Draw(); {
_tutorial.OpenTutorial(BasicTutorialSteps.Inheritance); var ret = new List<(CollectionType, bool, bool, string, uint)>(Buttons.Count);
}
void Add(CollectionType type, bool pre, bool post)
/// <summary> Draw all currently set special collections. </summary> {
private void DrawSpecialCollections() var (name, border) = (int)type >= Buttons.Count ? (type.ToName(), 0) : Buttons[(int)type];
{ ret.Add((type, pre, post, name, border));
foreach (var (type, name, desc) in CollectionTypeExtensions.Special) }
{
var collection = _collectionManager.Active.ByType(type); Add(CollectionType.Default, false, false);
if (collection == null) Add(CollectionType.Interface, false, false);
continue; Add(CollectionType.Inactive, false, false);
Add(CollectionType.Inactive, false, false);
using var id = ImRaii.PushId((int)type); Add(CollectionType.Yourself, false, true);
DrawCollectionSelector("##SpecialCombo", UiHelpers.InputTextWidth.X, type, true); Add(CollectionType.Inactive, false, true);
ImGui.SameLine(); Add(CollectionType.NonPlayerChild, false, true);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty, Add(CollectionType.NonPlayerElderly, false, true);
false, true)) Add(CollectionType.MalePlayerCharacter, true, true);
{ Add(CollectionType.FemalePlayerCharacter, true, true);
_collectionManager.Active.RemoveSpecialCollection(type); Add(CollectionType.MaleNonPlayerCharacter, true, true);
_specialCollectionCombo.ResetFilter(); Add(CollectionType.FemaleNonPlayerCharacter, true, true);
} var pre = true;
foreach (var race in Enum.GetValues<SubRace>().Skip(1))
ImGui.SameLine(); {
ImGui.AlignTextToFramePadding(); Add(CollectionTypeExtensions.FromParts(race, Gender.Male, false), pre, !pre);
ImGuiUtil.LabeledHelpMarker(name, desc); Add(CollectionTypeExtensions.FromParts(race, Gender.Female, false), pre, !pre);
} Add(CollectionTypeExtensions.FromParts(race, Gender.Male, true), pre, !pre);
} Add(CollectionTypeExtensions.FromParts(race, Gender.Female, true), pre, !pre);
pre = !pre;
#endregion }
}
return ret;
}
}
public sealed class CollectionPanel
{
private readonly CollectionManager _manager;
private readonly ModStorage _modStorage;
private readonly InheritanceUi _inheritanceUi;
public CollectionPanel(CollectionManager manager, ModStorage modStorage)
{
_manager = manager;
_modStorage = modStorage;
_inheritanceUi = new InheritanceUi(_manager);
}
public void Draw()
{
var collection = _manager.Active.Current;
DrawName(collection);
DrawStatistics(collection);
_inheritanceUi.Draw();
DrawSettingsList(collection);
DrawInactiveSettingsList(collection);
}
private void DrawName(ModCollection collection)
{
ImGui.TextUnformatted($"{collection.Name} ({collection.AnonymizedName})");
}
private void DrawStatistics(ModCollection collection)
{
ImGui.TextUnformatted("Used for:");
var sb = new StringBuilder(128);
if (_manager.Active.Default == collection)
sb.Append(CollectionType.Default.ToName()).Append(", ");
if (_manager.Active.Interface == collection)
sb.Append(CollectionType.Interface.ToName()).Append(", ");
foreach (var (type, _) in _manager.Active.SpecialAssignments.Where(p => p.Value == collection))
sb.Append(type.ToName()).Append(", ");
foreach (var (name, _) in _manager.Active.Individuals.Where(p => p.Collection == collection))
sb.Append(name).Append(", ");
ImGui.SameLine();
ImGuiUtil.TextWrapped(sb.Length == 0 ? "Nothing" : sb.ToString(0, sb.Length - 2));
if (collection.DirectParentOf.Count > 0)
{
ImGui.TextUnformatted("Inherited by:");
ImGui.SameLine();
ImGuiUtil.TextWrapped(string.Join(", ", collection.DirectParentOf.Select(c => c.Name)));
}
}
private void DrawSettingsList(ModCollection collection)
{
using var box = ImRaii.ListBox("##activeSettings");
if (!box)
return;
foreach (var (mod, (settings, parent)) in _modStorage.Select(m => (m, collection[m.Index])).Where(t => t.Item2.Settings != null)
.OrderBy(t => t.m.Name))
ImGui.TextUnformatted($"{mod}{(parent != collection ? $" (inherited from {parent.Name})" : string.Empty)}");
}
private void DrawInactiveSettingsList(ModCollection collection)
{
if (collection.UnusedSettings.Count == 0)
return;
if (ImGui.Button("Clear Unused Settings"))
_manager.Storage.CleanUnavailableSettings(collection);
using var box = ImRaii.ListBox("##inactiveSettings");
if (!box)
return;
foreach (var name in collection.UnusedSettings.Keys)
ImGui.TextUnformatted(name);
}
}
public sealed class CollectionSelector2 : ItemSelector<ModCollection>, IDisposable
{
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
private readonly CollectionStorage _storage;
private readonly ActiveCollections _active;
private ModCollection? _dragging;
public CollectionSelector2(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active)
: base(new List<ModCollection>(), Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter)
{
_config = config;
_communicator = communicator;
_storage = storage;
_active = active;
_communicator.CollectionChange.Subscribe(OnCollectionChange);
// Set items.
OnCollectionChange(CollectionType.Inactive, null, null, string.Empty);
// Set selection.
OnCollectionChange(CollectionType.Current, null, _active.Current, string.Empty);
}
protected override bool OnDelete(int idx)
{
if (idx < 0 || idx >= Items.Count)
return false;
return _storage.RemoveCollection(Items[idx]);
}
protected override bool DeleteButtonEnabled()
=> _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive();
protected override string DeleteButtonTooltip()
=> _storage.DefaultNamed == Current
? $"The selected collection {Current.Name} can not be deleted."
: $"Delete the currently selected collection {Current?.Name}. Hold {_config.DeleteModModifier} to delete.";
protected override bool OnAdd(string name)
=> _storage.AddCollection(name, null);
protected override bool OnDuplicate(string name, int idx)
{
if (idx < 0 || idx >= Items.Count)
return false;
return _storage.AddCollection(name, Items[idx]);
}
protected override bool Filtered(int idx)
=> !Items[idx].Name.Contains(Filter, StringComparison.OrdinalIgnoreCase);
protected override bool OnDraw(int idx)
{
using var color = ImRaii.PushColor(ImGuiCol.Header, Colors.SelectedColor);
var ret = ImGui.Selectable(Items[idx].Name, idx == CurrentIdx);
using var source = ImRaii.DragDropSource();
if (source)
{
_dragging = Items[idx];
ImGui.SetDragDropPayload("Assignment", nint.Zero, 0);
ImGui.TextUnformatted($"Assigning {_dragging.Name} to...");
}
if (ret)
_active.SetCollection(Items[idx], CollectionType.Current);
return ret;
}
public void DragTarget(CollectionType type, ActorIdentifier identifier)
{
using var target = ImRaii.DragDropTarget();
if (!target.Success || _dragging == null || !ImGuiUtil.IsDropping("Assignment"))
return;
_active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier));
_dragging = null;
}
public void Dispose()
{
_communicator.CollectionChange.Unsubscribe(OnCollectionChange);
}
private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? @new, string _3)
{
switch (type)
{
case CollectionType.Temporary: return;
case CollectionType.Current:
if (@new != null)
SetCurrent(@new);
SetFilterDirty();
return;
case CollectionType.Inactive:
Items.Clear();
foreach (var c in _storage.OrderBy(c => c.Name))
Items.Add(c);
if (old == Current)
ClearCurrentSelection();
else
TryRestoreCurrent();
SetFilterDirty();
return;
default:
SetFilterDirty();
return;
}
}
}
public class CollectionsTab : IDisposable, ITab
{
private readonly CommunicatorService _communicator;
private readonly Configuration _configuration;
private readonly CollectionManager _collectionManager;
private readonly CollectionSelector2 _selector;
private readonly CollectionPanel _panel;
private readonly CollectionTree _tree;
public enum PanelMode
{
SimpleAssignment,
ComplexAssignment,
Details,
};
public PanelMode Mode = PanelMode.SimpleAssignment;
public CollectionsTab(CommunicatorService communicator, Configuration configuration, CollectionManager collectionManager,
ModStorage modStorage, ActorService actors, TargetManager targets)
{
_communicator = communicator;
_configuration = configuration;
_collectionManager = collectionManager;
_selector = new CollectionSelector2(_configuration, _communicator, _collectionManager.Storage, _collectionManager.Active);
_panel = new CollectionPanel(_collectionManager, modStorage);
_tree = new CollectionTree(collectionManager, _selector, actors, targets);
}
public void Dispose()
{
_selector.Dispose();
}
public ReadOnlySpan<byte> Label
=> "Collections"u8;
public void DrawContent()
{
var width = ImGui.CalcTextSize("nnnnnnnnnnnnnnnnnnnnnnnn").X;
_selector.Draw(width);
ImGui.SameLine();
using var group = ImRaii.Group();
DrawHeaderLine();
DrawPanel();
}
private void DrawHeaderLine()
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
var buttonSize = new Vector2(ImGui.GetContentRegionAvail().X / 3f, 0);
using var _ = ImRaii.Group();
using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.SimpleAssignment);
if (ImGui.Button("Simple Assignments", buttonSize))
Mode = PanelMode.SimpleAssignment;
ImGui.SameLine();
color.Pop();
color.Push(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.Details);
if (ImGui.Button("Collection Details", buttonSize))
Mode = PanelMode.Details;
ImGui.SameLine();
color.Pop();
color.Push(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.TabActive), Mode is PanelMode.ComplexAssignment);
if (ImGui.Button("Advanced Assignments", buttonSize))
Mode = PanelMode.ComplexAssignment;
}
private void DrawPanel()
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
using var child = ImRaii.Child("##CollectionSettings", new Vector2(-1, 0), true, ImGuiWindowFlags.HorizontalScrollbar);
if (!child)
return;
style.Pop();
switch (Mode)
{
case PanelMode.SimpleAssignment:
_tree.DrawSimple();
break;
case PanelMode.ComplexAssignment:
_tree.DrawAdvanced();
break;
case PanelMode.Details:
_panel.Draw();
break;
}
style.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
}
}

View file

@ -0,0 +1,299 @@
using System;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Services;
using Penumbra.UI.CollectionTab;
namespace Penumbra.UI.Tabs;
public class CollectionsTabOld : IDisposable, ITab
{
private readonly CommunicatorService _communicator;
private readonly Configuration _config;
private readonly CollectionManager _collectionManager;
private readonly TutorialService _tutorial;
private readonly SpecialCombo _specialCollectionCombo;
private readonly CollectionCombo _collectionsWithEmpty;
private readonly CollectionCombo _collectionCombo;
private readonly InheritanceUi _inheritance;
private readonly IndividualCollectionUi _individualCollections;
public CollectionsTabOld(ActorService actorService, CommunicatorService communicator, CollectionManager collectionManager,
TutorialService tutorial, Configuration config)
{
_communicator = communicator;
_collectionManager = collectionManager;
_tutorial = tutorial;
_config = config;
_specialCollectionCombo = new SpecialCombo(_collectionManager, "##NewSpecial", 350);
_collectionsWithEmpty = new CollectionCombo(_collectionManager,
() => _collectionManager.Storage.OrderBy(c => c.Name).Prepend(ModCollection.Empty).ToList());
_collectionCombo = new CollectionCombo(_collectionManager, () => _collectionManager.Storage.OrderBy(c => c.Name).ToList());
_inheritance = new InheritanceUi(_collectionManager);
_individualCollections = new IndividualCollectionUi(actorService, _collectionManager, _collectionsWithEmpty);
_communicator.CollectionChange.Subscribe(_individualCollections.UpdateIdentifiers);
}
public ReadOnlySpan<byte> Label
=> "Collections"u8;
/// <summary> Draw a collection selector of a certain width for a certain type. </summary>
public void DrawCollectionSelector(string label, float width, CollectionType collectionType, bool withEmpty)
=> (withEmpty ? _collectionsWithEmpty : _collectionCombo).Draw(label, width, collectionType);
public void Dispose()
=> _communicator.CollectionChange.Unsubscribe(_individualCollections.UpdateIdentifiers);
/// <summary> Draw a tutorial step regardless of tab selection. </summary>
public void DrawHeader()
=> _tutorial.OpenTutorial(BasicTutorialSteps.Collections);
public void DrawContent()
{
using var child = ImRaii.Child("##collections", -Vector2.One);
if (child)
{
DrawActiveCollectionSelectors();
DrawMainSelectors();
}
}
#region New Collections
// Input text fields.
private string _newCollectionName = string.Empty;
private bool _canAddCollection;
/// <summary>
/// Create a new collection that is either empty or a duplicate of the current collection.
/// Resets the new collection name.
/// </summary>
private void CreateNewCollection(bool duplicate)
{
if (_collectionManager.Storage.AddCollection(_newCollectionName, duplicate ? _collectionManager.Active.Current : null))
_newCollectionName = string.Empty;
}
/// <summary> Draw the Clean Unused Settings button if there are any. </summary>
private void DrawCleanCollectionButton(Vector2 width)
{
if (_collectionManager.Active.Current.UnusedSettings.Count == 0)
return;
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(
$"Clean {_collectionManager.Active.Current.UnusedSettings.Count} Unused Settings###CleanSettings", width
, "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk."
, false))
_collectionManager.Storage.CleanUnavailableSettings(_collectionManager.Active.Current);
}
/// <summary> Draw the new collection input as well as its buttons. </summary>
private void DrawNewCollectionInput(Vector2 width)
{
// Input for new collection name. Also checks for validity when changed.
ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X);
if (ImGui.InputTextWithHint("##New Collection", "New Collection Name...", ref _newCollectionName, 64))
_canAddCollection = _collectionManager.Storage.CanAddCollection(_newCollectionName, out _);
ImGui.SameLine();
ImGuiComponents.HelpMarker(
"A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n"
+ "You can use multiple collections to quickly switch between sets of enabled mods.");
// Creation buttons.
var tt = _canAddCollection
? string.Empty
: "Please enter a unique name only consisting of symbols valid in a path but no '|' before creating a collection.";
if (ImGuiUtil.DrawDisabledButton("Create Empty Collection", width, tt, !_canAddCollection))
CreateNewCollection(false);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton($"Duplicate {TutorialService.SelectedCollection}", width, tt, !_canAddCollection))
CreateNewCollection(true);
}
#endregion
#region Collection Selection
/// <summary> Draw all collection assignment selections. </summary>
private void DrawActiveCollectionSelectors()
{
UiHelpers.DefaultLineSpace();
var open = ImGui.CollapsingHeader(TutorialService.ActiveCollections, ImGuiTreeNodeFlags.DefaultOpen);
_tutorial.OpenTutorial(BasicTutorialSteps.ActiveCollections);
if (!open)
return;
UiHelpers.DefaultLineSpace();
DrawDefaultCollectionSelector();
_tutorial.OpenTutorial(BasicTutorialSteps.DefaultCollection);
DrawInterfaceCollectionSelector();
_tutorial.OpenTutorial(BasicTutorialSteps.InterfaceCollection);
UiHelpers.DefaultLineSpace();
DrawSpecialAssignments();
_tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections1);
UiHelpers.DefaultLineSpace();
_individualCollections.Draw();
_tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections2);
UiHelpers.DefaultLineSpace();
}
private void DrawCurrentCollectionSelector(Vector2 width)
{
using var group = ImRaii.Group();
DrawCollectionSelector("##current", UiHelpers.InputTextWidth.X, CollectionType.Current, false);
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker(TutorialService.SelectedCollection,
"This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything.");
// Deletion conditions.
var deleteCondition = _collectionManager.Active.Current.Name != ModCollection.DefaultCollectionName;
var modifierHeld = Penumbra.Config.DeleteModModifier.IsActive();
var tt = deleteCondition
? modifierHeld ? string.Empty : $"Hold {_config.DeleteModModifier} while clicking to delete the collection."
: $"You can not delete the collection {ModCollection.DefaultCollectionName}.";
if (ImGuiUtil.DrawDisabledButton($"Delete {TutorialService.SelectedCollection}", width, tt, !deleteCondition || !modifierHeld))
_collectionManager.Storage.RemoveCollection(_collectionManager.Active.Current);
DrawCleanCollectionButton(width);
}
/// <summary> Draw the selector for the default collection assignment. </summary>
private void DrawDefaultCollectionSelector()
{
using var group = ImRaii.Group();
DrawCollectionSelector("##default", UiHelpers.InputTextWidth.X, CollectionType.Default, true);
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker(TutorialService.DefaultCollection,
$"Mods in the {TutorialService.DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game,"
+ "as well as any character for whom no more specific conditions from below apply.");
}
/// <summary> Draw the selector for the interface collection assignment. </summary>
private void DrawInterfaceCollectionSelector()
{
using var group = ImRaii.Group();
DrawCollectionSelector("##interface", UiHelpers.InputTextWidth.X, CollectionType.Interface, true);
ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker(TutorialService.InterfaceCollection,
$"Mods in the {TutorialService.InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves.");
}
/// <summary> Description for character groups used in multiple help markers. </summary>
private const string CharacterGroupDescription =
$"{TutorialService.CharacterGroups} apply to certain types of characters based on a condition.\n"
+ $"All of them take precedence before the {TutorialService.DefaultCollection},\n"
+ $"but all {TutorialService.IndividualAssignments} take precedence before them.";
/// <summary> Draw the entire group assignment section. </summary>
private void DrawSpecialAssignments()
{
using var _ = ImRaii.Group();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(TutorialService.CharacterGroups);
ImGuiComponents.HelpMarker(CharacterGroupDescription);
ImGui.Separator();
DrawSpecialCollections();
ImGui.Dummy(Vector2.Zero);
DrawNewSpecialCollection();
}
/// <summary> Draw a new combo to select special collections as well as button to create it. </summary>
private void DrawNewSpecialCollection()
{
ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X);
if (_specialCollectionCombo.CurrentIdx == -1
|| _collectionManager.Active.ByType(_specialCollectionCombo.CurrentType!.Value.Item1) != null)
{
_specialCollectionCombo.ResetFilter();
_specialCollectionCombo.CurrentIdx = CollectionTypeExtensions.Special
.IndexOf(t => _collectionManager.Active.ByType(t.Item1) == null);
}
if (_specialCollectionCombo.CurrentType == null)
return;
_specialCollectionCombo.Draw();
ImGui.SameLine();
var disabled = _specialCollectionCombo.CurrentType == null;
var tt = disabled
? $"Please select a condition for a {TutorialService.GroupAssignment} before creating the collection.\n\n"
+ CharacterGroupDescription
: CharacterGroupDescription;
if (!ImGuiUtil.DrawDisabledButton($"Assign {TutorialService.ConditionalGroup}", new Vector2(120 * UiHelpers.Scale, 0), tt, disabled))
return;
_collectionManager.Active.CreateSpecialCollection(_specialCollectionCombo.CurrentType!.Value.Item1);
_specialCollectionCombo.CurrentIdx = -1;
}
#endregion
#region Current Collection Editing
/// <summary> Draw the current collection selection, the creation of new collections and the inheritance block. </summary>
private void DrawMainSelectors()
{
UiHelpers.DefaultLineSpace();
var open = ImGui.CollapsingHeader("Collection Settings", ImGuiTreeNodeFlags.DefaultOpen);
_tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections);
if (!open)
return;
var width = new Vector2((UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
UiHelpers.DefaultLineSpace();
DrawCurrentCollectionSelector(width);
_tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection);
UiHelpers.DefaultLineSpace();
DrawNewCollectionInput(width);
UiHelpers.DefaultLineSpace();
_inheritance.Draw();
_tutorial.OpenTutorial(BasicTutorialSteps.Inheritance);
}
/// <summary> Draw all currently set special collections. </summary>
private void DrawSpecialCollections()
{
foreach (var (type, name, desc) in CollectionTypeExtensions.Special)
{
var collection = _collectionManager.Active.ByType(type);
if (collection == null)
continue;
using var id = ImRaii.PushId((int)type);
DrawCollectionSelector("##SpecialCombo", UiHelpers.InputTextWidth.X, type, true);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty,
false, true))
{
_collectionManager.Active.RemoveSpecialCollection(type);
_specialCollectionCombo.ResetFilter();
}
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGuiUtil.LabeledHelpMarker(name, desc);
}
}
#endregion
}

View file

@ -9,15 +9,15 @@ using System.Numerics;
using Dalamud.Interface; using Dalamud.Interface;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Interop;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.ModsTab; using Penumbra.UI.ModsTab;
using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.UI.CollectionTab;
namespace Penumbra.UI.Tabs; namespace Penumbra.UI.Tabs;
public class ModsTab : ITab public class ModsTab : ITab
@ -25,23 +25,23 @@ public class ModsTab : ITab
private readonly ModFileSystemSelector _selector; private readonly ModFileSystemSelector _selector;
private readonly ModPanel _panel; private readonly ModPanel _panel;
private readonly TutorialService _tutorial; private readonly TutorialService _tutorial;
private readonly ModManager _modManager; private readonly ModManager _modManager;
private readonly CollectionManager _collectionManager; private readonly ActiveCollections _activeCollections;
private readonly RedrawService _redrawService; private readonly RedrawService _redrawService;
private readonly Configuration _config; private readonly Configuration _config;
private readonly CollectionsTab _collectionsTab; private readonly CollectionCombo _collectionCombo;
public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel, public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel,
TutorialService tutorial, RedrawService redrawService, Configuration config, CollectionsTab collectionsTab) TutorialService tutorial, RedrawService redrawService, Configuration config)
{ {
_modManager = modManager; _modManager = modManager;
_collectionManager = collectionManager; _activeCollections = collectionManager.Active;
_selector = selector; _selector = selector;
_panel = panel; _panel = panel;
_tutorial = tutorial; _tutorial = tutorial;
_redrawService = redrawService; _redrawService = redrawService;
_config = config; _config = config;
_collectionsTab = collectionsTab; _collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Name).ToList());
} }
public bool IsVisible public bool IsVisible
@ -62,14 +62,14 @@ public class ModsTab : ITab
{ {
try try
{ {
_selector.Draw(GetModSelectorSize()); _selector.Draw(GetModSelectorSize(_config));
ImGui.SameLine(); ImGui.SameLine();
using var group = ImRaii.Group(); using var group = ImRaii.Group();
DrawHeaderLine(); DrawHeaderLine();
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
using (var child = ImRaii.Child("##ModsTabMod", new Vector2(-1, Penumbra.Config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()), using (var child = ImRaii.Child("##ModsTabMod", new Vector2(-1, _config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()),
true, ImGuiWindowFlags.HorizontalScrollbar)) true, ImGuiWindowFlags.HorizontalScrollbar))
{ {
style.Pop(); style.Pop();
@ -86,16 +86,29 @@ public class ModsTab : ITab
{ {
Penumbra.Log.Error($"Exception thrown during ModPanel Render:\n{e}"); Penumbra.Log.Error($"Exception thrown during ModPanel Render:\n{e}");
Penumbra.Log.Error($"{_modManager.Count} Mods\n" Penumbra.Log.Error($"{_modManager.Count} Mods\n"
+ $"{_collectionManager.Active.Current.AnonymizedName} Current Collection\n" + $"{_activeCollections.Current.AnonymizedName} Current Collection\n"
+ $"{_collectionManager.Active.Current.Settings.Count} Settings\n" + $"{_activeCollections.Current.Settings.Count} Settings\n"
+ $"{_selector.SortMode.Name} Sort Mode\n" + $"{_selector.SortMode.Name} Sort Mode\n"
+ $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n"
+ $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n"
+ $"{string.Join(", ", _collectionManager.Active.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n" + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n"
+ $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n"); + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n");
} }
} }
/// <summary> Get the correct size for the mod selector based on current config. </summary>
public static float GetModSelectorSize(Configuration config)
{
var absoluteSize = Math.Clamp(config.ModSelectorAbsoluteSize, Configuration.Constants.MinAbsoluteSize,
Math.Min(Configuration.Constants.MaxAbsoluteSize, ImGui.GetContentRegionAvail().X - 100));
var relativeSize = config.ScaleModSelector
? Math.Clamp(config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize)
: 0;
return !config.ScaleModSelector
? absoluteSize
: Math.Max(absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100);
}
private void DrawRedrawLine() private void DrawRedrawLine()
{ {
if (Penumbra.Config.HideRedrawBar) if (Penumbra.Config.HideRedrawBar)
@ -159,32 +172,32 @@ public class ModsTab : ITab
ImGui.SameLine(); ImGui.SameLine();
DrawInheritedCollectionButton(3 * buttonSize); DrawInheritedCollectionButton(3 * buttonSize);
ImGui.SameLine(); ImGui.SameLine();
_collectionsTab.DrawCollectionSelector("##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false); _collectionCombo.Draw("##collectionSelector", 2 * buttonSize.X, CollectionType.Current);
} }
_tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); _tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors);
if (!_collectionManager.Active.CurrentCollectionInUse) if (!_activeCollections.CurrentCollectionInUse)
ImGuiUtil.DrawTextButton("The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg); ImGuiUtil.DrawTextButton("The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg);
} }
private void DrawDefaultCollectionButton(Vector2 width) private void DrawDefaultCollectionButton(Vector2 width)
{ {
var name = $"{TutorialService.DefaultCollection} ({_collectionManager.Active.Default.Name})"; var name = $"{TutorialService.DefaultCollection} ({_activeCollections.Default.Name})";
var isCurrent = _collectionManager.Active.Default == _collectionManager.Active.Current; var isCurrent = _activeCollections.Default == _activeCollections.Current;
var isEmpty = _collectionManager.Active.Default == ModCollection.Empty; var isEmpty = _activeCollections.Default == ModCollection.Empty;
var tt = isCurrent ? $"The current collection is already the configured {TutorialService.DefaultCollection}." var tt = isCurrent ? $"The current collection is already the configured {TutorialService.DefaultCollection}."
: isEmpty ? $"The {TutorialService.DefaultCollection} is configured to be empty." : isEmpty ? $"The {TutorialService.DefaultCollection} is configured to be empty."
: $"Set the {TutorialService.SelectedCollection} to the configured {TutorialService.DefaultCollection}."; : $"Set the {TutorialService.SelectedCollection} to the configured {TutorialService.DefaultCollection}.";
if (ImGuiUtil.DrawDisabledButton(name, width, tt, isCurrent || isEmpty)) if (ImGuiUtil.DrawDisabledButton(name, width, tt, isCurrent || isEmpty))
_collectionManager.Active.SetCollection(_collectionManager.Active.Default, CollectionType.Current); _activeCollections.SetCollection(_activeCollections.Default, CollectionType.Current);
} }
private void DrawInheritedCollectionButton(Vector2 width) private void DrawInheritedCollectionButton(Vector2 width)
{ {
var noModSelected = _selector.Selected == null; var noModSelected = _selector.Selected == null;
var collection = _selector.SelectedSettingCollection; var collection = _selector.SelectedSettingCollection;
var modInherited = collection != _collectionManager.Active.Current; var modInherited = collection != _activeCollections.Current;
var (name, tt) = (noModSelected, modInherited) switch var (name, tt) = (noModSelected, modInherited) switch
{ {
(true, _) => ("Inherited Collection", "No mod selected."), (true, _) => ("Inherited Collection", "No mod selected."),
@ -193,19 +206,6 @@ public class ModsTab : ITab
(false, false) => ("Not Inherited", "The selected mod does not inherit its settings."), (false, false) => ("Not Inherited", "The selected mod does not inherit its settings."),
}; };
if (ImGuiUtil.DrawDisabledButton(name, width, tt, noModSelected || !modInherited)) if (ImGuiUtil.DrawDisabledButton(name, width, tt, noModSelected || !modInherited))
_collectionManager.Active.SetCollection(collection, CollectionType.Current); _activeCollections.SetCollection(collection, CollectionType.Current);
}
/// <summary> Get the correct size for the mod selector based on current config. </summary>
private float GetModSelectorSize()
{
var absoluteSize = Math.Clamp(_config.ModSelectorAbsoluteSize, Configuration.Constants.MinAbsoluteSize,
Math.Min(Configuration.Constants.MaxAbsoluteSize, ImGui.GetContentRegionAvail().X - 100));
var relativeSize = _config.ScaleModSelector
? Math.Clamp(_config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize)
: 0;
return !_config.ScaleModSelector
? absoluteSize
: Math.Max(absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100);
} }
} }