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, }; }