diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index 14742b9c..dc3ffff6 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Objects.Enums; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using Newtonsoft.Json.Linq; using Penumbra.String; @@ -58,12 +60,12 @@ public readonly struct ActorIdentifier : IEquatable ?? Type switch { IdentifierType.Player => $"{PlayerName} ({HomeWorld})", - IdentifierType.Owned => $"{PlayerName}s {Kind} {DataId} ({HomeWorld})", + IdentifierType.Owned => $"{PlayerName}s {Kind.ToName()} {DataId} ({HomeWorld})", IdentifierType.Special => Special.ToName(), IdentifierType.Npc => Index == ushort.MaxValue - ? $"{Kind} #{DataId}" - : $"{Kind} #{DataId} at {Index}", + ? $"{Kind.ToName()} #{DataId}" + : $"{Kind.ToName()} #{DataId} at {Index}", _ => "Invalid", }; @@ -87,7 +89,6 @@ public readonly struct ActorIdentifier : IEquatable PlayerName = playerName; } - public JObject ToJson() { var ret = new JObject { { nameof(Type), Type.ToString() } }; @@ -130,22 +131,19 @@ public static class ActorManagerExtensions if (manager == null) return lhs.Kind == rhs.Kind && lhs.DataId == rhs.DataId || lhs.DataId == uint.MaxValue || rhs.DataId == uint.MaxValue; - return lhs.Kind switch + var dict = lhs.Kind switch { - ObjectKind.MountType => manager.Mounts.TryGetValue(lhs.DataId, out var lhsName) - && manager.Mounts.TryGetValue(rhs.DataId, out var rhsName) - && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase), - ObjectKind.Companion => manager.Companions.TryGetValue(lhs.DataId, out var lhsName) - && manager.Companions.TryGetValue(rhs.DataId, out var rhsName) - && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase), - ObjectKind.BattleNpc => manager.BNpcs.TryGetValue(lhs.DataId, out var lhsName) - && manager.BNpcs.TryGetValue(rhs.DataId, out var rhsName) - && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase), - ObjectKind.EventNpc => manager.ENpcs.TryGetValue(lhs.DataId, out var lhsName) - && manager.ENpcs.TryGetValue(rhs.DataId, out var rhsName) - && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase), - _ => false, + ObjectKind.MountType => manager.Mounts, + ObjectKind.Companion => manager.Companions, + (ObjectKind)15 => manager.Ornaments, // TODO: CS Update + ObjectKind.BattleNpc => manager.BNpcs, + ObjectKind.EventNpc => manager.ENpcs, + _ => new Dictionary(), }; + + return dict.TryGetValue(lhs.DataId, out var lhsName) + && dict.TryGetValue(rhs.DataId, out var rhsName) + && lhsName.Equals(rhsName, StringComparison.OrdinalIgnoreCase); } public static string ToName(this ObjectKind kind) @@ -156,6 +154,7 @@ public static class ActorManagerExtensions ObjectKind.EventNpc => "Event NPC", ObjectKind.MountType => "Mount", ObjectKind.Companion => "Companion", + (ObjectKind)15 => "Accessory", // TODO: CS update _ => kind.ToString(), }; diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index a1ef4b68..f1c3c4bf 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -2,18 +2,19 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text; using Dalamud; using Dalamud.Data; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.Gui; using Dalamud.Plugin; using Dalamud.Utility; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using FFXIVClientStructs.FFXIV.Component.GUI; -using Lumina.Excel; using Lumina.Excel.GeneratedSheets; +using Lumina.Text; using Penumbra.GameData.Data; using Penumbra.String; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; @@ -31,6 +32,9 @@ public sealed partial class ActorManager : DataSharer /// Valid Companion names in title case by companion id. public IReadOnlyDictionary Companions { get; } + /// Valid ornament names by id. + public IReadOnlyDictionary Ornaments { get; } + /// Valid BNPC names in title case by BNPC Name id. public IReadOnlyDictionary BNpcs { get; } @@ -54,6 +58,7 @@ public sealed partial class ActorManager : DataSharer Worlds = TryCatchData("Worlds", () => CreateWorldData(gameData)); Mounts = TryCatchData("Mounts", () => CreateMountData(gameData)); Companions = TryCatchData("Companions", () => CreateCompanionData(gameData)); + Ornaments = TryCatchData("Ornaments", () => CreateOrnamentData(gameData)); BNpcs = TryCatchData("BNpcs", () => CreateBNpcData(gameData)); ENpcs = TryCatchData("ENpcs", () => CreateENpcData(gameData)); @@ -98,6 +103,7 @@ public sealed partial class ActorManager : DataSharer DisposeTag("Worlds"); DisposeTag("Mounts"); DisposeTag("Companions"); + DisposeTag("Ornaments"); DisposeTag("BNpcs"); DisposeTag("ENpcs"); } @@ -119,22 +125,50 @@ public sealed partial class ActorManager : DataSharer private IReadOnlyDictionary CreateMountData(DataManager gameData) => gameData.GetExcelSheet(Language)! .Where(m => m.Singular.RawData.Length > 0 && m.Order >= 0) - .ToDictionary(m => m.RowId, m => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(m.Singular.ToDalamudString().ToString())); + .ToDictionary(m => m.RowId, m => ToTitleCaseExtended(m.Singular, m.Article)); private IReadOnlyDictionary CreateCompanionData(DataManager gameData) => gameData.GetExcelSheet(Language)! .Where(c => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue) - .ToDictionary(c => c.RowId, c => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(c.Singular.ToDalamudString().ToString())); + .ToDictionary(c => c.RowId, c => ToTitleCaseExtended(c.Singular, c.Article)); + + private IReadOnlyDictionary CreateOrnamentData(DataManager gameData) + => gameData.GetExcelSheet(Language)! + .Where(o => o.Singular.RawData.Length > 0) + .ToDictionary(o => o.RowId, o => ToTitleCaseExtended(o.Singular, o.Article)); private IReadOnlyDictionary CreateBNpcData(DataManager gameData) => gameData.GetExcelSheet(Language)! .Where(n => n.Singular.RawData.Length > 0) - .ToDictionary(n => n.RowId, n => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(n.Singular.ToDalamudString().ToString())); + .ToDictionary(n => n.RowId, n => ToTitleCaseExtended(n.Singular, n.Article)); private IReadOnlyDictionary CreateENpcData(DataManager gameData) => gameData.GetExcelSheet(Language)! .Where(e => e.Singular.RawData.Length > 0) - .ToDictionary(e => e.RowId, e => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(e.Singular.ToDalamudString().ToString())); + .ToDictionary(e => e.RowId, e => ToTitleCaseExtended(e.Singular, e.Article)); + + private static string ToTitleCaseExtended(SeString s, sbyte article) + { + if (article == 1) + return s.ToDalamudString().ToString(); + + var sb = new StringBuilder(s.ToDalamudString().ToString()); + var lastSpace = true; + for (var i = 0; i < sb.Length; ++i) + { + if (sb[i] == ' ') + { + lastSpace = true; + } + else if (lastSpace) + { + lastSpace = false; + sb[i] = char.ToUpperInvariant(sb[i]); + } + } + + return sb.ToString(); + } [Signature("0F B7 0D ?? ?? ?? ?? C7 85", ScanType = ScanType.StaticAddress)] diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 6b5ebb25..ca5731f5 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -52,7 +52,7 @@ public partial class ActorManager } /// - /// Return the world name including the All Worlds option. + /// Return the world name including the Any World option. /// public string ToWorldName(ushort worldId) => worldId == ushort.MaxValue ? "Any World" : Worlds.TryGetValue(worldId, out var name) ? name : "Invalid"; @@ -98,6 +98,7 @@ public partial class ActorManager { ObjectKind.MountType => Mounts.TryGetValue(dataId, out name), ObjectKind.Companion => Companions.TryGetValue(dataId, out name), + (ObjectKind)15 => Ornaments.TryGetValue(dataId, out name), // TODO: CS Update ObjectKind.BattleNpc => BNpcs.TryGetValue(dataId, out name), ObjectKind.EventNpc => ENpcs.TryGetValue(dataId, out name), _ => false, @@ -154,6 +155,7 @@ public partial class ActorManager case ObjectKind.EventNpc: return CreateNpc(ObjectKind.EventNpc, actor->DataID, actor->ObjectIndex); case ObjectKind.MountType: case ObjectKind.Companion: + case (ObjectKind)15: // TODO: CS Update { if (actor->ObjectIndex % 2 == 0) return ActorIdentifier.Invalid; @@ -173,11 +175,12 @@ public partial class ActorManager /// Obtain the current companion ID for an object by its actor and owner. /// private unsafe uint GetCompanionId(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, - FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner) + FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner) // TODO: CS Update { return (ObjectKind)actor->ObjectKind switch { - ObjectKind.MountType => *(ushort*)((byte*)owner + 0x668), + ObjectKind.MountType => *(ushort*)((byte*)owner + 0x650 + 0x18), + (ObjectKind)15 => *(ushort*)((byte*)owner + 0x860 + 0x18), ObjectKind.Companion => *(ushort*)((byte*)actor + 0x1AAC), _ => actor->DataID, }; @@ -196,6 +199,12 @@ public partial class ActorManager _ => ActorIdentifier.Invalid, }; + /// + /// Only use this if you are sure the input is valid. + /// + public ActorIdentifier CreateIndividualUnchecked(IdentifierType type, ByteString name, ushort homeWorld, ObjectKind kind, uint dataId) + => new(type, kind, homeWorld, dataId, name); + public ActorIdentifier CreatePlayer(ByteString name, ushort homeWorld) { if (!VerifyWorld(homeWorld) || !VerifyPlayerName(name.Span)) @@ -345,6 +354,7 @@ public partial class ActorManager { ObjectKind.MountType => Mounts.ContainsKey(dataId), ObjectKind.Companion => Companions.ContainsKey(dataId), + (ObjectKind)15 => Ornaments.ContainsKey(dataId), // TODO: CS Update ObjectKind.BattleNpc => BNpcs.ContainsKey(dataId), _ => false, }; @@ -355,6 +365,7 @@ public partial class ActorManager { ObjectKind.MountType => Mounts.ContainsKey(dataId), ObjectKind.Companion => Companions.ContainsKey(dataId), + (ObjectKind)15 => Ornaments.ContainsKey(dataId), // TODO: CS Update ObjectKind.BattleNpc => BNpcs.ContainsKey(dataId), ObjectKind.EventNpc => ENpcs.ContainsKey(dataId), _ => false, diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 836ed3ec..53db52ee 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Penumbra.GameData.Actors; namespace Penumbra.Collections; @@ -43,8 +44,8 @@ public partial class ModCollection => _characters; // If a name does not correspond to a character, return the default collection instead. - public ModCollection Character( string name ) - => _characters.TryGetValue( name, out var c ) ? c : Default; + public ModCollection Individual( ActorIdentifier identifier ) + => Individuals.Individuals.TryGetValue( identifier, out var c ) ? c : Default; // Special Collections private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< CollectionType >().Length - 4]; @@ -62,7 +63,7 @@ public partial class ModCollection CollectionType.Default => Default, CollectionType.Interface => Interface, CollectionType.Current => Current, - CollectionType.Character => name != null ? _characters.TryGetValue( name, out var c ) ? c : null : null, + CollectionType.Individual => name != null ? _characters.TryGetValue( name, out var c ) ? c : null : null, CollectionType.Inactive => name != null ? ByName( name, out var c ) ? c : null : null, _ => null, }; @@ -76,7 +77,7 @@ public partial class ModCollection CollectionType.Default => Default.Index, CollectionType.Interface => Interface.Index, CollectionType.Current => Current.Index, - CollectionType.Character => characterName?.Length > 0 + CollectionType.Individual => characterName?.Length > 0 ? _characters.TryGetValue( characterName, out var c ) ? c.Index : Default.Index @@ -113,7 +114,7 @@ public partial class ModCollection case CollectionType.Current: Current = newCollection; break; - case CollectionType.Character: + case CollectionType.Individual: _characters[ characterName! ] = newCollection; break; default: @@ -176,7 +177,7 @@ public partial class ModCollection } _characters[ characterName ] = Default; - CollectionChanged.Invoke( CollectionType.Character, null, Default, characterName ); + CollectionChanged.Invoke( CollectionType.Individual, null, Default, characterName ); return true; } @@ -187,7 +188,7 @@ public partial class ModCollection { RemoveCache( collection.Index ); _characters.Remove( characterName ); - CollectionChanged.Invoke( CollectionType.Character, collection, null, characterName ); + CollectionChanged.Invoke( CollectionType.Individual, collection, null, characterName ); } } diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index 9e9c0f61..fed83db5 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -97,8 +97,7 @@ public enum CollectionType : byte Inactive, // A collection was added or removed Default, // The default collection was changed Interface, // The ui collection was changed - Character, // A character collection was changed - Individual, // An Individual collection was changed + Individual, // An individual collection was changed Current, // The current collection was changed } @@ -288,7 +287,7 @@ public static class CollectionTypeExtensions CollectionType.Inactive => "Collection", CollectionType.Default => "Default", CollectionType.Interface => "Interface", - CollectionType.Character => "Character", + CollectionType.Individual => "Character", CollectionType.Current => "Current", _ => string.Empty, }; diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs index 0d295c3b..933121f7 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -105,7 +105,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ return true; } - identifier = _manager.CreateIndividual( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId ); + identifier = _manager.CreateIndividualUnchecked( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId ); if( identifier.IsValid && _individuals.TryGetValue( identifier, out collection ) ) { return true; diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index 86a7410c..fc4a83b2 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -102,7 +102,7 @@ public sealed partial class IndividualCollections _ => throw new NotImplementedException(), }; return table.Where( kvp => kvp.Value == name ) - .Select( kvp => manager.CreateIndividual( identifier.Type, identifier.PlayerName, identifier.HomeWorld, identifier.Kind, kvp.Key ) ).ToArray(); + .Select( kvp => manager.CreateIndividualUnchecked( identifier.Type, identifier.PlayerName, identifier.HomeWorld, identifier.Kind, kvp.Key ) ).ToArray(); } return identifier.Type switch @@ -115,9 +115,27 @@ public sealed partial class IndividualCollections }; } - public bool Add( string displayName, ActorIdentifier[] identifiers, ModCollection collection ) + public bool Add( ActorIdentifier[] identifiers, ModCollection collection ) { - if( CanAdd( identifiers ) != AddResult.Valid || _assignments.ContainsKey( displayName ) ) + if( identifiers.Length == 0 || !identifiers[ 0 ].IsValid ) + { + return false; + } + + var name = identifiers[ 0 ].Type switch + { + IdentifierType.Player => $"{identifiers[ 0 ].PlayerName} ({_manager.ToWorldName( identifiers[ 0 ].HomeWorld )})", + IdentifierType.Owned => + $"{identifiers[ 0 ].PlayerName} ({_manager.ToWorldName( identifiers[ 0 ].HomeWorld )})'s {_manager.ToName( identifiers[ 0 ].Kind, identifiers[ 0 ].DataId )}", + IdentifierType.Npc => $"{_manager.ToName( identifiers[ 0 ].Kind, identifiers[ 0 ].DataId )} ({identifiers[ 0 ].Kind})", + _ => string.Empty, + }; + return Add( name, identifiers, collection ); + } + + private bool Add( string displayName, ActorIdentifier[] identifiers, ModCollection collection ) + { + if( CanAdd( identifiers ) != AddResult.Valid || displayName.Length == 0 || _assignments.ContainsKey( displayName ) ) { return false; } diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs index 8d7eba65..c850b418 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Dalamud.Interface; using ImGuiNET; @@ -7,8 +8,10 @@ using Penumbra.Collections; using System.Linq; using System.Numerics; using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Interface.Components; using OtterGui.Widgets; using Penumbra.GameData.Actors; +using Lumina.Data.Parsing; namespace Penumbra.UI; @@ -18,7 +21,7 @@ public partial class ConfigWindow { private sealed class WorldCombo : FilterComboCache< KeyValuePair< ushort, string > > { - private static readonly KeyValuePair< ushort, string > AllWorldPair = new(ushort.MaxValue, "All Worlds"); + private static readonly KeyValuePair< ushort, string > AllWorldPair = new(ushort.MaxValue, "Any World"); public WorldCombo( IReadOnlyDictionary< ushort, string > worlds ) : base( worlds.OrderBy( kvp => kvp.Value ).Prepend( AllWorldPair ) ) @@ -30,7 +33,7 @@ public partial class ConfigWindow protected override string ToString( KeyValuePair< ushort, string > obj ) => obj.Value; - public void Draw( float width ) + public bool Draw( float width ) => Draw( "##worldCombo", CurrentSelection.Value, width, ImGui.GetTextLineHeightWithSpacing() ); } @@ -57,147 +60,78 @@ public partial class ConfigWindow return ret; } - public void Draw( float width ) + public bool Draw( float width ) => Draw( _label, CurrentSelection.Name, width, ImGui.GetTextLineHeightWithSpacing() ); } // Input Selections. - private string _newCharacterName = string.Empty; - private IdentifierType _newType = IdentifierType.Player; - private ObjectKind _newKind = ObjectKind.BattleNpc; + private string _newCharacterName = string.Empty; + private ObjectKind _newKind = ObjectKind.BattleNpc; private readonly WorldCombo _worldCombo = new(Penumbra.Actors.Worlds); private readonly NpcCombo _mountCombo = new("##mountCombo", Penumbra.Actors.Mounts); private readonly NpcCombo _companionCombo = new("##companionCombo", Penumbra.Actors.Companions); + private readonly NpcCombo _ornamentCombo = new("##ornamentCombo", Penumbra.Actors.Ornaments); private readonly NpcCombo _bnpcCombo = new("##bnpcCombo", Penumbra.Actors.BNpcs); private readonly NpcCombo _enpcCombo = new("##enpcCombo", Penumbra.Actors.ENpcs); - private void DrawNewIdentifierOptions( float width ) + 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 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 bool DrawNewObjectKindOptions( float width ) { ImGui.SetNextItemWidth( width ); - using var combo = ImRaii.Combo( "##newType", _newType.ToString() ); - if( combo ) + using var combo = ImRaii.Combo( "##newKind", _newKind.ToName() ); + if( !combo ) { - if( ImGui.Selectable( IdentifierType.Player.ToString(), _newType == IdentifierType.Player ) ) - { - _newType = IdentifierType.Player; - } + return false; + } - if( ImGui.Selectable( IdentifierType.Owned.ToString(), _newType == IdentifierType.Owned ) ) + var ret = false; + foreach( var kind in new[] { ObjectKind.BattleNpc, ObjectKind.EventNpc, ObjectKind.Companion, ObjectKind.MountType, ( ObjectKind )15 } ) // TODO: CS Update + { + if( ImGui.Selectable( kind.ToName(), _newKind == kind ) ) { - _newType = IdentifierType.Owned; - } - - if( ImGui.Selectable( IdentifierType.Npc.ToString(), _newType == IdentifierType.Npc ) ) - { - _newType = IdentifierType.Npc; + _newKind = kind; + ret = true; } } + + return ret; } - private void DrawNewObjectKindOptions( float width ) - { - ImGui.SetNextItemWidth( width ); - using var combo = ImRaii.Combo( "##newKind", _newKind.ToString() ); - if( combo ) - { - if( ImGui.Selectable( ObjectKind.BattleNpc.ToString(), _newKind == ObjectKind.BattleNpc ) ) - { - _newKind = ObjectKind.BattleNpc; - } - - if( ImGui.Selectable( ObjectKind.EventNpc.ToString(), _newKind == ObjectKind.EventNpc ) ) - { - _newKind = ObjectKind.EventNpc; - } - - if( ImGui.Selectable( ObjectKind.Companion.ToString(), _newKind == ObjectKind.Companion ) ) - { - _newKind = ObjectKind.Companion; - } - - if( ImGui.Selectable( ObjectKind.MountType.ToString(), _newKind == ObjectKind.MountType ) ) - { - _newKind = ObjectKind.MountType; - } - } - } - - // We do not check for valid character names. - private void DrawNewCharacterCollection() - { - const string description = "Character Collections apply specifically to individual game objects of the given name.\n" - + $"More general {GroupAssignment} or the {DefaultCollection} do not apply if an .\n" - + "Certain actors - like the ones in cutscenes or preview windows - will try to use appropriate character collections.\n"; - - var width = ( _window._inputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X ) / 3; - DrawNewIdentifierOptions( width ); - ImGui.SameLine(); - using( var dis = ImRaii.Disabled( _newType == IdentifierType.Npc ) ) - { - _worldCombo.Draw( width ); - } - - ImGui.SameLine(); - - using( var dis = ImRaii.Disabled( _newType == IdentifierType.Player ) ) - { - DrawNewObjectKindOptions( width ); - } - - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - using( var dis = ImRaii.Disabled( _newType == IdentifierType.Npc ) ) - { - ImGui.InputTextWithHint( "##NewCharacter", "Character Name...", ref _newCharacterName, 32 ); - } - - ImGui.SameLine(); - var disabled = _newCharacterName.Length == 0; - var tt = disabled - ? $"Please enter the name of a {ConditionalIndividual} before assigning the collection.\n\n" + description - : description; - if( ImGuiUtil.DrawDisabledButton( $"Assign {ConditionalIndividual}", new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ), tt, - disabled ) ) - { - Penumbra.CollectionManager.CreateCharacterCollection( _newCharacterName ); - _newCharacterName = string.Empty; - } - - using( var dis = ImRaii.Disabled( _newType == IdentifierType.Player ) ) - { - switch( _newKind ) - { - case ObjectKind.BattleNpc: - _bnpcCombo.Draw( _window._inputTextWidth.X ); - break; - case ObjectKind.EventNpc: - _enpcCombo.Draw( _window._inputTextWidth.X ); - break; - case ObjectKind.Companion: - _companionCombo.Draw( _window._inputTextWidth.X ); - break; - case ObjectKind.MountType: - _mountCombo.Draw( _window._inputTextWidth.X ); - break; - } - } - } private void DrawIndividualAssignments() { - using var _ = ImRaii.Group(); + using var _ = ImRaii.Group(); + using var mainId = ImRaii.PushId( "Individual" ); + ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted( $"Individual {ConditionalIndividual}s" ); + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Individual Collections apply specifically to individual game objects that fulfill the given criteria.\n" + + $"More general {GroupAssignment} or the {DefaultCollection} do not apply if an Individual Collection takes effect.\n" + + "Certain related actors - like the ones in cutscenes or preview windows - will try to use appropriate individual collections." ); ImGui.Separator(); - foreach( var name in Penumbra.CollectionManager.Characters.Keys.OrderBy( k => k ).ToArray() ) + for( var i = 0; i < Penumbra.CollectionManager.Individuals.Count; ++i ) { - using var id = ImRaii.PushId( name ); + var (name, collection) = Penumbra.CollectionManager.Individuals[ i ]; + using var id = ImRaii.PushId( i ); DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, CollectionType.Character, true, name ); ImGui.SameLine(); if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, false, true ) ) { - Penumbra.CollectionManager.RemoveCharacterCollection( name ); + Penumbra.CollectionManager.Individuals.Delete( i ); } ImGui.SameLine(); @@ -206,7 +140,120 @@ public partial class ConfigWindow } ImGui.Dummy( Vector2.Zero ); - DrawNewCharacterCollection(); + DrawNewIndividualCollection(); + } + + private bool DrawNewPlayerCollection( Vector2 buttonWidth, float width ) + { + var change = _worldCombo.Draw( width ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( _window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width ); + change |= ImGui.InputTextWithHint( "##NewCharacter", "Character Name...", ref _newCharacterName, 32 ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Assign Player", buttonWidth, _newPlayerTooltip, _newPlayerTooltip.Length > 0 || _newPlayerIdentifiers.Length == 0 ) ) + { + Penumbra.CollectionManager.Individuals.Add( _newPlayerIdentifiers, Penumbra.CollectionManager.Default ); + change = true; + } + + return change; + } + + private bool DrawNewNpcCollection( NpcCombo combo, Vector2 buttonWidth, float width ) + { + var comboWidth = _window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width; + var change = DrawNewObjectKindOptions( width ); + ImGui.SameLine(); + change |= combo.Draw( comboWidth ); + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Assign NPC", buttonWidth, _newNpcTooltip, _newNpcIdentifiers.Length == 0 || _newNpcTooltip.Length > 0 ) ) + { + Penumbra.CollectionManager.Individuals.Add( _newNpcIdentifiers, Penumbra.CollectionManager.Default ); + change = true; + } + + return change; + } + + 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 NpcCombo GetNpcCombo( ObjectKind kind ) + => kind switch + { + ObjectKind.BattleNpc => _bnpcCombo, + ObjectKind.EventNpc => _enpcCombo, + ObjectKind.MountType => _mountCombo, + ObjectKind.Companion => _companionCombo, + ( ObjectKind )15 => _ornamentCombo, // TODO: CS update + _ => throw new NotImplementedException(), + }; + + private void DrawNewIndividualCollection() + { + var width = ( _window._inputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X ) / 3; + var buttonWidth = new Vector2( 90 * ImGuiHelpers.GlobalScale, 0 ); + + var combo = GetNpcCombo( _newKind ); + var change = DrawNewPlayerCollection( buttonWidth, width ); + change |= DrawNewOwnedCollection( Vector2.Zero ); + change |= DrawNewNpcCollection( combo, buttonWidth, width ); + + if( change ) + { + UpdateIdentifiers(); + } + } + + private void UpdateIdentifiers() + { + var combo = GetNpcCombo( _newKind ); + _newPlayerTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Player, _newCharacterName, _worldCombo.CurrentSelection.Key, ObjectKind.None, + Array.Empty< uint >(), out _newPlayerIdentifiers ) switch + { + _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + if( combo.CurrentSelection.Ids != null ) + { + _newNpcTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, + combo.CurrentSelection.Ids, out _newNpcIdentifiers ) switch + { + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + _newOwnedTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Owned, _newCharacterName, _worldCombo.CurrentSelection.Key, _newKind, + combo.CurrentSelection.Ids, out _newOwnedIdentifiers ) switch + { + _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + } + else + { + _newNpcTooltip = NewNpcTooltipEmpty; + _newOwnedTooltip = NewNpcTooltipEmpty; + _newNpcIdentifiers = Array.Empty< ActorIdentifier >(); + _newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); + } } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 6025cb70..1fd968c5 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -161,7 +161,7 @@ public partial class ConfigWindow public void Draw() { var preview = CurrentIdx >= 0 ? Items[ CurrentIdx ].Item2 : string.Empty; - Draw(_label, preview, ref CurrentIdx, _unscaledWidth * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeightWithSpacing()); + Draw( _label, preview, ref CurrentIdx, _unscaledWidth * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeightWithSpacing() ); } protected override string ToString( (CollectionType, string, string) obj ) @@ -176,15 +176,16 @@ public partial class ConfigWindow private readonly SpecialCombo _specialCollectionCombo = new("##NewSpecial", 350); + private const string CharacterGroupDescription = $"{CharacterGroups} apply to certain types of characters based on a condition.\n" + + $"All of them take precedence before the {DefaultCollection},\n" + + $"but all {IndividualAssignments} take precedence before them."; + + // We do not check for valid character names. private void DrawNewSpecialCollection() { - const string description = $"{CharacterGroups} apply to certain types of characters based on a condition.\n" - + $"All of them take precedence before the {DefaultCollection},\n" - + $"but all {IndividualAssignments} take precedence before them."; - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( _specialCollectionCombo.CurrentIdx == -1 + if( _specialCollectionCombo.CurrentIdx == -1 || Penumbra.CollectionManager.ByType( _specialCollectionCombo.CurrentType!.Value.Item1 ) != null ) { _specialCollectionCombo.ResetFilter(); @@ -201,8 +202,8 @@ public partial class ConfigWindow ImGui.SameLine(); var disabled = _specialCollectionCombo.CurrentType == null; var tt = disabled - ? $"Please select a condition for a {GroupAssignment} before creating the collection.\n\n" + description - : description; + ? $"Please select a condition for a {GroupAssignment} before creating the collection.\n\n" + CharacterGroupDescription + : CharacterGroupDescription; if( ImGuiUtil.DrawDisabledButton( $"Assign {ConditionalGroup}", new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ), tt, disabled ) ) { Penumbra.CollectionManager.CreateSpecialCollection( _specialCollectionCombo.CurrentType!.Value.Item1 ); @@ -237,7 +238,9 @@ public partial class ConfigWindow private void DrawSpecialAssignments() { using var _ = ImRaii.Group(); + ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted( CharacterGroups ); + ImGuiComponents.HelpMarker( CharacterGroupDescription ); ImGui.Separator(); DrawSpecialCollections(); ImGui.Dummy( Vector2.Zero );