using System; using System.Collections.Concurrent; using Dalamud; using Dalamud.Data; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Plugin; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Actors; using Action = Lumina.Excel.GeneratedSheets.Action; using ObjectType = Penumbra.GameData.Enums.ObjectType; namespace Penumbra.GameData.Data; internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier { public const int IdentificationVersion = 3; public IGamePathParser GamePathParser { get; } = new GamePathParser(); public readonly IReadOnlyList> BnpcNames; public readonly IReadOnlyList> ModelCharaToObjects; public readonly IReadOnlyDictionary> Actions; private readonly ActorManager.ActorManagerData _actorData; private readonly EquipmentIdentificationList _equipment; private readonly WeaponIdentificationList _weapons; private readonly ModelIdentificationList _modelIdentifierToModelChara; public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ItemData itemData, ClientLanguage language) : base(pluginInterface, language, IdentificationVersion) { _actorData = new ActorManager.ActorManagerData(pluginInterface, dataManager, language); _equipment = new EquipmentIdentificationList(pluginInterface, language, itemData); _weapons = new WeaponIdentificationList(pluginInterface, language, itemData); Actions = TryCatchData("Actions", () => CreateActionList(dataManager)); _modelIdentifierToModelChara = new ModelIdentificationList(pluginInterface, language, dataManager); BnpcNames = TryCatchData("BNpcNames", NpcNames.CreateNames); ModelCharaToObjects = TryCatchData("ModelObjects", () => CreateModelObjects(_actorData, dataManager, language)); } public void Identify(IDictionary set, string path) { if (path.EndsWith(".pap", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".tmb", StringComparison.OrdinalIgnoreCase)) if (IdentifyVfx(set, path)) return; var info = GamePathParser.GetFileInfo(path); IdentifyParsed(set, info); } public Dictionary Identify(string path) { Dictionary ret = new(); Identify(ret, path); return ret; } public IEnumerable Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot) => slot switch { EquipSlot.MainHand => _weapons.Between(setId, weaponType, (byte)variant), EquipSlot.OffHand => _weapons.Between(setId, weaponType, (byte)variant), _ => _equipment.Between(setId, slot, (byte)variant), }; public IReadOnlyList GetBnpcNames(uint bNpcId) => bNpcId >= BnpcNames.Count ? Array.Empty() : BnpcNames[(int)bNpcId]; public IReadOnlyList<(string Name, ObjectKind Kind, uint Id)> ModelCharaNames(uint modelId) => modelId >= ModelCharaToObjects.Count ? Array.Empty<(string Name, ObjectKind Kind, uint Id)>() : ModelCharaToObjects[(int)modelId]; public int NumModelChara => ModelCharaToObjects.Count; protected override void DisposeInternal() { _actorData.Dispose(); _weapons.Dispose(PluginInterface, Language); _equipment.Dispose(PluginInterface, Language); DisposeTag("Actions"); DisposeTag("Models"); _modelIdentifierToModelChara.Dispose(PluginInterface, Language); DisposeTag("BNpcNames"); DisposeTag("ModelObjects"); } private IReadOnlyDictionary> CreateActionList(DataManager gameData) { var sheet = gameData.GetExcelSheet(Language)!; var storage = new ConcurrentDictionary>(); void AddAction(string? key, Action action) { if (key.IsNullOrEmpty()) return; key = key.ToLowerInvariant(); if (storage.TryGetValue(key, out var actions)) actions.Add(action); else storage[key] = new ConcurrentBag { action }; } var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, }; Parallel.ForEach(sheet.Where(a => !a.Name.RawData.IsEmpty), options, action => { var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToDalamudString().ToString(); var endKey = action.AnimationEnd?.Value?.Key.ToDalamudString().ToString(); var hitKey = action.ActionTimelineHit?.Value?.Key.ToDalamudString().ToString(); AddAction(startKey, action); AddAction(endKey, action); AddAction(hitKey, action); }); return storage.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray()); } private void FindEquipment(IDictionary set, GameObjectInfo info) { var items = _equipment.Between(info.PrimaryId, info.EquipSlot, info.Variant); foreach (var item in items) set[item.Name] = item; } private void FindWeapon(IDictionary set, GameObjectInfo info) { var items = _weapons.Between(info.PrimaryId, info.SecondaryId, info.Variant); foreach (var item in items) set[item.Name] = item; } private void FindModel(IDictionary set, GameObjectInfo info) { var type = info.ObjectType.ToModelType(); if (type is 0 or CharacterBase.ModelType.Weapon) return; var models = _modelIdentifierToModelChara.Between(type, info.PrimaryId, (byte)info.SecondaryId, info.Variant); foreach (var model in models.Where(m => m.RowId != 0 && m.RowId < ModelCharaToObjects.Count)) { var objectList = ModelCharaToObjects[(int)model.RowId]; foreach (var (name, kind, _) in objectList) set[$"{name} ({kind.ToName()})"] = model; } } private static void AddCounterString(IDictionary set, string data) { if (set.TryGetValue(data, out var obj) && obj is int counter) set[data] = counter + 1; else set[data] = 1; } private void IdentifyParsed(IDictionary set, GameObjectInfo info) { switch (info.FileType) { case FileType.Sound: AddCounterString(set, FileType.Sound.ToString()); return; case FileType.Animation: case FileType.Pap: AddCounterString(set, FileType.Animation.ToString()); return; case FileType.Shader: AddCounterString(set, FileType.Shader.ToString()); return; } switch (info.ObjectType) { case ObjectType.LoadingScreen: case ObjectType.Map: case ObjectType.Interface: case ObjectType.Vfx: case ObjectType.World: case ObjectType.Housing: case ObjectType.Font: AddCounterString(set, info.ObjectType.ToString()); break; case ObjectType.DemiHuman: FindModel(set, info); break; case ObjectType.Monster: FindModel(set, info); break; case ObjectType.Icon: set[$"Icon: {info.IconId}"] = null; break; case ObjectType.Accessory: case ObjectType.Equipment: FindEquipment(set, info); break; case ObjectType.Weapon: FindWeapon(set, info); break; case ObjectType.Character: var (gender, race) = info.GenderRace.Split(); var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player "; switch (info.CustomizationType) { case CustomizationType.Skin: set[$"Customization: {raceString}{genderString}Skin Textures"] = null; break; case CustomizationType.DecalFace: set[$"Customization: Face Decal {info.PrimaryId}"] = null; break; case CustomizationType.Iris when race == ModelRace.Unknown: set[$"Customization: All Eyes (Catchlight)"] = null; break; case CustomizationType.DecalEquip: set[$"Equipment Decal {info.PrimaryId}"] = null; break; default: { var customizationString = race == ModelRace.Unknown || info.BodySlot == BodySlot.Unknown || info.CustomizationType == CustomizationType.Unknown ? "Customization: Unknown" : $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}"; set[customizationString] = null; break; } } break; } } private bool IdentifyVfx(IDictionary set, string path) { var key = GamePathParser.VfxToKey(path); if (key.Length == 0 || !Actions.TryGetValue(key, out var actions) || actions.Count == 0) return false; foreach (var action in actions) set[$"Action: {action.Name}"] = action; return true; } private IReadOnlyList> CreateModelObjects(ActorManager.ActorManagerData actors, DataManager gameData, ClientLanguage language) { var modelSheet = gameData.GetExcelSheet(language)!; var ret = new List>((int)modelSheet.RowCount); for (var i = -1; i < modelSheet.Last().RowId; ++i) ret.Add(new ConcurrentBag<(string Name, ObjectKind Kind, uint Id)>()); void AddChara(int modelChara, ObjectKind kind, uint dataId, uint displayId) { if (modelChara >= ret.Count) return; if (actors.TryGetName(kind, dataId, out var name)) ret[modelChara].Add((name, kind, displayId)); } var oTask = Task.Run(() => { foreach (var ornament in gameData.GetExcelSheet(language)!) AddChara(ornament.Model, ObjectKind.Ornament, ornament.RowId, ornament.RowId); }); var mTask = Task.Run(() => { foreach (var mount in gameData.GetExcelSheet(language)!) AddChara((int)mount.ModelChara.Row, ObjectKind.MountType, mount.RowId, mount.RowId); }); var cTask = Task.Run(() => { foreach (var companion in gameData.GetExcelSheet(language)!) AddChara((int)companion.Model.Row, ObjectKind.Companion, companion.RowId, companion.RowId); }); var eTask = Task.Run(() => { foreach (var eNpc in gameData.GetExcelSheet(language)!) AddChara((int)eNpc.ModelChara.Row, ObjectKind.EventNpc, eNpc.RowId, eNpc.RowId); }); var options = new ParallelOptions() { MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2), }; Parallel.ForEach(gameData.GetExcelSheet(language)!.Where(b => b.RowId < BnpcNames.Count), options, bNpc => { foreach (var name in BnpcNames[(int)bNpc.RowId]) AddChara((int)bNpc.ModelChara.Row, ObjectKind.BattleNpc, name, bNpc.RowId); }); Task.WaitAll(oTask, mTask, cTask, eTask); return ret.Select(s => !s.IsEmpty ? s.ToArray() : Array.Empty<(string Name, ObjectKind Kind, uint Id)>()).ToArray(); } public static unsafe ulong KeyFromCharacterBase(CharacterBase* drawObject) { var type = (*(delegate* unmanaged**)drawObject)[Offsets.DrawObjectGetModelTypeVfunc](drawObject); var unk = (ulong)*((byte*)drawObject + Offsets.DrawObjectModelUnk1) << 8; return type switch { 1 => type | unk, 2 => type | unk | ((ulong)*(ushort*)((byte*)drawObject + Offsets.DrawObjectModelUnk3) << 16), 3 => type | unk | ((ulong)*(ushort*)((byte*)drawObject + Offsets.DrawObjectModelUnk2) << 16) | ((ulong)**(ushort**)((byte*)drawObject + Offsets.DrawObjectModelUnk4) << 32) | ((ulong)**(ushort**)((byte*)drawObject + Offsets.DrawObjectModelUnk3) << 40), _ => 0u, }; } }