Add support for retainer collections, fix deleted assignments not updating identifiers.

This commit is contained in:
Ottermandias 2022-11-21 15:33:33 +01:00
parent e47ca842b2
commit 304b75e7d2
8 changed files with 233 additions and 107 deletions

View file

@ -25,7 +25,7 @@ public readonly struct ActorIdentifier : IEquatable<ActorIdentifier>
// @formatter:on // @formatter:on
public ActorIdentifier CreatePermanent() 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) public bool Equals(ActorIdentifier other)
{ {
@ -35,11 +35,13 @@ public readonly struct ActorIdentifier : IEquatable<ActorIdentifier>
return Type switch return Type switch
{ {
IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName), 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.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName) && Manager.DataIdEquals(this, other),
IdentifierType.Special => Special == other.Special, IdentifierType.Special => Special == other.Special,
IdentifierType.Npc => Manager.DataIdEquals(this, other) IdentifierType.Npc => Manager.DataIdEquals(this, other)
&& (Index == other.Index || Index == ushort.MaxValue || other.Index == ushort.MaxValue), && (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<ActorIdentifier>
=> !lhs.Equals(rhs); => !lhs.Equals(rhs);
public bool IsValid public bool IsValid
=> Type != IdentifierType.Invalid; => Type is not (IdentifierType.UnkObject or IdentifierType.Invalid);
public override string ToString() public override string ToString()
=> Manager?.ToString(this) => Manager?.ToString(this)
?? Type switch ?? Type switch
{ {
IdentifierType.Player => $"{PlayerName} ({HomeWorld})", IdentifierType.Player => $"{PlayerName} ({HomeWorld})",
IdentifierType.Owned => $"{PlayerName}s {Kind.ToName()} {DataId} ({HomeWorld})", IdentifierType.Retainer => $"{PlayerName} (Retainer)",
IdentifierType.Special => Special.ToName(), IdentifierType.Owned => $"{PlayerName}s {Kind.ToName()} {DataId} ({HomeWorld})",
IdentifierType.Special => Special.ToName(),
IdentifierType.Npc => IdentifierType.Npc =>
Index == ushort.MaxValue Index == ushort.MaxValue
? $"{Kind.ToName()} #{DataId}" ? $"{Kind.ToName()} #{DataId}"
: $"{Kind.ToName()} #{DataId} at {Index}", : $"{Kind.ToName()} #{DataId} at {Index}",
IdentifierType.UnkObject => PlayerName.IsEmpty
? $"Unknown Object at {Index}"
: $"{PlayerName} at {Index}",
_ => "Invalid", _ => "Invalid",
}; };
public override int GetHashCode() public override int GetHashCode()
=> Type switch => Type switch
{ {
IdentifierType.Player => HashCode.Combine(IdentifierType.Player, PlayerName, HomeWorld), IdentifierType.Player => HashCode.Combine(IdentifierType.Player, PlayerName, HomeWorld),
IdentifierType.Owned => HashCode.Combine(IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId), IdentifierType.Retainer => HashCode.Combine(IdentifierType.Player, PlayerName),
IdentifierType.Special => HashCode.Combine(IdentifierType.Special, Special), IdentifierType.Owned => HashCode.Combine(IdentifierType.Owned, Kind, PlayerName, HomeWorld, DataId),
IdentifierType.Npc => HashCode.Combine(IdentifierType.Npc, Kind, DataId), IdentifierType.Special => HashCode.Combine(IdentifierType.Special, Special),
_ => 0, 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) internal ActorIdentifier(IdentifierType type, ObjectKind kind, ushort index, uint data, ByteString playerName)
@ -98,6 +106,9 @@ public readonly struct ActorIdentifier : IEquatable<ActorIdentifier>
ret.Add(nameof(PlayerName), PlayerName.ToString()); ret.Add(nameof(PlayerName), PlayerName.ToString());
ret.Add(nameof(HomeWorld), HomeWorld); ret.Add(nameof(HomeWorld), HomeWorld);
return ret; return ret;
case IdentifierType.Retainer:
ret.Add(nameof(PlayerName), PlayerName.ToString());
return ret;
case IdentifierType.Owned: case IdentifierType.Owned:
ret.Add(nameof(PlayerName), PlayerName.ToString()); ret.Add(nameof(PlayerName), PlayerName.ToString());
ret.Add(nameof(HomeWorld), HomeWorld); ret.Add(nameof(HomeWorld), HomeWorld);
@ -113,6 +124,10 @@ public readonly struct ActorIdentifier : IEquatable<ActorIdentifier>
ret.Add(nameof(Index), Index); ret.Add(nameof(Index), Index);
ret.Add(nameof(DataId), DataId); ret.Add(nameof(DataId), DataId);
return ret; return ret;
case IdentifierType.UnkObject:
ret.Add(nameof(PlayerName), PlayerName.ToString());
ret.Add(nameof(Index), Index);
return ret;
} }
return ret; return ret;
@ -162,11 +177,13 @@ public static class ActorManagerExtensions
public static string ToName(this IdentifierType type) public static string ToName(this IdentifierType type)
=> type switch => type switch
{ {
IdentifierType.Player => "Player", IdentifierType.Player => "Player",
IdentifierType.Owned => "Owned NPC", IdentifierType.Retainer => "Retainer (Bell)",
IdentifierType.Special => "Special Actor", IdentifierType.Owned => "Owned NPC",
IdentifierType.Npc => "NPC", IdentifierType.Special => "Special Actor",
_ => "Invalid", IdentifierType.Npc => "NPC",
IdentifierType.UnkObject => "Unknown Object",
_ => "Invalid",
}; };
/// <summary> /// <summary>

View file

@ -28,6 +28,11 @@ public partial class ActorManager
var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.ToObject<ushort>() ?? 0; var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.ToObject<ushort>() ?? 0;
return CreatePlayer(name, homeWorld); return CreatePlayer(name, homeWorld);
} }
case IdentifierType.Retainer:
{
var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject<string>(), false);
return CreateRetainer(name);
}
case IdentifierType.Owned: case IdentifierType.Owned:
{ {
var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject<string>(), false); var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject<string>(), false);
@ -48,6 +53,12 @@ public partial class ActorManager
var dataId = data[nameof(ActorIdentifier.DataId)]?.ToObject<uint>() ?? 0; var dataId = data[nameof(ActorIdentifier.DataId)]?.ToObject<uint>() ?? 0;
return CreateNpc(kind, dataId, index); return CreateNpc(kind, dataId, index);
} }
case IdentifierType.UnkObject:
{
var index = data[nameof(ActorIdentifier.Index)]?.ToObject<ushort>() ?? ushort.MaxValue;
var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject<string>(), false);
return CreateIndividualUnchecked(IdentifierType.UnkObject, name, index, ObjectKind.None, 0);
}
default: return ActorIdentifier.Invalid; default: return ActorIdentifier.Invalid;
} }
} }
@ -68,6 +79,7 @@ public partial class ActorManager
IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id
? $"{id.PlayerName} ({ToWorldName(id.HomeWorld)})" ? $"{id.PlayerName} ({ToWorldName(id.HomeWorld)})"
: id.PlayerName.ToString(), : id.PlayerName.ToString(),
IdentifierType.Retainer => id.PlayerName.ToString(),
IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id
? $"{id.PlayerName} ({ToWorldName(id.HomeWorld)})'s {ToName(id.Kind, id.DataId)}" ? $"{id.PlayerName} ({ToWorldName(id.HomeWorld)})'s {ToName(id.Kind, id.DataId)}"
: $"{id.PlayerName}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 id.Index == ushort.MaxValue
? ToName(id.Kind, id.DataId) ? ToName(id.Kind, id.DataId)
: $"{ToName(id.Kind, id.DataId)} at {id.Index}", : $"{ToName(id.Kind, id.DataId)} at {id.Index}",
IdentifierType.UnkObject => id.PlayerName.IsEmpty
? $"Unknown Object at {id.Index}"
: $"{id.PlayerName} at {id.Index}",
_ => "Invalid", _ => "Invalid",
}; };
} }
@ -188,7 +203,19 @@ public partial class ActorManager
: CreateIndividualUnchecked(IdentifierType.Owned, new ByteString(owner->GameObject.Name), owner->HomeWorld, : CreateIndividualUnchecked(IdentifierType.Owned, new ByteString(owner->GameObject.Name), owner->HomeWorld,
(ObjectKind)actor->ObjectKind, dataId); (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) public ActorIdentifier CreateIndividual(IdentifierType type, ByteString name, ushort homeWorld, ObjectKind kind, uint dataId)
=> type switch => type switch
{ {
IdentifierType.Player => CreatePlayer(name, homeWorld), IdentifierType.Player => CreatePlayer(name, homeWorld),
IdentifierType.Owned => CreateOwned(name, homeWorld, kind, dataId), IdentifierType.Retainer => CreateRetainer(name),
IdentifierType.Special => CreateSpecial((SpecialActor)homeWorld), IdentifierType.Owned => CreateOwned(name, homeWorld, kind, dataId),
IdentifierType.Npc => CreateNpc(kind, dataId, homeWorld), IdentifierType.Special => CreateSpecial((SpecialActor)homeWorld),
_ => ActorIdentifier.Invalid, IdentifierType.Npc => CreateNpc(kind, dataId, homeWorld),
IdentifierType.UnkObject => CreateIndividualUnchecked(IdentifierType.UnkObject, name, homeWorld, ObjectKind.None, 0),
_ => ActorIdentifier.Invalid,
}; };
/// <summary> /// <summary>
@ -234,6 +263,14 @@ public partial class ActorManager
return new ActorIdentifier(IdentifierType.Player, ObjectKind.Player, homeWorld, 0, name); 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) public ActorIdentifier CreateSpecial(SpecialActor actor)
{ {
if (!VerifySpecial(actor)) if (!VerifySpecial(actor))
@ -270,37 +307,7 @@ public partial class ActorManager
if (splitIndex < 0 || name[(splitIndex + 1)..].IndexOf((byte)' ') >= 0) if (splitIndex < 0 || name[(splitIndex + 1)..].IndexOf((byte)' ') >= 0)
return false; return false;
static bool CheckNamePart(ReadOnlySpan<byte> part) return CheckNamePart(name[..splitIndex], 2, 15) && CheckNamePart(name[(splitIndex + 1)..], 2, 15);
{
// 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)..]);
} }
/// <summary> Checks SE naming rules. </summary> /// <summary> Checks SE naming rules. </summary>
@ -315,37 +322,75 @@ public partial class ActorManager
if (splitIndex < 0 || name[(splitIndex + 1)..].IndexOf(' ') >= 0) if (splitIndex < 0 || name[(splitIndex + 1)..].IndexOf(' ') >= 0)
return false; return false;
static bool CheckNamePart(ReadOnlySpan<char> part) return CheckNamePart(name[..splitIndex], 2, 15) && CheckNamePart(name[(splitIndex + 1)..], 2, 15);
}
/// <summary> Checks SE naming rules. </summary>
public static bool VerifyRetainerName(ReadOnlySpan<byte> name)
=> CheckNamePart(name, 3, 20);
/// <summary> Checks SE naming rules. </summary>
public static bool VerifyRetainerName(ReadOnlySpan<char> name)
=> CheckNamePart(name, 3, 20);
private static bool CheckNamePart(ReadOnlySpan<char> 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. var current = part[i];
if (part.Length is < 2 or > 15) if (current is not ('\'' or '-' or (>= 'a' and <= 'z')))
return false; return false;
// Each part starting with capitalized letter. // Hyphens can not be used in succession, after or before apostrophes or as the last symbol.
if (part[0] is < 'A' or > 'Z') if (last is '\'' && current is '-')
return false;
if (last is '-' && current is '-' or '\'')
return false; return false;
// Every other symbol needs to be lowercase letter, hyphen or apostrophe. last = current;
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] != '-';
} }
return CheckNamePart(name[..splitIndex]) && CheckNamePart(name[(splitIndex + 1)..]); return part[^1] != '-';
}
private static bool CheckNamePart(ReadOnlySpan<byte> 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)'-';
} }
/// <summary> Checks if the world is a valid public world or ushort.MaxValue (any world). </summary> /// <summary> Checks if the world is a valid public world or ushort.MaxValue (any world). </summary>

View file

@ -6,5 +6,7 @@ public enum IdentifierType : byte
Player, Player,
Owned, Owned,
Special, Special,
Npc, Npc,
Retainer,
UnkObject,
}; };

View file

@ -28,6 +28,20 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ
switch( identifier.Type ) switch( identifier.Type )
{ {
case IdentifierType.Player: return CheckWorlds( identifier, out collection ); 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: case IdentifierType.Owned:
{ {
if( CheckWorlds( identifier, out collection! ) ) if( CheckWorlds( identifier, out collection! ) )

View file

@ -62,8 +62,15 @@ public sealed partial class IndividualCollections
return AddResult.Invalid; return AddResult.Invalid;
} }
var identifier = _actorManager.CreatePlayer( playerName, homeWorld ); identifiers = new[] { _actorManager.CreatePlayer( playerName, homeWorld ) };
identifiers = new[] { identifier }; break;
case IdentifierType.Retainer:
if( !ByteString.FromString( name, out var retainerName ) )
{
return AddResult.Invalid;
}
identifiers = new[] { _actorManager.CreateRetainer( retainerName ) };
break; break;
case IdentifierType.Owned: case IdentifierType.Owned:
if( !ByteString.FromString( name, out var ownerName ) ) if( !ByteString.FromString( name, out var ownerName ) )
@ -108,11 +115,12 @@ public sealed partial class IndividualCollections
return identifier.Type switch return identifier.Type switch
{ {
IdentifierType.Player => new[] { identifier.CreatePermanent() }, IdentifierType.Player => new[] { identifier.CreatePermanent() },
IdentifierType.Special => new[] { identifier }, IdentifierType.Special => new[] { identifier },
IdentifierType.Owned => CreateNpcs( _actorManager, identifier.CreatePermanent() ), IdentifierType.Retainer => new[] { identifier.CreatePermanent() },
IdentifierType.Npc => CreateNpcs( _actorManager, identifier ), IdentifierType.Owned => CreateNpcs( _actorManager, identifier.CreatePermanent() ),
_ => Array.Empty< ActorIdentifier >(), IdentifierType.Npc => CreateNpcs( _actorManager, identifier ),
_ => Array.Empty< ActorIdentifier >(),
}; };
} }
@ -197,7 +205,8 @@ public sealed partial class IndividualCollections
{ {
return identifier.Type switch 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 => IdentifierType.Owned =>
$"{identifier.PlayerName} ({_actorManager.ToWorldName( identifier.HomeWorld )})'s {_actorManager.ToName( identifier.Kind, identifier.DataId )}", $"{identifier.PlayerName} ({_actorManager.ToWorldName( identifier.HomeWorld )})'s {_actorManager.ToName( identifier.Kind, identifier.DataId )}",
IdentifierType.Npc => $"{_actorManager.ToName( identifier.Kind, identifier.DataId )} ({identifier.Kind.ToName()})", IdentifierType.Npc => $"{_actorManager.ToName( identifier.Kind, identifier.DataId )} ({identifier.Kind.ToName()})",

View file

@ -75,17 +75,21 @@ public partial class ConfigWindow
private readonly NpcCombo _bnpcCombo = new("##bnpcCombo", Penumbra.Actors.BNpcs); private readonly NpcCombo _bnpcCombo = new("##bnpcCombo", Penumbra.Actors.BNpcs);
private readonly NpcCombo _enpcCombo = new("##enpcCombo", Penumbra.Actors.ENpcs); 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 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 NewRetainerTooltipEmpty = "Please enter a valid retainer name.";
private const string AlreadyAssigned = "The Individual you specified has already been assigned a collection."; private const string NewPlayerTooltipInvalid = "The entered name is not a valid name for a player character.";
private const string NewNpcTooltipEmpty = "Please select a valid NPC from the drop down menu first."; 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 ActorIdentifier[] _newPlayerIdentifiers = Array.Empty< ActorIdentifier >();
private string _newPlayerTooltip = NewPlayerTooltipEmpty; private string _newPlayerTooltip = NewPlayerTooltipEmpty;
private ActorIdentifier[] _newNpcIdentifiers = Array.Empty< ActorIdentifier >(); private ActorIdentifier[] _newRetainerIdentifiers = Array.Empty< ActorIdentifier >();
private string _newNpcTooltip = NewNpcTooltipEmpty; private string _newRetainerTooltip = NewRetainerTooltipEmpty;
private ActorIdentifier[] _newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); private ActorIdentifier[] _newNpcIdentifiers = Array.Empty< ActorIdentifier >();
private string _newOwnedTooltip = NewPlayerTooltipEmpty; private string _newNpcTooltip = NewNpcTooltipEmpty;
private ActorIdentifier[] _newOwnedIdentifiers = Array.Empty< ActorIdentifier >();
private string _newOwnedTooltip = NewPlayerTooltipEmpty;
private bool DrawNewObjectKindOptions( float width ) private bool DrawNewObjectKindOptions( float width )
{ {
@ -201,16 +205,22 @@ public partial class ConfigWindow
private bool DrawNewOwnedCollection( Vector2 buttonWidth ) 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 ) ) if( ImGuiUtil.DrawDisabledButton( "Assign Owned NPC", buttonWidth, _newOwnedTooltip, _newOwnedIdentifiers.Length == 0 || _newOwnedTooltip.Length > 0 ) )
{ {
Penumbra.CollectionManager.Individuals.Add( _newOwnedIdentifiers, Penumbra.CollectionManager.Default ); Penumbra.CollectionManager.Individuals.Add( _newOwnedIdentifiers, Penumbra.CollectionManager.Default );
return true; 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; return false;
} }
@ -228,13 +238,19 @@ public partial class ConfigWindow
private void DrawNewIndividualCollection() private void DrawNewIndividualCollection()
{ {
var width = ( _window._inputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X ) / 3; var width = ( _window._inputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X ) / 3;
var buttonWidth = new Vector2( 90 * ImGuiHelpers.GlobalScale, 0 );
var buttonWidth1 = new Vector2( 90 * ImGuiHelpers.GlobalScale, 0 );
var buttonWidth2 = new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 );
var combo = GetNpcCombo( _newKind ); var combo = GetNpcCombo( _newKind );
var change = DrawNewPlayerCollection( buttonWidth, width ); var change = DrawNewPlayerCollection( buttonWidth1, width );
change |= DrawNewOwnedCollection( Vector2.Zero ); ImGui.SameLine();
change |= DrawNewNpcCollection( combo, buttonWidth, width ); change |= DrawNewRetainerCollection( buttonWidth2 );
change |= DrawNewNpcCollection( combo, buttonWidth1, width );
ImGui.SameLine();
change |= DrawNewOwnedCollection( buttonWidth2 );
if( change ) if( change )
{ {
@ -253,6 +269,14 @@ public partial class ConfigWindow
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty, _ => 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 ) if( combo.CurrentSelection.Ids != null )
{ {
_newNpcTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, _newNpcTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind,
@ -278,5 +302,11 @@ public partial class ConfigWindow
_newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); _newOwnedIdentifiers = Array.Empty< ActorIdentifier >();
} }
} }
private void UpdateIdentifiers( CollectionType type, ModCollection? _1, ModCollection? _2, string _3 )
{
if( type == CollectionType.Individual )
UpdateIdentifiers();
}
} }
} }

View file

@ -1,3 +1,4 @@
using System;
using System.Numerics; using System.Numerics;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
@ -13,12 +14,19 @@ namespace Penumbra.UI;
public partial class ConfigWindow public partial class ConfigWindow
{ {
// Encapsulate for less pollution. // Encapsulate for less pollution.
private partial class CollectionsTab private partial class CollectionsTab : IDisposable
{ {
private readonly ConfigWindow _window; private readonly ConfigWindow _window;
public CollectionsTab( ConfigWindow window ) public CollectionsTab( ConfigWindow window )
=> _window = window; {
_window = window;
Penumbra.CollectionManager.CollectionChanged += UpdateIdentifiers;
}
public void Dispose()
=> Penumbra.CollectionManager.CollectionChanged -= UpdateIdentifiers;
public void Draw() public void Draw()
{ {

View file

@ -137,6 +137,7 @@ public sealed partial class ConfigWindow : Window, IDisposable
{ {
_selector.Dispose(); _selector.Dispose();
_modPanel.Dispose(); _modPanel.Dispose();
_collectionsTab.Dispose();
ModEditPopup.Dispose(); ModEditPopup.Dispose();
} }