From bda3c1f1ac4592030aca265f3e4ea867f121ae5d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 16 Nov 2022 15:33:41 +0100 Subject: [PATCH] Continued work on actor identification, migration seems to work. --- Penumbra.GameData/Actors/ActorIdentifier.cs | 37 +++++- Penumbra.GameData/Actors/ActorManager.Data.cs | 64 +++++++++- .../Actors/ActorManager.Identifiers.cs | 33 +++-- .../Collections/CollectionManager.Active.cs | 15 +-- Penumbra/Collections/CollectionType.cs | 11 +- .../IndividualCollections.Access.cs | 117 ++++++++++++++++++ .../IndividualCollections.Files.cs | 89 +++++++++++++ Penumbra/Collections/IndividualCollections.cs | 109 +++++----------- Penumbra/Penumbra.cs | 8 +- Penumbra/UI/ConfigWindow.DebugTab.cs | 19 +++ Penumbra/Util/ChatUtil.cs | 20 +++ 11 files changed, 407 insertions(+), 115 deletions(-) create mode 100644 Penumbra/Collections/IndividualCollections.Access.cs create mode 100644 Penumbra/Collections/IndividualCollections.Files.cs diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index 7872bb65..14742b9c 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -59,7 +59,7 @@ public readonly struct ActorIdentifier : IEquatable { IdentifierType.Player => $"{PlayerName} ({HomeWorld})", IdentifierType.Owned => $"{PlayerName}s {Kind} {DataId} ({HomeWorld})", - IdentifierType.Special => ActorManager.ToName(Special), + IdentifierType.Special => Special.ToName(), IdentifierType.Npc => Index == ushort.MaxValue ? $"{Kind} #{DataId}" @@ -147,4 +147,39 @@ public static class ActorManagerExtensions _ => false, }; } + + public static string ToName(this ObjectKind kind) + => kind switch + { + ObjectKind.None => "Unknown", + ObjectKind.BattleNpc => "Battle NPC", + ObjectKind.EventNpc => "Event NPC", + ObjectKind.MountType => "Mount", + ObjectKind.Companion => "Companion", + _ => kind.ToString(), + }; + + public static string ToName(this IdentifierType type) + => type switch + { + IdentifierType.Player => "Player", + IdentifierType.Owned => "Owned NPC", + IdentifierType.Special => "Special Actor", + IdentifierType.Npc => "NPC", + _ => "Invalid", + }; + + /// + /// Fixed names for special actors. + /// + public static string ToName(this SpecialActor actor) + => actor switch + { + SpecialActor.CharacterScreen => "Character Screen Actor", + SpecialActor.ExamineScreen => "Examine Screen Actor", + SpecialActor.FittingRoom => "Fitting Room Actor", + SpecialActor.DyePreview => "Dye Preview Actor", + SpecialActor.Portrait => "Portrait Actor", + _ => "Invalid", + }; } diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index c00e6dd2..a1ef4b68 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -6,10 +6,17 @@ using Dalamud; using Dalamud.Data; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; +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 Penumbra.GameData.Data; +using Penumbra.String; +using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; namespace Penumbra.GameData.Actors; @@ -30,16 +37,17 @@ public sealed partial class ActorManager : DataSharer /// Valid ENPC names in title case by ENPC id. public IReadOnlyDictionary ENpcs { get; } - public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, + public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, GameGui gameGui, Func toParentIdx) - : this(pluginInterface, objects, state, gameData, gameData.Language, toParentIdx) + : this(pluginInterface, objects, state, gameData, gameGui, gameData.Language, toParentIdx) { } - public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, + public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, GameGui gameGui, ClientLanguage language, Func toParentIdx) : base(pluginInterface, language, 1) { _objects = objects; + _gameGui = gameGui; _clientState = state; _toParentIdx = toParentIdx; @@ -50,6 +58,39 @@ public sealed partial class ActorManager : DataSharer ENpcs = TryCatchData("ENpcs", () => CreateENpcData(gameData)); ActorIdentifier.Manager = this; + + SignatureHelper.Initialise(this); + } + + public unsafe ActorIdentifier GetCurrentPlayer() + { + var address = (Character*)(_objects[0]?.Address ?? IntPtr.Zero); + return address == null ? ActorIdentifier.Invalid : CreatePlayer(new ByteString(address->GameObject.Name), address->HomeWorld); + } + + public ActorIdentifier GetInspectPlayer() + { + var addon = _gameGui.GetAddonByName("CharacterInspect", 1); + if (addon == IntPtr.Zero) + return ActorIdentifier.Invalid; + + return CreatePlayer(InspectName, InspectWorldId); + } + + public unsafe ActorIdentifier GetCardPlayer() + { + var agent = AgentCharaCard.Instance(); + if (agent == null || agent->Data == null) + return ActorIdentifier.Invalid; + + var worldId = *(ushort*)((byte*)agent->Data + 0xC0); + return CreatePlayer(new ByteString(agent->Data->Name.StringPtr), worldId); + } + + public ActorIdentifier GetGlamourPlayer() + { + var addon = _gameGui.GetAddonByName("MiragePrismMiragePlate", 1); + return addon == IntPtr.Zero ? ActorIdentifier.Invalid : GetCurrentPlayer(); } protected override void DisposeInternal() @@ -66,6 +107,7 @@ public sealed partial class ActorManager : DataSharer private readonly ObjectTable _objects; private readonly ClientState _clientState; + private readonly GameGui _gameGui; private readonly Func _toParentIdx; @@ -93,4 +135,20 @@ public sealed partial class ActorManager : DataSharer => gameData.GetExcelSheet(Language)! .Where(e => e.Singular.RawData.Length > 0) .ToDictionary(e => e.RowId, e => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(e.Singular.ToDalamudString().ToString())); + + + [Signature("0F B7 0D ?? ?? ?? ?? C7 85", ScanType = ScanType.StaticAddress)] + private static unsafe ushort* _inspectTitleId = null!; + + [Signature("0F B7 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B D0", ScanType = ScanType.StaticAddress)] + private static unsafe ushort* _inspectWorldId = null!; + + private static unsafe ushort InspectTitleId + => *_inspectTitleId; + + private static unsafe ByteString InspectName + => new((byte*)(_inspectWorldId + 1)); + + private static unsafe ushort InspectWorldId + => *_inspectWorldId; } diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index eb2a73b4..6b5ebb25 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -51,6 +51,12 @@ public partial class ActorManager } } + /// + /// Return the world name including the All Worlds option. + /// + public string ToWorldName(ushort worldId) + => worldId == ushort.MaxValue ? "Any World" : Worlds.TryGetValue(worldId, out var name) ? name : "Invalid"; + /// /// Use stored data to convert an ActorIdentifier to a string. /// @@ -59,12 +65,12 @@ public partial class ActorManager return id.Type switch { IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id - ? $"{id.PlayerName} ({Worlds[id.HomeWorld]})" + ? $"{id.PlayerName} ({ToWorldName(id.HomeWorld)})" : id.PlayerName.ToString(), IdentifierType.Owned => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id - ? $"{id.PlayerName} ({Worlds[id.HomeWorld]})'s {ToName(id.Kind, id.DataId)}" - : $"{id.PlayerName}s {ToName(id.Kind, id.DataId)}", - IdentifierType.Special => ToName(id.Special), + ? $"{id.PlayerName} ({ToWorldName(id.HomeWorld)})'s {ToName(id.Kind, id.DataId)}" + : $"{id.PlayerName}s {ToName(id.Kind, id.DataId)}", + IdentifierType.Special => id.Special.ToName(), IdentifierType.Npc => id.Index == ushort.MaxValue ? ToName(id.Kind, id.DataId) @@ -74,20 +80,6 @@ public partial class ActorManager } - /// - /// Fixed names for special actors. - /// - public static string ToName(SpecialActor actor) - => actor switch - { - SpecialActor.CharacterScreen => "Character Screen Actor", - SpecialActor.ExamineScreen => "Examine Screen Actor", - SpecialActor.FittingRoom => "Fitting Room Actor", - SpecialActor.DyePreview => "Dye Preview Actor", - SpecialActor.Portrait => "Portrait Actor", - _ => "Invalid", - }; - /// /// Convert a given ID for a certain ObjectKind to a name. /// @@ -328,7 +320,7 @@ public partial class ActorManager /// Checks if the world is a valid public world or ushort.MaxValue (any world). public bool VerifyWorld(ushort worldId) - => Worlds.ContainsKey(worldId); + => worldId == ushort.MaxValue || Worlds.ContainsKey(worldId); /// Verify that the enum value is a specific actor and return the name if it is. public static bool VerifySpecial(SpecialActor actor) @@ -339,6 +331,7 @@ public partial class ActorManager { return index switch { + ushort.MaxValue => true, < 200 => index % 2 == 0, > (ushort)SpecialActor.Portrait => index < 426, _ => false, @@ -360,6 +353,8 @@ public partial class ActorManager public bool VerifyNpcData(ObjectKind kind, uint dataId) => kind switch { + ObjectKind.MountType => Mounts.ContainsKey(dataId), + ObjectKind.Companion => Companions.ContainsKey(dataId), 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 1afcdf3f..836ed3ec 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -37,6 +37,7 @@ public partial class ModCollection // The list of character collections. private readonly Dictionary< string, ModCollection > _characters = new(); + public readonly IndividualCollections Individuals = new(Penumbra.Actors); public IReadOnlyDictionary< string, ModCollection > Characters => _characters; @@ -288,6 +289,8 @@ public partial class ModCollection { SaveActiveCollections(); } + + MigrateIndividualCollections( jObject ); } // Migrate ungendered collections to Male and Female for 0.5.9.0. @@ -320,13 +323,12 @@ public partial class ModCollection } // Migrate individual collections to Identifiers for 0.6.0. - private bool MigrateIndividualCollections(JObject jObject, out IndividualCollections collections) + private bool MigrateIndividualCollections(JObject jObject) { var version = jObject[ nameof( Version ) ]?.Value< int >() ?? 0; - collections = new IndividualCollections( Penumbra.Actors ); if( version > 0 ) return false; - + // Load character collections. If a player name comes up multiple times, the last one is applied. var characters = jObject[nameof( Characters )]?.ToObject>() ?? new Dictionary(); var dict = new Dictionary< string, ModCollection >( characters.Count ); @@ -340,15 +342,14 @@ public partial class ModCollection } else { - dict.Add( player, this[idx] ); + dict.Add( player, this[ idx ] ); } } - - collections.Migrate0To1( dict ); + + Individuals.Migrate0To1( dict ); return true; } - public void SaveActiveCollections() { Penumbra.Framework.RegisterDelayed( nameof( SaveActiveCollections ), diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index 01ab5ab0..9e9c0f61 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -94,11 +94,12 @@ public enum CollectionType : byte MaleVeenaNpc, FemaleVeenaNpc, - 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 - Current, // The current collection was changed + 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 + Current, // The current collection was changed } public static class CollectionTypeExtensions diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/IndividualCollections.Access.cs new file mode 100644 index 00000000..0d295c3b --- /dev/null +++ b/Penumbra/Collections/IndividualCollections.Access.cs @@ -0,0 +1,117 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Dalamud.Game.ClientState.Objects.Types; +using Penumbra.GameData.Actors; + +namespace Penumbra.Collections; + +public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) > +{ + public IEnumerator< (string DisplayName, ModCollection Collection) > GetEnumerator() + => _assignments.Select( kvp => ( kvp.Key, kvp.Value.Collection ) ).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _assignments.Count; + + public (string DisplayName, ModCollection Collection) this[ int index ] + => ( _assignments.Keys[ index ], _assignments.Values[ index ].Collection ); + + public bool TryGetCollection( ActorIdentifier identifier, [NotNullWhen( true )] out ModCollection? collection ) + { + switch( identifier.Type ) + { + case IdentifierType.Player: return CheckWorlds( identifier, out collection ); + case IdentifierType.Owned: + { + if( CheckWorlds( identifier, out collection! ) ) + { + return true; + } + + // Handle generic NPC + var npcIdentifier = _manager.CreateNpc( identifier.Kind, identifier.DataId ); + if( npcIdentifier.IsValid && _individuals.TryGetValue( identifier, out collection ) ) + { + return true; + } + + // Handle Ownership. + if( Penumbra.Config.UseOwnerNameForCharacterCollection ) + { + identifier = _manager.CreatePlayer( identifier.PlayerName, identifier.HomeWorld ); + return CheckWorlds( identifier, out collection ); + } + + return false; + } + case IdentifierType.Npc: return _individuals.TryGetValue( identifier, out collection ); + case IdentifierType.Special: + switch( identifier.Special ) + { + case SpecialActor.CharacterScreen when Penumbra.Config.UseCharacterCollectionInMainWindow: + case SpecialActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn: + case SpecialActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn: + case SpecialActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards: + return CheckWorlds( _manager.GetCurrentPlayer(), out collection ); + case SpecialActor.ExamineScreen: + { + if( CheckWorlds( _manager.GetInspectPlayer(), out collection! ) ) + { + return true; + } + + if( CheckWorlds( _manager.GetCardPlayer(), out collection! ) ) + { + return true; + } + + if( CheckWorlds( _manager.GetGlamourPlayer(), out collection! ) ) + { + return true; + } + + break; + } + } + + break; + } + + collection = null; + return false; + } + + public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection ) + => TryGetCollection( _manager.FromObject( gameObject ), out collection ); + + public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection ) + => TryGetCollection( _manager.FromObject( gameObject ), out collection ); + + private bool CheckWorlds( ActorIdentifier identifier, out ModCollection? collection ) + { + if( !identifier.IsValid ) + { + collection = null; + return false; + } + + if( _individuals.TryGetValue( identifier, out collection ) ) + { + return true; + } + + identifier = _manager.CreateIndividual( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId ); + if( identifier.IsValid && _individuals.TryGetValue( identifier, out collection ) ) + { + return true; + } + + collection = null; + return false; + } +} \ No newline at end of file diff --git a/Penumbra/Collections/IndividualCollections.Files.cs b/Penumbra/Collections/IndividualCollections.Files.cs new file mode 100644 index 00000000..8c0997f0 --- /dev/null +++ b/Penumbra/Collections/IndividualCollections.Files.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Interface.Internal.Notifications; +using Penumbra.GameData.Actors; +using Penumbra.String; +using Penumbra.Util; + +namespace Penumbra.Collections; + +public partial class IndividualCollections +{ + public const int Version = 1; + + internal void Migrate0To1( Dictionary< string, ModCollection > old ) + { + static bool FindDataId( string name, IReadOnlyDictionary< uint, string > data, out uint dataId ) + { + var kvp = data.FirstOrDefault( kvp => kvp.Value.Equals( name, StringComparison.OrdinalIgnoreCase ), + new KeyValuePair< uint, string >( uint.MaxValue, string.Empty ) ); + dataId = kvp.Key; + return kvp.Value.Length > 0; + } + + foreach( var (name, collection) in old ) + { + var kind = ObjectKind.None; + var lowerName = name.ToLowerInvariant(); + // Prefer matching NPC names, fewer false positives than preferring players. + if( FindDataId( lowerName, _manager.Companions, out var dataId ) ) + { + kind = ObjectKind.Companion; + } + else if( FindDataId( lowerName, _manager.Mounts, out dataId ) ) + { + kind = ObjectKind.MountType; + } + else if( FindDataId( lowerName, _manager.BNpcs, out dataId ) ) + { + kind = ObjectKind.BattleNpc; + } + else if( FindDataId( lowerName, _manager.ENpcs, out dataId ) ) + { + kind = ObjectKind.EventNpc; + } + + var identifier = _manager.CreateNpc( kind, dataId ); + if( identifier.IsValid ) + { + // If the name corresponds to a valid npc, add it as a group. If this fails, notify users. + var group = GetGroup( identifier ); + var ids = string.Join( ", ", group.Select( i => i.DataId.ToString() ) ); + if( Add( $"{_manager.ToName( kind, dataId )} ({kind.ToName()})", group, collection ) ) + { + Penumbra.Log.Information( $"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]." ); + } + else + { + ChatUtil.NotificationMessage( + $"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.", + "Migration Failure", NotificationType.Error ); + } + } + // If it is not a valid NPC name, check if it can be a player name. + else if( ActorManager.VerifyPlayerName( name ) ) + { + identifier = _manager.CreatePlayer( ByteString.FromStringUnsafe( name, false ), ushort.MaxValue ); + var shortName = string.Join( " ", name.Split().Select( n => $"{n[ 0 ]}." ) ); + // Try to migrate the player name without logging full names. + if( Add( $"{name} ({_manager.ToWorldName( identifier.HomeWorld )})", new[] { identifier }, collection ) ) + { + Penumbra.Log.Information( $"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier." ); + } + else + { + ChatUtil.NotificationMessage( $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.", + "Migration Failure", NotificationType.Error ); + } + } + else + { + ChatUtil.NotificationMessage( + $"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.", + "Migration Failure", NotificationType.Error ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index c6516f2c..86a7410c 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -1,39 +1,13 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.ClientState.Objects.Types; -using Newtonsoft.Json.Linq; using Penumbra.GameData.Actors; using Penumbra.String; namespace Penumbra.Collections; -public partial class IndividualCollections -{ - public const int Version = 1; - - internal void Migrate0To1( Dictionary< string, ModCollection > old ) - { - foreach( var (name, collection) in old ) - { - if( ActorManager.VerifyPlayerName( name ) ) - { - var identifier = _manager.CreatePlayer( ByteString.FromStringUnsafe( name, false ), ushort.MaxValue ); - if( Add( name, new[] { identifier }, collection ) ) - { - var shortName = string.Join( " ", name.Split().Select( n => $"{n[0]}." ) ); - Penumbra.Log.Information( $"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier." ); - continue; - } - } - - } - } -} - -public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) > +public sealed partial class IndividualCollections { private readonly ActorManager _manager; private readonly SortedList< string, (IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > _assignments = new(); @@ -48,10 +22,34 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ public IndividualCollections( ActorManager manager ) => _manager = manager; - public bool CanAdd( params ActorIdentifier[] identifiers ) - => identifiers.Length > 0 && identifiers.All( i => i.IsValid && !Individuals.ContainsKey( i ) ); + public enum AddResult + { + Valid, + AlreadySet, + Invalid, + } - public bool CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IEnumerable< uint > dataIds, out ActorIdentifier[] identifiers ) + public AddResult CanAdd( params ActorIdentifier[] identifiers ) + { + if( identifiers.Length == 0 ) + { + return AddResult.Invalid; + } + + if( identifiers.Any( i => !i.IsValid ) ) + { + return AddResult.Invalid; + } + + if( identifiers.Any( Individuals.ContainsKey ) ) + { + return AddResult.AlreadySet; + } + + return AddResult.Valid; + } + + public AddResult CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IEnumerable< uint > dataIds, out ActorIdentifier[] identifiers ) { identifiers = Array.Empty< ActorIdentifier >(); @@ -60,7 +58,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ case IdentifierType.Player: if( !ByteString.FromString( name, out var playerName ) ) { - return false; + return AddResult.Invalid; } var identifier = _manager.CreatePlayer( playerName, homeWorld ); @@ -69,7 +67,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ case IdentifierType.Owned: if( !ByteString.FromString( name, out var ownerName ) ) { - return false; + return AddResult.Invalid; } identifiers = dataIds.Select( id => _manager.CreateOwned( ownerName, homeWorld, kind, id ) ).ToArray(); @@ -119,7 +117,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ public bool Add( string displayName, ActorIdentifier[] identifiers, ModCollection collection ) { - if( !CanAdd( identifiers ) || _assignments.ContainsKey( displayName ) ) + if( CanAdd( identifiers ) != AddResult.Valid || _assignments.ContainsKey( displayName ) ) { return false; } @@ -177,49 +175,4 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ return true; } - - public IEnumerator< (string DisplayName, ModCollection Collection) > GetEnumerator() - => _assignments.Select( kvp => ( kvp.Key, kvp.Value.Collection ) ).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public int Count - => _assignments.Count; - - public (string DisplayName, ModCollection Collection) this[ int index ] - => ( _assignments.Keys[ index ], _assignments.Values[ index ].Collection ); - - public bool TryGetCollection( ActorIdentifier identifier, out ModCollection? collection ) - { - collection = null; - if( !identifier.IsValid ) - { - return false; - } - - if( _individuals.TryGetValue( identifier, out collection ) ) - { - return true; - } - - if( identifier.Type is not (IdentifierType.Player or IdentifierType.Owned) ) - { - return false; - } - - identifier = _manager.CreateIndividual( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId ); - if( identifier.IsValid && _individuals.TryGetValue( identifier, out collection ) ) - { - return true; - } - - return false; - } - - public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection ) - => TryGetCollection( _manager.FromObject( gameObject ), out collection ); - - public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection ) - => TryGetCollection( _manager.FromObject( gameObject ), out collection ); } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index a2f167ce..6744462d 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -36,6 +36,7 @@ namespace Penumbra; public class Penumbra : IDalamudPlugin { public const string Repository = "https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json"; + public string Name => "Penumbra"; @@ -93,6 +94,7 @@ public class Penumbra : IDalamudPlugin Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ); GamePathParser = GameData.GameData.GetGamePathParser(); StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData ); + Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, Dalamud.GameGui, ResolveCutscene ); Framework = new FrameworkManager(); CharacterUtility = new CharacterUtility(); @@ -112,7 +114,6 @@ public class Penumbra : IDalamudPlugin ModFileSystem = ModFileSystem.Load(); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); - Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, u => ( short )PathResolver.CutsceneActor( u ) ); Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) { @@ -296,6 +297,9 @@ public class Penumbra : IDalamudPlugin WebServer = null; } + private short ResolveCutscene( ushort index ) + => ( short )PathResolver.CutsceneActor( index ); + public void Dispose() { StainManager?.Dispose(); @@ -586,7 +590,7 @@ public class Penumbra : IDalamudPlugin return Dalamud.PluginInterface.SourceRepository.Trim().ToLowerInvariant() switch { null => false, - Repository => true, + Repository => true, "https://raw.githubusercontent.com/xivdev/Penumbra/test/repo.json" => true, _ => false, }; diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 5cf59e80..1818fbe8 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData.Actors; using Penumbra.GameData.Files; using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; @@ -167,6 +168,24 @@ public partial class ConfigWindow return; } + static void DrawSpecial( string name, ActorIdentifier id ) + { + if( !id.IsValid ) + { + return; + } + + ImGuiUtil.DrawTableColumn( name ); + ImGuiUtil.DrawTableColumn( string.Empty ); + ImGuiUtil.DrawTableColumn( Penumbra.Actors.ToString( id ) ); + ImGuiUtil.DrawTableColumn( string.Empty ); + } + + DrawSpecial( "Current Player", Penumbra.Actors.GetCurrentPlayer() ); + DrawSpecial( "Current Inspect", Penumbra.Actors.GetInspectPlayer() ); + DrawSpecial( "Current Card", Penumbra.Actors.GetCardPlayer() ); + DrawSpecial( "Current Glamour", Penumbra.Actors.GetGlamourPlayer() ); + foreach( var obj in Dalamud.Objects ) { ImGuiUtil.DrawTableColumn( $"{( ( GameObject* )obj.Address )->ObjectIndex}" ); diff --git a/Penumbra/Util/ChatUtil.cs b/Penumbra/Util/ChatUtil.cs index 0b500f17..220da9bf 100644 --- a/Penumbra/Util/ChatUtil.cs +++ b/Penumbra/Util/ChatUtil.cs @@ -1,8 +1,13 @@ +using System; using System.Collections.Generic; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Utility; using Lumina.Excel.GeneratedSheets; +using OtterGui.Log; namespace Penumbra.Util; @@ -32,4 +37,19 @@ public static class ChatUtil Message = payload, } ); } + + public static void NotificationMessage( string content, string? title = null, NotificationType type = NotificationType.None ) + { + var logLevel = type switch + { + NotificationType.None => Logger.LogLevel.Information, + NotificationType.Success => Logger.LogLevel.Information, + NotificationType.Warning => Logger.LogLevel.Warning, + NotificationType.Error => Logger.LogLevel.Error, + NotificationType.Info => Logger.LogLevel.Information, + _ => Logger.LogLevel.Debug, + }; + Dalamud.PluginInterface.UiBuilder.AddNotification( content, title, type ); + Penumbra.Log.Message( logLevel, title.IsNullOrEmpty() ? content : $"[{title}] {content}" ); + } } \ No newline at end of file