diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index 74e6d5f1..c45821e9 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -25,7 +25,7 @@ public readonly struct ActorIdentifier : IEquatable // @formatter:on public ActorIdentifier CreatePermanent() - => new(Type, Kind, Index, DataId, PlayerName.Clone()); + => new(Type, Kind, Index, DataId, PlayerName.IsEmpty ? PlayerName : PlayerName.Clone()); public bool Equals(ActorIdentifier other) { @@ -35,11 +35,13 @@ public readonly struct ActorIdentifier : IEquatable return Type switch { IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName), + IdentifierType.Retainer => PlayerName.EqualsCi(other.PlayerName), IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName) && Manager.DataIdEquals(this, other), IdentifierType.Special => Special == other.Special, IdentifierType.Npc => Manager.DataIdEquals(this, other) && (Index == other.Index || Index == ushort.MaxValue || other.Index == ushort.MaxValue), - _ => false, + IdentifierType.UnkObject => PlayerName.EqualsCi(other.PlayerName) && Index == other.Index, + _ => false, }; } @@ -53,30 +55,36 @@ public readonly struct ActorIdentifier : IEquatable => !lhs.Equals(rhs); public bool IsValid - => Type != IdentifierType.Invalid; + => Type is not (IdentifierType.UnkObject or IdentifierType.Invalid); public override string ToString() => Manager?.ToString(this) ?? Type switch { - IdentifierType.Player => $"{PlayerName} ({HomeWorld})", - IdentifierType.Owned => $"{PlayerName}s {Kind.ToName()} {DataId} ({HomeWorld})", - IdentifierType.Special => Special.ToName(), + IdentifierType.Player => $"{PlayerName} ({HomeWorld})", + IdentifierType.Retainer => $"{PlayerName} (Retainer)", + IdentifierType.Owned => $"{PlayerName}s {Kind.ToName()} {DataId} ({HomeWorld})", + IdentifierType.Special => Special.ToName(), IdentifierType.Npc => Index == ushort.MaxValue ? $"{Kind.ToName()} #{DataId}" : $"{Kind.ToName()} #{DataId} at {Index}", + IdentifierType.UnkObject => PlayerName.IsEmpty + ? $"Unknown Object at {Index}" + : $"{PlayerName} at {Index}", _ => "Invalid", }; public override int GetHashCode() => Type switch { - IdentifierType.Player => HashCode.Combine(IdentifierType.Player, PlayerName, HomeWorld), - IdentifierType.Owned => HashCode.Combine(IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId), - IdentifierType.Special => HashCode.Combine(IdentifierType.Special, Special), - IdentifierType.Npc => HashCode.Combine(IdentifierType.Npc, Kind, DataId), - _ => 0, + IdentifierType.Player => HashCode.Combine(IdentifierType.Player, PlayerName, HomeWorld), + IdentifierType.Retainer => HashCode.Combine(IdentifierType.Player, PlayerName), + IdentifierType.Owned => HashCode.Combine(IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId), + IdentifierType.Special => HashCode.Combine(IdentifierType.Special, Special), + IdentifierType.Npc => HashCode.Combine(IdentifierType.Npc, Kind, DataId), + IdentifierType.UnkObject => HashCode.Combine(IdentifierType.UnkObject, PlayerName, Index), + _ => 0, }; internal ActorIdentifier(IdentifierType type, ObjectKind kind, ushort index, uint data, ByteString playerName) @@ -98,6 +106,9 @@ public readonly struct ActorIdentifier : IEquatable ret.Add(nameof(PlayerName), PlayerName.ToString()); ret.Add(nameof(HomeWorld), HomeWorld); return ret; + case IdentifierType.Retainer: + ret.Add(nameof(PlayerName), PlayerName.ToString()); + return ret; case IdentifierType.Owned: ret.Add(nameof(PlayerName), PlayerName.ToString()); ret.Add(nameof(HomeWorld), HomeWorld); @@ -113,6 +124,10 @@ public readonly struct ActorIdentifier : IEquatable ret.Add(nameof(Index), Index); ret.Add(nameof(DataId), DataId); return ret; + case IdentifierType.UnkObject: + ret.Add(nameof(PlayerName), PlayerName.ToString()); + ret.Add(nameof(Index), Index); + return ret; } return ret; @@ -162,11 +177,13 @@ public static class ActorManagerExtensions public static string ToName(this IdentifierType type) => type switch { - IdentifierType.Player => "Player", - IdentifierType.Owned => "Owned NPC", - IdentifierType.Special => "Special Actor", - IdentifierType.Npc => "NPC", - _ => "Invalid", + IdentifierType.Player => "Player", + IdentifierType.Retainer => "Retainer (Bell)", + IdentifierType.Owned => "Owned NPC", + IdentifierType.Special => "Special Actor", + IdentifierType.Npc => "NPC", + IdentifierType.UnkObject => "Unknown Object", + _ => "Invalid", }; /// diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 48c8a2e2..eb21861b 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -28,6 +28,11 @@ public partial class ActorManager var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.ToObject() ?? 0; return CreatePlayer(name, homeWorld); } + case IdentifierType.Retainer: + { + var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject(), false); + return CreateRetainer(name); + } case IdentifierType.Owned: { var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject(), false); @@ -48,6 +53,12 @@ public partial class ActorManager var dataId = data[nameof(ActorIdentifier.DataId)]?.ToObject() ?? 0; return CreateNpc(kind, dataId, index); } + case IdentifierType.UnkObject: + { + var index = data[nameof(ActorIdentifier.Index)]?.ToObject() ?? ushort.MaxValue; + var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject(), false); + return CreateIndividualUnchecked(IdentifierType.UnkObject, name, index, ObjectKind.None, 0); + } default: return ActorIdentifier.Invalid; } } @@ -68,6 +79,7 @@ public partial class ActorManager IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id ? $"{id.PlayerName} ({ToWorldName(id.HomeWorld)})" : id.PlayerName.ToString(), + IdentifierType.Retainer => id.PlayerName.ToString(), IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id ? $"{id.PlayerName} ({ToWorldName(id.HomeWorld)})'s {ToName(id.Kind, id.DataId)}" : $"{id.PlayerName}s {ToName(id.Kind, id.DataId)}", @@ -76,6 +88,9 @@ public partial class ActorManager id.Index == ushort.MaxValue ? ToName(id.Kind, id.DataId) : $"{ToName(id.Kind, id.DataId)} at {id.Index}", + IdentifierType.UnkObject => id.PlayerName.IsEmpty + ? $"Unknown Object at {id.Index}" + : $"{id.PlayerName} at {id.Index}", _ => "Invalid", }; } @@ -188,7 +203,19 @@ public partial class ActorManager : CreateIndividualUnchecked(IdentifierType.Owned, new ByteString(owner->GameObject.Name), owner->HomeWorld, (ObjectKind)actor->ObjectKind, dataId); } - default: return ActorIdentifier.Invalid; + case ObjectKind.Retainer: + { + var name = new ByteString(actor->Name); + return check + ? CreateRetainer(name) + : CreateIndividualUnchecked(IdentifierType.Retainer, name, 0, ObjectKind.None, uint.MaxValue); + } + default: + { + var name = new ByteString(actor->Name); + var index = actor->ObjectIndex; + return CreateIndividualUnchecked(IdentifierType.UnkObject, name, index, ObjectKind.None, 0); + } } } @@ -213,11 +240,13 @@ public partial class ActorManager public ActorIdentifier CreateIndividual(IdentifierType type, ByteString name, ushort homeWorld, ObjectKind kind, uint dataId) => type switch { - IdentifierType.Player => CreatePlayer(name, homeWorld), - IdentifierType.Owned => CreateOwned(name, homeWorld, kind, dataId), - IdentifierType.Special => CreateSpecial((SpecialActor)homeWorld), - IdentifierType.Npc => CreateNpc(kind, dataId, homeWorld), - _ => ActorIdentifier.Invalid, + IdentifierType.Player => CreatePlayer(name, homeWorld), + IdentifierType.Retainer => CreateRetainer(name), + IdentifierType.Owned => CreateOwned(name, homeWorld, kind, dataId), + IdentifierType.Special => CreateSpecial((SpecialActor)homeWorld), + IdentifierType.Npc => CreateNpc(kind, dataId, homeWorld), + IdentifierType.UnkObject => CreateIndividualUnchecked(IdentifierType.UnkObject, name, homeWorld, ObjectKind.None, 0), + _ => ActorIdentifier.Invalid, }; /// @@ -234,6 +263,14 @@ public partial class ActorManager return new ActorIdentifier(IdentifierType.Player, ObjectKind.Player, homeWorld, 0, name); } + public ActorIdentifier CreateRetainer(ByteString name) + { + if (!VerifyRetainerName(name.Span)) + return ActorIdentifier.Invalid; + + return new ActorIdentifier(IdentifierType.Retainer, ObjectKind.Retainer, 0, 0, name); + } + public ActorIdentifier CreateSpecial(SpecialActor actor) { if (!VerifySpecial(actor)) @@ -270,37 +307,7 @@ public partial class ActorManager if (splitIndex < 0 || name[(splitIndex + 1)..].IndexOf((byte)' ') >= 0) return false; - static bool CheckNamePart(ReadOnlySpan part) - { - // Each name part at least 2 and at most 15 characters. - if (part.Length is < 2 or > 15) - return false; - - // Each part starting with capitalized letter. - if (part[0] is < (byte)'A' or > (byte)'Z') - return false; - - // Every other symbol needs to be lowercase letter, hyphen or apostrophe. - var last = (byte)'\0'; - for (var i = 1; i < part.Length; ++i) - { - var current = part[i]; - if (current is not ((byte)'\'' or (byte)'-' or (>= (byte)'a' and <= (byte)'z'))) - return false; - - // Hyphens can not be used in succession, after or before apostrophes or as the last symbol. - if (last is (byte)'\'' && current is (byte)'-') - return false; - if (last is (byte)'-' && current is (byte)'-' or (byte)'\'') - return false; - - last = current; - } - - return part[^1] != (byte)'-'; - } - - return CheckNamePart(name[..splitIndex]) && CheckNamePart(name[(splitIndex + 1)..]); + return CheckNamePart(name[..splitIndex], 2, 15) && CheckNamePart(name[(splitIndex + 1)..], 2, 15); } /// Checks SE naming rules. @@ -315,37 +322,75 @@ public partial class ActorManager if (splitIndex < 0 || name[(splitIndex + 1)..].IndexOf(' ') >= 0) return false; - static bool CheckNamePart(ReadOnlySpan part) + return CheckNamePart(name[..splitIndex], 2, 15) && CheckNamePart(name[(splitIndex + 1)..], 2, 15); + } + + /// Checks SE naming rules. + public static bool VerifyRetainerName(ReadOnlySpan name) + => CheckNamePart(name, 3, 20); + + /// Checks SE naming rules. + public static bool VerifyRetainerName(ReadOnlySpan name) + => CheckNamePart(name, 3, 20); + + private static bool CheckNamePart(ReadOnlySpan part, int minLength, int maxLength) + { + // Each name part at least 2 and at most 15 characters for players, and at least 3 and at most 20 characters for retainers. + if (part.Length < minLength || part.Length > maxLength) + return false; + + // Each part starting with capitalized letter. + if (part[0] is < 'A' or > 'Z') + return false; + + // Every other symbol needs to be lowercase letter, hyphen or apostrophe. + var last = '\0'; + for (var i = 1; i < part.Length; ++i) { - // Each name part at least 2 and at most 15 characters. - if (part.Length is < 2 or > 15) + var current = part[i]; + if (current is not ('\'' or '-' or (>= 'a' and <= 'z'))) return false; - // Each part starting with capitalized letter. - if (part[0] is < 'A' or > 'Z') + // Hyphens can not be used in succession, after or before apostrophes or as the last symbol. + if (last is '\'' && current is '-') + return false; + if (last is '-' && current is '-' or '\'') return false; - // Every other symbol needs to be lowercase letter, hyphen or apostrophe. - var last = '\0'; - for (var i = 1; i < part.Length; ++i) - { - var current = part[i]; - if (current is not ('\'' or '-' or (>= 'a' and <= 'z'))) - return false; - - // Hyphens can not be used in succession, after or before apostrophes or as the last symbol. - if (last is '\'' && current is '-') - return false; - if (last is '-' && current is '-' or '\'') - return false; - - last = current; - } - - return part[^1] != '-'; + last = current; } - return CheckNamePart(name[..splitIndex]) && CheckNamePart(name[(splitIndex + 1)..]); + return part[^1] != '-'; + } + + private static bool CheckNamePart(ReadOnlySpan part, int minLength, int maxLength) + { + // Each name part at least 2 and at most 15 characters for players, and at least 3 and at most 20 characters for retainers. + if (part.Length < minLength || part.Length > maxLength) + return false; + + // Each part starting with capitalized letter. + if (part[0] is < (byte)'A' or > (byte)'Z') + return false; + + // Every other symbol needs to be lowercase letter, hyphen or apostrophe. + var last = (byte)'\0'; + for (var i = 1; i < part.Length; ++i) + { + var current = part[i]; + if (current is not ((byte)'\'' or (byte)'-' or (>= (byte)'a' and <= (byte)'z'))) + return false; + + // Hyphens can not be used in succession, after or before apostrophes or as the last symbol. + if (last is (byte)'\'' && current is (byte)'-') + return false; + if (last is (byte)'-' && current is (byte)'-' or (byte)'\'') + return false; + + last = current; + } + + return part[^1] != (byte)'-'; } /// Checks if the world is a valid public world or ushort.MaxValue (any world). diff --git a/Penumbra.GameData/Actors/IdentifierType.cs b/Penumbra.GameData/Actors/IdentifierType.cs index a582aa14..8fe1ee4f 100644 --- a/Penumbra.GameData/Actors/IdentifierType.cs +++ b/Penumbra.GameData/Actors/IdentifierType.cs @@ -6,5 +6,7 @@ public enum IdentifierType : byte Player, Owned, Special, - Npc, + Npc, + Retainer, + UnkObject, }; \ No newline at end of file diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index f09dadb0..01858010 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -28,6 +28,20 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ switch( identifier.Type ) { case IdentifierType.Player: return CheckWorlds( identifier, out collection ); + case IdentifierType.Retainer: + { + if( _individuals.TryGetValue( identifier, out collection ) ) + { + return true; + } + + if( Penumbra.Config.UseOwnerNameForCharacterCollection ) + { + return CheckWorlds( _actorManager.GetCurrentPlayer(), out collection ); + } + + break; + } case IdentifierType.Owned: { if( CheckWorlds( identifier, out collection! ) ) diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index a8239eb5..57f7f824 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -62,8 +62,15 @@ public sealed partial class IndividualCollections return AddResult.Invalid; } - var identifier = _actorManager.CreatePlayer( playerName, homeWorld ); - identifiers = new[] { identifier }; + identifiers = new[] { _actorManager.CreatePlayer( playerName, homeWorld ) }; + break; + case IdentifierType.Retainer: + if( !ByteString.FromString( name, out var retainerName ) ) + { + return AddResult.Invalid; + } + + identifiers = new[] { _actorManager.CreateRetainer( retainerName ) }; break; case IdentifierType.Owned: if( !ByteString.FromString( name, out var ownerName ) ) @@ -108,11 +115,12 @@ public sealed partial class IndividualCollections return identifier.Type switch { - IdentifierType.Player => new[] { identifier.CreatePermanent() }, - IdentifierType.Special => new[] { identifier }, - IdentifierType.Owned => CreateNpcs( _actorManager, identifier.CreatePermanent() ), - IdentifierType.Npc => CreateNpcs( _actorManager, identifier ), - _ => Array.Empty< ActorIdentifier >(), + IdentifierType.Player => new[] { identifier.CreatePermanent() }, + IdentifierType.Special => new[] { identifier }, + IdentifierType.Retainer => new[] { identifier.CreatePermanent() }, + IdentifierType.Owned => CreateNpcs( _actorManager, identifier.CreatePermanent() ), + IdentifierType.Npc => CreateNpcs( _actorManager, identifier ), + _ => Array.Empty< ActorIdentifier >(), }; } @@ -197,7 +205,8 @@ public sealed partial class IndividualCollections { return identifier.Type switch { - IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.ToWorldName( identifier.HomeWorld )})", + IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.ToWorldName( identifier.HomeWorld )})", + IdentifierType.Retainer => $"{identifier.PlayerName} (Retainer)", IdentifierType.Owned => $"{identifier.PlayerName} ({_actorManager.ToWorldName( identifier.HomeWorld )})'s {_actorManager.ToName( identifier.Kind, identifier.DataId )}", IdentifierType.Npc => $"{_actorManager.ToName( identifier.Kind, identifier.DataId )} ({identifier.Kind.ToName()})", diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 9737969d..5e8c4967 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -75,17 +75,21 @@ public partial class ConfigWindow private readonly NpcCombo _bnpcCombo = new("##bnpcCombo", Penumbra.Actors.BNpcs); private readonly NpcCombo _enpcCombo = new("##enpcCombo", Penumbra.Actors.ENpcs); - private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'."; - private const string NewPlayerTooltipInvalid = "The entered name is not a valid name for a player character."; - private const string AlreadyAssigned = "The Individual you specified has already been assigned a collection."; - private const string NewNpcTooltipEmpty = "Please select a valid NPC from the drop down menu first."; + private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'."; + private const string NewRetainerTooltipEmpty = "Please enter a valid retainer name."; + private const string NewPlayerTooltipInvalid = "The entered name is not a valid name for a player character."; + private const string NewRetainerTooltipInvalid = "The entered name is not a valid name for a retainer."; + private const string AlreadyAssigned = "The Individual you specified has already been assigned a collection."; + private const string NewNpcTooltipEmpty = "Please select a valid NPC from the drop down menu first."; - private ActorIdentifier[] _newPlayerIdentifiers = Array.Empty< ActorIdentifier >(); - private string _newPlayerTooltip = NewPlayerTooltipEmpty; - private ActorIdentifier[] _newNpcIdentifiers = Array.Empty< ActorIdentifier >(); - private string _newNpcTooltip = NewNpcTooltipEmpty; - private ActorIdentifier[] _newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); - private string _newOwnedTooltip = NewPlayerTooltipEmpty; + private ActorIdentifier[] _newPlayerIdentifiers = Array.Empty< ActorIdentifier >(); + private string _newPlayerTooltip = NewPlayerTooltipEmpty; + private ActorIdentifier[] _newRetainerIdentifiers = Array.Empty< ActorIdentifier >(); + private string _newRetainerTooltip = NewRetainerTooltipEmpty; + private ActorIdentifier[] _newNpcIdentifiers = Array.Empty< ActorIdentifier >(); + private string _newNpcTooltip = NewNpcTooltipEmpty; + private ActorIdentifier[] _newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); + private string _newOwnedTooltip = NewPlayerTooltipEmpty; private bool DrawNewObjectKindOptions( float width ) { @@ -201,16 +205,22 @@ public partial class ConfigWindow private bool DrawNewOwnedCollection( Vector2 buttonWidth ) { - var oldPos = ImGui.GetCursorPos(); - ImGui.SameLine(); - ImGui.SetCursorPos( ImGui.GetCursorPos() + new Vector2( -ImGui.GetStyle().ItemSpacing.X / 2, ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y ) / 2 ); if( ImGuiUtil.DrawDisabledButton( "Assign Owned NPC", buttonWidth, _newOwnedTooltip, _newOwnedIdentifiers.Length == 0 || _newOwnedTooltip.Length > 0 ) ) { Penumbra.CollectionManager.Individuals.Add( _newOwnedIdentifiers, Penumbra.CollectionManager.Default ); return true; } - ImGui.SetCursorPos( oldPos ); + return false; + } + + private bool DrawNewRetainerCollection( Vector2 buttonWidth ) + { + if( ImGuiUtil.DrawDisabledButton( "Assign Bell Retainer", buttonWidth, _newRetainerTooltip, _newRetainerIdentifiers.Length == 0 || _newRetainerTooltip.Length > 0 ) ) + { + Penumbra.CollectionManager.Individuals.Add( _newRetainerIdentifiers, Penumbra.CollectionManager.Default ); + return true; + } return false; } @@ -228,13 +238,19 @@ public partial class ConfigWindow private void DrawNewIndividualCollection() { - var width = ( _window._inputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X ) / 3; - var buttonWidth = new Vector2( 90 * ImGuiHelpers.GlobalScale, 0 ); + var width = ( _window._inputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X ) / 3; + + var buttonWidth1 = new Vector2( 90 * ImGuiHelpers.GlobalScale, 0 ); + var buttonWidth2 = new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ); var combo = GetNpcCombo( _newKind ); - var change = DrawNewPlayerCollection( buttonWidth, width ); - change |= DrawNewOwnedCollection( Vector2.Zero ); - change |= DrawNewNpcCollection( combo, buttonWidth, width ); + var change = DrawNewPlayerCollection( buttonWidth1, width ); + ImGui.SameLine(); + change |= DrawNewRetainerCollection( buttonWidth2 ); + + change |= DrawNewNpcCollection( combo, buttonWidth1, width ); + ImGui.SameLine(); + change |= DrawNewOwnedCollection( buttonWidth2 ); if( change ) { @@ -253,6 +269,14 @@ public partial class ConfigWindow IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, _ => string.Empty, }; + _newRetainerTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Retainer, _newCharacterName, _worldCombo.CurrentSelection.Key, ObjectKind.None, + Array.Empty< uint >(), out _newRetainerIdentifiers ) switch + { + _ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewRetainerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; if( combo.CurrentSelection.Ids != null ) { _newNpcTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, @@ -278,5 +302,11 @@ public partial class ConfigWindow _newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); } } + + private void UpdateIdentifiers( CollectionType type, ModCollection? _1, ModCollection? _2, string _3 ) + { + if( type == CollectionType.Individual ) + UpdateIdentifiers(); + } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index aa6b6a4e..d1769cd3 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Components; @@ -13,12 +14,19 @@ namespace Penumbra.UI; public partial class ConfigWindow { // Encapsulate for less pollution. - private partial class CollectionsTab + private partial class CollectionsTab : IDisposable { private readonly ConfigWindow _window; public CollectionsTab( ConfigWindow window ) - => _window = window; + { + _window = window; + + Penumbra.CollectionManager.CollectionChanged += UpdateIdentifiers; + } + + public void Dispose() + => Penumbra.CollectionManager.CollectionChanged -= UpdateIdentifiers; public void Draw() { diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index afe8c9be..f367a92b 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -137,6 +137,7 @@ public sealed partial class ConfigWindow : Window, IDisposable { _selector.Dispose(); _modPanel.Dispose(); + _collectionsTab.Dispose(); ModEditPopup.Dispose(); }