diff --git a/OtterGui b/OtterGui index 0d2284a8..49f5aaa7 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0d2284a82504aac0bff797fa3355f750a3e68834 +Subproject commit 49f5aaa7733fc74d77435e9b84ce347eb06f61be diff --git a/Penumbra.GameData/Actors/ActorIdentifier.cs b/Penumbra.GameData/Actors/ActorIdentifier.cs index 5651382e..7872bb65 100644 --- a/Penumbra.GameData/Actors/ActorIdentifier.cs +++ b/Penumbra.GameData/Actors/ActorIdentifier.cs @@ -6,8 +6,8 @@ using Penumbra.String; namespace Penumbra.GameData.Actors; -[StructLayout( LayoutKind.Explicit )] -public readonly struct ActorIdentifier : IEquatable< ActorIdentifier > +[StructLayout(LayoutKind.Explicit)] +public readonly struct ActorIdentifier : IEquatable { public static ActorManager? Manager; @@ -26,36 +26,40 @@ public readonly struct ActorIdentifier : IEquatable< ActorIdentifier > public ActorIdentifier CreatePermanent() => new(Type, Kind, Index, DataId, PlayerName.Clone()); - public bool Equals( ActorIdentifier other ) + public bool Equals(ActorIdentifier other) { - if( Type != other.Type ) - { + if (Type != other.Type) return false; - } return Type switch { - IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi( other.PlayerName ), - IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi( other.PlayerName ) && Manager.DataIdEquals( this, other ), + IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName), + IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName) && Manager.DataIdEquals(this, other), IdentifierType.Special => Special == other.Special, - IdentifierType.Npc => Index == other.Index && DataId == other.DataId && Manager.DataIdEquals( this, other ), - _ => false, + IdentifierType.Npc => Index == other.Index && DataId == other.DataId && Manager.DataIdEquals(this, other), + _ => false, }; } - public override bool Equals( object? obj ) - => obj is ActorIdentifier other && Equals( other ); + public override bool Equals(object? obj) + => obj is ActorIdentifier other && Equals(other); + + public static bool operator ==(ActorIdentifier lhs, ActorIdentifier rhs) + => lhs.Equals(rhs); + + public static bool operator !=(ActorIdentifier lhs, ActorIdentifier rhs) + => !lhs.Equals(rhs); public bool IsValid => Type != IdentifierType.Invalid; public override string ToString() - => Manager?.ToString( this ) + => Manager?.ToString(this) ?? Type switch { IdentifierType.Player => $"{PlayerName} ({HomeWorld})", IdentifierType.Owned => $"{PlayerName}s {Kind} {DataId} ({HomeWorld})", - IdentifierType.Special => ActorManager.ToName( Special ), + IdentifierType.Special => ActorManager.ToName(Special), IdentifierType.Npc => Index == ushort.MaxValue ? $"{Kind} #{DataId}" @@ -66,18 +70,18 @@ public readonly struct ActorIdentifier : IEquatable< ActorIdentifier > 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, Index, DataId ), + 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, Index, DataId), _ => 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) { Type = type; Kind = kind; - Special = ( SpecialActor )index; + Special = (SpecialActor)index; HomeWorld = Index = index; DataId = data; PlayerName = playerName; @@ -86,26 +90,26 @@ public readonly struct ActorIdentifier : IEquatable< ActorIdentifier > public JObject ToJson() { - var ret = new JObject { { nameof( Type ), Type.ToString() } }; - switch( Type ) + var ret = new JObject { { nameof(Type), Type.ToString() } }; + switch (Type) { case IdentifierType.Player: - ret.Add( nameof( PlayerName ), PlayerName.ToString() ); - ret.Add( nameof( HomeWorld ), HomeWorld ); + ret.Add(nameof(PlayerName), PlayerName.ToString()); + ret.Add(nameof(HomeWorld), HomeWorld); return ret; case IdentifierType.Owned: - ret.Add( nameof( PlayerName ), PlayerName.ToString() ); - ret.Add( nameof( HomeWorld ), HomeWorld ); - ret.Add( nameof( Kind ), Kind.ToString() ); - ret.Add( nameof( DataId ), DataId ); + ret.Add(nameof(PlayerName), PlayerName.ToString()); + ret.Add(nameof(HomeWorld), HomeWorld); + ret.Add(nameof(Kind), Kind.ToString()); + ret.Add(nameof(DataId), DataId); return ret; case IdentifierType.Special: - ret.Add( nameof( Special ), Special.ToString() ); + ret.Add(nameof(Special), Special.ToString()); return ret; case IdentifierType.Npc: - ret.Add( nameof( Kind ), Kind.ToString() ); - ret.Add( nameof( Index ), Index ); - ret.Add( nameof( DataId ), DataId ); + ret.Add(nameof(Kind), Kind.ToString()); + ret.Add(nameof(Index), Index); + ret.Add(nameof(DataId), DataId); return ret; } @@ -115,38 +119,32 @@ public readonly struct ActorIdentifier : IEquatable< ActorIdentifier > public static class ActorManagerExtensions { - public static bool DataIdEquals( this ActorManager? manager, ActorIdentifier lhs, ActorIdentifier rhs ) + public static bool DataIdEquals(this ActorManager? manager, ActorIdentifier lhs, ActorIdentifier rhs) { - if( lhs.Kind != rhs.Kind ) - { + if (lhs.Kind != rhs.Kind) return false; - } - if( lhs.DataId == rhs.DataId ) - { + if (lhs.DataId == rhs.DataId) return true; - } - if( manager == null ) - { + if (manager == null) return lhs.Kind == rhs.Kind && lhs.DataId == rhs.DataId || lhs.DataId == uint.MaxValue || rhs.DataId == uint.MaxValue; - } return 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 ), + 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, }; } -} \ No newline at end of file +} diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs new file mode 100644 index 00000000..9cf233c4 --- /dev/null +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Dalamud; +using Dalamud.Data; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Utility; +using Lumina.Excel.GeneratedSheets; + +namespace Penumbra.GameData.Actors; + +public partial class ActorManager : IDisposable +{ + /// Worlds available for players. + public IReadOnlyDictionary Worlds { get; } + + /// Valid Mount names in title case by mount id. + public IReadOnlyDictionary Mounts { get; } + + /// Valid Companion names in title case by companion id. + public IReadOnlyDictionary Companions { get; } + + /// Valid BNPC names in title case by BNPC Name id. + public IReadOnlyDictionary BNpcs { get; } + + /// Valid ENPC names in title case by ENPC id. + public IReadOnlyDictionary ENpcs { get; } + + public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, Func toParentIdx) + : this(pluginInterface, objects, state, gameData, gameData.Language, toParentIdx) + {} + + public ActorManager(DalamudPluginInterface pluginInterface, ObjectTable objects, ClientState state, DataManager gameData, + ClientLanguage language, Func toParentIdx) + { + _pluginInterface = pluginInterface; + _objects = objects; + _clientState = state; + _gameData = gameData; + _language = language; + _toParentIdx = toParentIdx; + + Worlds = TryCatchData("Worlds", CreateWorldData); + Mounts = TryCatchData("Mounts", CreateMountData); + Companions = TryCatchData("Companions", CreateCompanionData); + BNpcs = TryCatchData("BNpcs", CreateBNpcData); + ENpcs = TryCatchData("ENpcs", CreateENpcData); + + ActorIdentifier.Manager = this; + } + + public void Dispose() + { + if (_disposed) + return; + + GC.SuppressFinalize(this); + _pluginInterface.RelinquishData(GetVersionedTag("Worlds")); + _pluginInterface.RelinquishData(GetVersionedTag("Mounts")); + _pluginInterface.RelinquishData(GetVersionedTag("Companions")); + _pluginInterface.RelinquishData(GetVersionedTag("BNpcs")); + _pluginInterface.RelinquishData(GetVersionedTag("ENpcs")); + _disposed = true; + } + + ~ActorManager() + => Dispose(); + + private const int Version = 1; + + private readonly DalamudPluginInterface _pluginInterface; + private readonly ObjectTable _objects; + private readonly ClientState _clientState; + private readonly DataManager _gameData; + private readonly ClientLanguage _language; + private bool _disposed; + + private readonly Func _toParentIdx; + + private IReadOnlyDictionary CreateWorldData() + => _gameData.GetExcelSheet(_language)! + .Where(w => w.IsPublic && !w.Name.RawData.IsEmpty) + .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); + + private IReadOnlyDictionary CreateMountData() + => _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())); + + private IReadOnlyDictionary CreateCompanionData() + => _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())); + + private IReadOnlyDictionary CreateBNpcData() + => _gameData.GetExcelSheet(_language)! + .Where(n => n.Singular.RawData.Length > 0) + .ToDictionary(n => n.RowId, n => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(n.Singular.ToDalamudString().ToString())); + + private IReadOnlyDictionary CreateENpcData() + => _gameData.GetExcelSheet(_language)! + .Where(e => e.Singular.RawData.Length > 0) + .ToDictionary(e => e.RowId, e => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(e.Singular.ToDalamudString().ToString())); + + private string GetVersionedTag(string tag) + => $"Penumbra.Actors.{tag}.{_language}.V{Version}"; + + private T TryCatchData(string tag, Func func) where T : class + { + try + { + return _pluginInterface.GetOrCreateData(GetVersionedTag(tag), func); + } + catch (Exception ex) + { + PluginLog.Error($"Error creating shared actor data for {tag}:\n{ex}"); + return func(); + } + } +} diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs new file mode 100644 index 00000000..b8c9640b --- /dev/null +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -0,0 +1,302 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Newtonsoft.Json.Linq; +using Penumbra.String; + +namespace Penumbra.GameData.Actors; + +public partial class ActorManager +{ + /// + /// Try to create an ActorIdentifier from a already parsed JObject . + /// + /// A parsed JObject + /// ActorIdentifier.Invalid if the JObject can not be converted, a valid ActorIdentifier otherwise. + public ActorIdentifier FromJson(JObject data) + { + var type = data[nameof(ActorIdentifier.Type)]?.Value() ?? IdentifierType.Invalid; + switch (type) + { + case IdentifierType.Player: + { + var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.Value(), false); + var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.Value() ?? 0; + return CreatePlayer(name, homeWorld); + } + case IdentifierType.Owned: + { + var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.Value(), false); + var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.Value() ?? 0; + var kind = data[nameof(ActorIdentifier.Kind)]?.Value() ?? ObjectKind.CardStand; + var dataId = data[nameof(ActorIdentifier.DataId)]?.Value() ?? 0; + return CreateOwned(name, homeWorld, kind, dataId); + } + case IdentifierType.Special: + { + var special = data[nameof(ActorIdentifier.Special)]?.Value() ?? 0; + return CreateSpecial(special); + } + case IdentifierType.Npc: + { + var index = data[nameof(ActorIdentifier.Index)]?.Value() ?? ushort.MaxValue; + var kind = data[nameof(ActorIdentifier.Kind)]?.Value() ?? ObjectKind.CardStand; + var dataId = data[nameof(ActorIdentifier.DataId)]?.Value() ?? 0; + return CreateNpc(kind, dataId, index); + } + case IdentifierType.Invalid: + default: + return ActorIdentifier.Invalid; + } + } + + /// + /// Use stored data to convert an ActorIdentifier to a string. + /// + public string ToString(ActorIdentifier id) + { + return id.Type switch + { + IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id + ? $"{id.PlayerName} ({Worlds[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), + IdentifierType.Npc => + id.Index == ushort.MaxValue + ? ToName(id.Kind, id.DataId) + : $"{ToName(id.Kind, id.DataId)} at {id.Index}", + _ => "Invalid", + }; + } + + + /// + /// 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. + /// + /// Invalid or a valid name. + public string ToName(ObjectKind kind, uint dataId) + => TryGetName(kind, dataId, out var ret) ? ret : "Invalid"; + + + /// + /// Convert a given ID for a certain ObjectKind to a name. + /// + public bool TryGetName(ObjectKind kind, uint dataId, [NotNullWhen(true)] out string? name) + { + name = null; + return kind switch + { + ObjectKind.MountType => Mounts.TryGetValue(dataId, out name), + ObjectKind.Companion => Companions.TryGetValue(dataId, out name), + ObjectKind.BattleNpc => BNpcs.TryGetValue(dataId, out name), + ObjectKind.EventNpc => ENpcs.TryGetValue(dataId, out name), + _ => false, + }; + } + + /// + /// Compute an ActorIdentifier from a GameObject. + /// + public unsafe ActorIdentifier FromObject(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor) + { + if (actor == null) + return ActorIdentifier.Invalid; + + var idx = actor->ObjectIndex; + if (idx is >= (ushort)SpecialActor.CutsceneStart and < (ushort)SpecialActor.CutsceneEnd) + { + var parentIdx = _toParentIdx(idx); + if (parentIdx >= 0) + return FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_objects.GetObjectAddress(parentIdx)); + } + else if (idx is >= (ushort)SpecialActor.CharacterScreen and <= (ushort)SpecialActor.Portrait) + { + return CreateSpecial((SpecialActor)idx); + } + + switch ((ObjectKind)actor->ObjectKind) + { + case ObjectKind.Player: + { + var name = new ByteString(actor->Name); + var homeWorld = ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->HomeWorld; + return CreatePlayer(name, homeWorld); + } + case ObjectKind.BattleNpc: + { + var ownerId = actor->OwnerID; + if (ownerId != 0xE0000000) + { + var owner = + (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)(_objects.SearchById(ownerId)?.Address ?? IntPtr.Zero); + if (owner == null) + return ActorIdentifier.Invalid; + + var name = new ByteString(owner->GameObject.Name); + var homeWorld = owner->HomeWorld; + return CreateOwned(name, homeWorld, ObjectKind.BattleNpc, + ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID); + } + + return CreateNpc(ObjectKind.BattleNpc, ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor)->NameID, actor->ObjectIndex); + } + case ObjectKind.EventNpc: return CreateNpc(ObjectKind.EventNpc, actor->DataID, actor->ObjectIndex); + case ObjectKind.MountType: + case ObjectKind.Companion: + { + if (actor->ObjectIndex % 2 == 0) + return ActorIdentifier.Invalid; + + var owner = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)_objects.GetObjectAddress(actor->ObjectIndex - 1); + if (owner == null) + return ActorIdentifier.Invalid; + + var dataId = GetCompanionId(actor, &owner->GameObject); + return CreateOwned(new ByteString(owner->GameObject.Name), owner->HomeWorld, (ObjectKind)actor->ObjectKind, dataId); + } + default: return ActorIdentifier.Invalid; + } + } + + /// + /// 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) + { + return (ObjectKind)actor->ObjectKind switch + { + ObjectKind.MountType => *(ushort*)((byte*)owner + 0x668), + ObjectKind.Companion => *(ushort*)((byte*)actor + 0x1AAC), + _ => actor->DataID, + }; + } + + public unsafe ActorIdentifier FromObject(GameObject? actor) + => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero)); + + public ActorIdentifier CreatePlayer(ByteString name, ushort homeWorld) + { + if (!VerifyWorld(homeWorld) || !VerifyPlayerName(name)) + return ActorIdentifier.Invalid; + + return new ActorIdentifier(IdentifierType.Player, ObjectKind.Player, homeWorld, 0, name); + } + + public ActorIdentifier CreateSpecial(SpecialActor actor) + { + if (!VerifySpecial(actor)) + return ActorIdentifier.Invalid; + + return new ActorIdentifier(IdentifierType.Special, ObjectKind.Player, (ushort)actor, 0, ByteString.Empty); + } + + public ActorIdentifier CreateNpc(ObjectKind kind, uint data, ushort index = ushort.MaxValue) + { + if (!VerifyIndex(index) || !VerifyNpcData(kind, data)) + return ActorIdentifier.Invalid; + + return new ActorIdentifier(IdentifierType.Npc, kind, index, data, ByteString.Empty); + } + + public ActorIdentifier CreateOwned(ByteString ownerName, ushort homeWorld, ObjectKind kind, uint dataId) + { + if (!VerifyWorld(homeWorld) || !VerifyPlayerName(ownerName) || !VerifyOwnedData(kind, dataId)) + return ActorIdentifier.Invalid; + + return new ActorIdentifier(IdentifierType.Owned, kind, homeWorld, dataId, ownerName); + } + + /// Checks SE naming rules. + private static bool VerifyPlayerName(ByteString name) + { + // Total no more than 20 characters + space. + if (name.Length is < 5 or > 21) + return false; + + var split = name.Split((byte)' '); + + // Forename and surname, no more spaces. + if (split.Count != 2) + return false; + + static bool CheckNamePart(ByteString 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. + if (part.Skip(1).Any(c => c != (byte)'\'' && c != (byte)'-' && c is < (byte)'a' or > (byte)'z')) + return false; + + var hyphens = part.Split((byte)'-'); + // Apostrophes can not be used in succession, after or before apostrophes. + return !hyphens.Any(p => p.Length == 0 || p[0] == (byte)'\'' || p.Last() == (byte)'\''); + } + + return CheckNamePart(split[0]) && CheckNamePart(split[1]); + } + + /// Checks if the world is a valid public world or ushort.MaxValue (any world). + private bool VerifyWorld(ushort worldId) + => Worlds.ContainsKey(worldId); + + /// Verify that the enum value is a specific actor and return the name if it is. + private static bool VerifySpecial(SpecialActor actor) + => actor is >= SpecialActor.CharacterScreen and <= SpecialActor.Portrait; + + /// Verify that the object index is a valid index for an NPC. + private static bool VerifyIndex(ushort index) + { + return index switch + { + < 200 => index % 2 == 0, + > (ushort)SpecialActor.Portrait => index < 426, + _ => false, + }; + } + + /// Verify that the object kind is a valid owned object, and the corresponding data Id. + private bool VerifyOwnedData(ObjectKind kind, uint dataId) + { + return kind switch + { + ObjectKind.MountType => Mounts.ContainsKey(dataId), + ObjectKind.Companion => Companions.ContainsKey(dataId), + ObjectKind.BattleNpc => BNpcs.ContainsKey(dataId), + _ => false, + }; + } + + private bool VerifyNpcData(ObjectKind kind, uint dataId) + => kind switch + { + ObjectKind.BattleNpc => BNpcs.ContainsKey(dataId), + ObjectKind.EventNpc => ENpcs.ContainsKey(dataId), + _ => false, + }; +} diff --git a/Penumbra.GameData/Actors/ActorManager.cs b/Penumbra.GameData/Actors/ActorManager.cs deleted file mode 100644 index f3a1936e..00000000 --- a/Penumbra.GameData/Actors/ActorManager.cs +++ /dev/null @@ -1,352 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using Dalamud.Data; -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Utility; -using Lumina.Excel.GeneratedSheets; -using Newtonsoft.Json.Linq; -using Penumbra.String; - -namespace Penumbra.GameData.Actors; - -public class ActorManager -{ - private readonly ObjectTable _objects; - private readonly ClientState _clientState; - - public readonly IReadOnlyDictionary< ushort, string > Worlds; - public readonly IReadOnlyDictionary< uint, string > Mounts; - public readonly IReadOnlyDictionary< uint, string > Companions; - public readonly IReadOnlyDictionary< uint, string > BNpcs; - public readonly IReadOnlyDictionary< uint, string > ENpcs; - - public IEnumerable< KeyValuePair< ushort, string > > AllWorlds - => Worlds.OrderBy( kvp => kvp.Key ).Prepend( new KeyValuePair< ushort, string >( ushort.MaxValue, "Any World" ) ); - - private readonly Func< ushort, short > _toParentIdx; - - public ActorManager( ObjectTable objects, ClientState state, DataManager gameData, Func< ushort, short > toParentIdx ) - { - _objects = objects; - _clientState = state; - Worlds = gameData.GetExcelSheet< World >()! - .Where( w => w.IsPublic && !w.Name.RawData.IsEmpty ) - .ToDictionary( w => ( ushort )w.RowId, w => w.Name.ToString() ); - - Mounts = gameData.GetExcelSheet< Mount >()! - .Where( m => m.Singular.RawData.Length > 0 && m.Order >= 0 ) - .ToDictionary( m => m.RowId, m => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( m.Singular.ToDalamudString().ToString() ) ); - Companions = gameData.GetExcelSheet< Companion >()! - .Where( c => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue ) - .ToDictionary( c => c.RowId, c => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( c.Singular.ToDalamudString().ToString() ) ); - - BNpcs = gameData.GetExcelSheet< BNpcName >()! - .Where( n => n.Singular.RawData.Length > 0 ) - .ToDictionary( n => n.RowId, n => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( n.Singular.ToDalamudString().ToString() ) ); - - ENpcs = gameData.GetExcelSheet< ENpcResident >()! - .Where( e => e.Singular.RawData.Length > 0 ) - .ToDictionary( e => e.RowId, e => CultureInfo.InvariantCulture.TextInfo.ToTitleCase( e.Singular.ToDalamudString().ToString() ) ); - - _toParentIdx = toParentIdx; - - ActorIdentifier.Manager = this; - } - - public ActorIdentifier FromJson( JObject data ) - { - var type = data[ nameof( ActorIdentifier.Type ) ]?.Value< IdentifierType >() ?? IdentifierType.Invalid; - switch( type ) - { - case IdentifierType.Player: - { - var name = ByteString.FromStringUnsafe( data[ nameof( ActorIdentifier.PlayerName ) ]?.Value< string >(), false ); - var homeWorld = data[ nameof( ActorIdentifier.HomeWorld ) ]?.Value< ushort >() ?? 0; - return CreatePlayer( name, homeWorld ); - } - case IdentifierType.Owned: - { - var name = ByteString.FromStringUnsafe( data[ nameof( ActorIdentifier.PlayerName ) ]?.Value< string >(), false ); - var homeWorld = data[ nameof( ActorIdentifier.HomeWorld ) ]?.Value< ushort >() ?? 0; - var kind = data[ nameof( ActorIdentifier.Kind ) ]?.Value< ObjectKind >() ?? ObjectKind.CardStand; - var dataId = data[ nameof( ActorIdentifier.DataId ) ]?.Value< uint >() ?? 0; - return CreateOwned( name, homeWorld, kind, dataId ); - } - case IdentifierType.Special: - { - var special = data[ nameof( ActorIdentifier.Special ) ]?.Value< SpecialActor >() ?? 0; - return CreateSpecial( special ); - } - case IdentifierType.Npc: - { - var index = data[ nameof( ActorIdentifier.Index ) ]?.Value< ushort >() ?? 0; - var kind = data[ nameof( ActorIdentifier.Kind ) ]?.Value< ObjectKind >() ?? ObjectKind.CardStand; - var dataId = data[ nameof( ActorIdentifier.DataId ) ]?.Value< uint >() ?? 0; - return CreateNpc( kind, index, dataId ); - } - case IdentifierType.Invalid: - default: - return ActorIdentifier.Invalid; - } - } - - public string ToString( ActorIdentifier id ) - { - return id.Type switch - { - IdentifierType.Player => id.HomeWorld != _clientState.LocalPlayer?.HomeWorld.Id - ? $"{id.PlayerName} ({Worlds[ 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 ), - IdentifierType.Npc => - id.Index == ushort.MaxValue - ? ToName( id.Kind, id.DataId ) - : $"{ToName( id.Kind, id.DataId )} at {id.Index}", - _ => "Invalid", - }; - } - - 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", - }; - - public string ToName( ObjectKind kind, uint dataId ) - => TryGetName( kind, dataId, out var ret ) ? ret : "Invalid"; - - public bool TryGetName( ObjectKind kind, uint dataId, [NotNullWhen( true )] out string? name ) - { - name = null; - return kind switch - { - ObjectKind.MountType => Mounts.TryGetValue( dataId, out name ), - ObjectKind.Companion => Companions.TryGetValue( dataId, out name ), - ObjectKind.BattleNpc => BNpcs.TryGetValue( dataId, out name ), - ObjectKind.EventNpc => ENpcs.TryGetValue( dataId, out name ), - _ => false, - }; - } - - public unsafe ActorIdentifier FromObject( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor ) - { - if( actor == null ) - { - return ActorIdentifier.Invalid; - } - - var idx = actor->ObjectIndex; - if( idx is >= ( ushort )SpecialActor.CutsceneStart and < ( ushort )SpecialActor.CutsceneEnd ) - { - var parentIdx = _toParentIdx( idx ); - if( parentIdx >= 0 ) - { - return FromObject( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )_objects.GetObjectAddress( parentIdx ) ); - } - } - else if( idx is >= ( ushort )SpecialActor.CharacterScreen and <= ( ushort )SpecialActor.Portrait ) - { - return CreateSpecial( ( SpecialActor )idx ); - } - - switch( ( ObjectKind )actor->ObjectKind ) - { - case ObjectKind.Player: - { - var name = new ByteString( actor->Name ); - var homeWorld = ( ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )actor )->HomeWorld; - return CreatePlayer( name, homeWorld ); - } - case ObjectKind.BattleNpc: - { - var ownerId = actor->OwnerID; - if( ownerId != 0xE0000000 ) - { - var owner = ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )( _objects.SearchById( ownerId )?.Address ?? IntPtr.Zero ); - if( owner == null ) - { - return ActorIdentifier.Invalid; - } - - var name = new ByteString( owner->GameObject.Name ); - var homeWorld = owner->HomeWorld; - return CreateOwned( name, homeWorld, ObjectKind.BattleNpc, ( ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )actor )->NameID ); - } - - return CreateNpc( ObjectKind.BattleNpc, actor->ObjectIndex, ( ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )actor )->NameID ); - } - case ObjectKind.EventNpc: return CreateNpc( ObjectKind.EventNpc, actor->ObjectIndex, actor->DataID ); - case ObjectKind.MountType: - case ObjectKind.Companion: - { - if( actor->ObjectIndex % 2 == 0 ) - { - return ActorIdentifier.Invalid; - } - - var owner = ( FFXIVClientStructs.FFXIV.Client.Game.Character.Character* )_objects.GetObjectAddress( actor->ObjectIndex - 1 ); - if( owner == null ) - { - return ActorIdentifier.Invalid; - } - - var dataId = GetCompanionId( actor, &owner->GameObject ); - return CreateOwned( new ByteString( owner->GameObject.Name ), owner->HomeWorld, ( ObjectKind )actor->ObjectKind, dataId ); - } - default: return ActorIdentifier.Invalid; - } - } - - private unsafe uint GetCompanionId( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* actor, FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner ) - { - return ( ObjectKind )actor->ObjectKind switch - { - ObjectKind.MountType => *( ushort* )( ( byte* )owner + 0x668 ), - ObjectKind.Companion => *( ushort* )( ( byte* )actor + 0x1AAC ), - _ => actor->DataID, - }; - } - - public unsafe ActorIdentifier FromObject( GameObject? actor ) - => FromObject( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )( actor?.Address ?? IntPtr.Zero ) ); - - - public ActorIdentifier CreatePlayer( ByteString name, ushort homeWorld ) - { - if( !VerifyWorld( homeWorld ) || !VerifyPlayerName( name ) ) - { - return ActorIdentifier.Invalid; - } - - return new ActorIdentifier( IdentifierType.Player, ObjectKind.Player, homeWorld, 0, name ); - } - - public ActorIdentifier CreateSpecial( SpecialActor actor ) - { - if( !VerifySpecial( actor ) ) - { - return ActorIdentifier.Invalid; - } - - return new ActorIdentifier( IdentifierType.Special, ObjectKind.Player, ( ushort )actor, 0, ByteString.Empty ); - } - - public ActorIdentifier CreateNpc( ObjectKind kind, ushort index = ushort.MaxValue, uint data = uint.MaxValue ) - { - if( !VerifyIndex( index ) || !VerifyNpcData( kind, data ) ) - { - return ActorIdentifier.Invalid; - } - - return new ActorIdentifier( IdentifierType.Npc, kind, index, data, ByteString.Empty ); - } - - public ActorIdentifier CreateOwned( ByteString ownerName, ushort homeWorld, ObjectKind kind, uint dataId ) - { - if( !VerifyWorld( homeWorld ) || !VerifyPlayerName( ownerName ) || !VerifyOwnedData( kind, dataId ) ) - { - return ActorIdentifier.Invalid; - } - - return new ActorIdentifier( IdentifierType.Owned, kind, homeWorld, dataId, ownerName ); - } - - - /// Checks SE naming rules. - private static bool VerifyPlayerName( ByteString name ) - { - // Total no more than 20 characters + space. - if( name.Length is < 5 or > 21 ) - { - return false; - } - - var split = name.Split( ( byte )' ' ); - - // Forename and surname, no more spaces. - if( split.Count != 2 ) - { - return false; - } - - static bool CheckNamePart( ByteString 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. - if( part.Skip( 1 ).Any( c => c != ( byte )'\'' && c != ( byte )'-' && c is < ( byte )'a' or > ( byte )'z' ) ) - { - return false; - } - - var hyphens = part.Split( ( byte )'-' ); - // Apostrophes can not be used in succession, after or before apostrophes. - return !hyphens.Any( p => p.Length == 0 || p[ 0 ] == ( byte )'\'' || p.Last() == ( byte )'\'' ); - } - - return CheckNamePart( split[ 0 ] ) && CheckNamePart( split[ 1 ] ); - } - - /// Checks if the world is a valid public world or ushort.MaxValue (any world). - private bool VerifyWorld( ushort worldId ) - => Worlds.ContainsKey( worldId ); - - /// Verify that the enum value is a specific actor and return the name if it is. - private static bool VerifySpecial( SpecialActor actor ) - => actor is >= SpecialActor.CharacterScreen and <= SpecialActor.Portrait; - - /// Verify that the object index is a valid index for an NPC. - private static bool VerifyIndex( ushort index ) - { - return index switch - { - < 200 => index % 2 == 0, - > ( ushort )SpecialActor.Portrait => index < 426, - _ => false, - }; - } - - /// Verify that the object kind is a valid owned object, and the corresponding data Id. - private bool VerifyOwnedData( ObjectKind kind, uint dataId ) - { - return kind switch - { - ObjectKind.MountType => Mounts.ContainsKey( dataId ), - ObjectKind.Companion => Companions.ContainsKey( dataId ), - ObjectKind.BattleNpc => BNpcs.ContainsKey( dataId ), - _ => false, - }; - } - - private bool VerifyNpcData( ObjectKind kind, uint dataId ) - => kind switch - { - ObjectKind.BattleNpc => BNpcs.ContainsKey( dataId ), - ObjectKind.EventNpc => ENpcs.ContainsKey( dataId ), - _ => false, - }; -} \ No newline at end of file diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index 7eace70e..639d13d1 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -57,11 +57,10 @@ public interface IObjectIdentifier : IDisposable /// The secondary model ID for weapons, WeaponType.Zero for equipment and accessories. /// The variant ID of the model. /// The equipment slot the piece of equipment uses. - /// - public IReadOnlyList? Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot); + public IReadOnlyList Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot); /// - public IReadOnlyList? Identify(SetId setId, ushort variant, EquipSlot slot) + public IReadOnlyList Identify(SetId setId, ushort variant, EquipSlot slot) => Identify(setId, 0, variant, slot); } diff --git a/Penumbra.GameData/ObjectIdentification.cs b/Penumbra.GameData/ObjectIdentification.cs index 20642156..6d1e8595 100644 --- a/Penumbra.GameData/ObjectIdentification.cs +++ b/Penumbra.GameData/ObjectIdentification.cs @@ -16,7 +16,7 @@ namespace Penumbra.GameData; internal class ObjectIdentification : IObjectIdentifier { - public IGamePathParser GamePathParser { get; } = new GamePathParser(); + public IGamePathParser GamePathParser { get; } = new GamePathParser(); public void Identify(IDictionary set, string path) { @@ -38,7 +38,7 @@ internal class ObjectIdentification : IObjectIdentifier return ret; } - public IReadOnlyList? Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) + public IReadOnlyList Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) { switch (slot) { @@ -55,7 +55,7 @@ internal class ObjectIdentification : IObjectIdentifier var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_equipment, ((ulong)setId << 32) | ((ulong)slot.ToSlot() << 16) | variant, 0xFFFFFFFFFFFF); - return begin >= 0 ? _equipment[begin].Item2 : null; + return begin >= 0 ? _equipment[begin].Item2 : Array.Empty(); } } } @@ -68,12 +68,8 @@ internal class ObjectIdentification : IObjectIdentifier private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _weapons; private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _equipment; - private readonly IReadOnlyDictionary> _actions; - - private readonly string _weaponsTag; - private readonly string _equipmentTag; - private readonly string _actionsTag; - private bool _disposed = false; + private readonly IReadOnlyDictionary> _actions; + private bool _disposed = false; public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) { @@ -81,39 +77,9 @@ internal class ObjectIdentification : IObjectIdentifier _dataManager = dataManager; _language = language; - _weaponsTag = $"Penumbra.Identification.Weapons.{_language}.V{Version}"; - _equipmentTag = $"Penumbra.Identification.Equipment.{_language}.V{Version}"; - _actionsTag = $"Penumbra.Identification.Actions.{_language}.V{Version}"; - - try - { - _weapons = pluginInterface.GetOrCreateData(_weaponsTag, CreateWeaponList); - } - catch (Exception ex) - { - PluginLog.Error($"Error creating shared identification data for weapons:\n{ex}"); - _weapons = CreateWeaponList(); - } - - try - { - _equipment = pluginInterface.GetOrCreateData(_equipmentTag, CreateEquipmentList); - } - catch (Exception ex) - { - PluginLog.Error($"Error creating shared identification data for equipment:\n{ex}"); - _equipment = CreateEquipmentList(); - } - - try - { - _actions = pluginInterface.GetOrCreateData(_actionsTag, CreateActionList); - } - catch (Exception ex) - { - PluginLog.Error($"Error creating shared identification data for actions:\n{ex}"); - _actions = CreateActionList(); - } + _weapons = TryCatchData("Weapons", CreateWeaponList); + _equipment = TryCatchData("Equipment", CreateEquipmentList); + _actions = TryCatchData("Actions", CreateActionList); } public void Dispose() @@ -122,15 +88,31 @@ internal class ObjectIdentification : IObjectIdentifier return; GC.SuppressFinalize(this); - _pluginInterface.RelinquishData(_weaponsTag); - _pluginInterface.RelinquishData(_equipmentTag); - _pluginInterface.RelinquishData(_actionsTag); + _pluginInterface.RelinquishData(GetVersionedTag("Weapons")); + _pluginInterface.RelinquishData(GetVersionedTag("Equipment")); + _pluginInterface.RelinquishData(GetVersionedTag("Actions")); _disposed = true; } ~ObjectIdentification() => Dispose(); + private string GetVersionedTag(string tag) + => $"Penumbra.Identification.{tag}.{_language}.V{Version}"; + + private T TryCatchData(string tag, Func func) where T : class + { + try + { + return _pluginInterface.GetOrCreateData(GetVersionedTag(tag), func); + } + catch (Exception ex) + { + PluginLog.Error($"Error creating shared identification data for {tag}:\n{ex}"); + return func(); + } + } + private static bool Add(IDictionary> dict, ulong key, Item item) { if (dict.TryGetValue(key, out var list)) @@ -181,7 +163,7 @@ internal class ObjectIdentification : IObjectIdentifier var storage = new SortedList>(); foreach (var item in items) { - switch (((EquipSlot)item.EquipSlotCategory.Row).ToSlot()) + switch ((EquipSlot)item.EquipSlotCategory.Row) { // Accessories case EquipSlot.RFinger: diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 0ede3d33..3633adbd 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -104,7 +104,7 @@ public class Penumbra : IDalamudPlugin ModFileSystem = ModFileSystem.Load(); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); - Actors = new ActorManager( Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, u => ( short )PathResolver.CutsceneActor( u ) ); + Actors = new ActorManager( Dalamud.PluginInterface, Dalamud.Objects, Dalamud.ClientState, Dalamud.GameData, u => ( short )PathResolver.CutsceneActor( u ) ); Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) { @@ -290,7 +290,8 @@ public class Penumbra : IDalamudPlugin public void Dispose() { - Dalamud.PluginInterface.RelinquishData( "test1" ); + Actors?.Dispose(); + Identifier?.Dispose(); Framework?.Dispose(); ShutdownWebServer(); DisposeInterface();