diff --git a/MareSynchronos/FileCache/TransientResourceManager.cs b/MareSynchronos/FileCache/TransientResourceManager.cs index 00586a2..d71eab6 100644 --- a/MareSynchronos/FileCache/TransientResourceManager.cs +++ b/MareSynchronos/FileCache/TransientResourceManager.cs @@ -279,4 +279,14 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase Mediator.Publish(new TransientResourceChangedMessage(gameObject)); } } + + internal void RemoveTransientResource(ObjectKind objectKind, string path) + { + if (SemiTransientResources.TryGetValue(objectKind, out var resources)) + { + resources.RemoveWhere(f => string.Equals(path, f, StringComparison.OrdinalIgnoreCase)); + _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = resources; + _configurationService.Save(); + } + } } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs b/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs index 61c4a0a..4e81655 100644 --- a/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs +++ b/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs @@ -40,6 +40,7 @@ public abstract class ConfigurationServiceBase : IDisposable where T : IMareC { _periodicCheckCts.Cancel(); _periodicCheckCts.Dispose(); + if (_configIsDirty) SaveDirtyConfig(); } protected T LoadConfig() @@ -94,10 +95,12 @@ public abstract class ConfigurationServiceBase : IDisposable where T : IMareC // ignore if file cannot be backupped once } - File.WriteAllText(ConfigurationPath, JsonSerializer.Serialize(Current, new JsonSerializerOptions() + var temp = ConfigurationPath + ".tmp"; + File.WriteAllText(temp, JsonSerializer.Serialize(Current, new JsonSerializerOptions() { WriteIndented = true })); + File.Move(temp, ConfigurationPath, true); _configLastWriteTime = new FileInfo(ConfigurationPath).LastWriteTimeUtc; } diff --git a/MareSynchronos/MareConfiguration/Configurations/TriangleCalculationConfig.cs b/MareSynchronos/MareConfiguration/Configurations/XivDataStorageConfig.cs similarity index 58% rename from MareSynchronos/MareConfiguration/Configurations/TriangleCalculationConfig.cs rename to MareSynchronos/MareConfiguration/Configurations/XivDataStorageConfig.cs index 6377e06..f9225e1 100644 --- a/MareSynchronos/MareConfiguration/Configurations/TriangleCalculationConfig.cs +++ b/MareSynchronos/MareConfiguration/Configurations/XivDataStorageConfig.cs @@ -2,8 +2,9 @@ namespace MareSynchronos.MareConfiguration.Configurations; -public class TriangleCalculationConfig : IMareConfiguration +public class XivDataStorageConfig : IMareConfiguration { public ConcurrentDictionary TriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase); + public ConcurrentDictionary>> BoneDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase); public int Version { get; set; } = 0; } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/TriangleCalculationConfigService.cs b/MareSynchronos/MareConfiguration/TriangleCalculationConfigService.cs deleted file mode 100644 index d653b28..0000000 --- a/MareSynchronos/MareConfiguration/TriangleCalculationConfigService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using MareSynchronos.MareConfiguration.Configurations; - -namespace MareSynchronos.MareConfiguration; - -public class TriangleCalculationConfigService : ConfigurationServiceBase -{ - public const string ConfigName = "trianglecache.json"; - - public TriangleCalculationConfigService(string configDir) : base(configDir) { } - - protected override string ConfigurationName => ConfigName; -} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/XivDataStorageService.cs b/MareSynchronos/MareConfiguration/XivDataStorageService.cs new file mode 100644 index 0000000..96e08bf --- /dev/null +++ b/MareSynchronos/MareConfiguration/XivDataStorageService.cs @@ -0,0 +1,12 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public class XivDataStorageService : ConfigurationServiceBase +{ + public const string ConfigName = "xivdatastorage.json"; + + public XivDataStorageService(string configDir) : base(configDir) { } + + protected override string ConfigurationName => ConfigName; +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs index 127b245..5c2019e 100644 --- a/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs +++ b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs @@ -20,13 +20,13 @@ public class PairHandlerFactory private readonly IpcManager _ipcManager; private readonly ILoggerFactory _loggerFactory; private readonly MareMediator _mareMediator; - private readonly ModelAnalyzer _modelAnalyzer; + private readonly XivDataAnalyzer _xivDataAnalyzer; private readonly PluginWarningNotificationService _pluginWarningNotificationManager; public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager, FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService, PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime, - FileCacheManager fileCacheManager, MareMediator mareMediator, ModelAnalyzer modelAnalyzer) + FileCacheManager fileCacheManager, MareMediator mareMediator, XivDataAnalyzer modelAnalyzer) { _loggerFactory = loggerFactory; _gameObjectHandlerFactory = gameObjectHandlerFactory; @@ -37,13 +37,13 @@ public class PairHandlerFactory _hostApplicationLifetime = hostApplicationLifetime; _fileCacheManager = fileCacheManager; _mareMediator = mareMediator; - _modelAnalyzer = modelAnalyzer; + _xivDataAnalyzer = modelAnalyzer; } public PairHandler Create(OnlineUserIdentDto onlineUserIdentDto) { return new PairHandler(_loggerFactory.CreateLogger(), onlineUserIdentDto, _gameObjectHandlerFactory, _ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime, - _fileCacheManager, _mareMediator, _modelAnalyzer); + _fileCacheManager, _mareMediator, _xivDataAnalyzer); } } \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs index 201047c..4f26b7e 100644 --- a/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs +++ b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs @@ -5,6 +5,7 @@ using MareSynchronos.Interop.Ipc; using MareSynchronos.PlayerData.Data; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; using Microsoft.Extensions.Logging; using CharacterData = MareSynchronos.PlayerData.Data.CharacterData; @@ -18,11 +19,13 @@ public class PlayerDataFactory private readonly IpcManager _ipcManager; private readonly ILogger _logger; private readonly PerformanceCollectorService _performanceCollector; + private readonly XivDataAnalyzer _modelAnalyzer; + private readonly MareMediator _mareMediator; private readonly TransientResourceManager _transientResourceManager; public PlayerDataFactory(ILogger logger, DalamudUtilService dalamudUtil, IpcManager ipcManager, TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory, - PerformanceCollectorService performanceCollector) + PerformanceCollectorService performanceCollector, XivDataAnalyzer modelAnalyzer, MareMediator mareMediator) { _logger = logger; _dalamudUtil = dalamudUtil; @@ -30,6 +33,8 @@ public class PlayerDataFactory _transientResourceManager = transientResourceManager; _fileCacheManager = fileReplacementFactory; _performanceCollector = performanceCollector; + _modelAnalyzer = modelAnalyzer; + _mareMediator = mareMediator; _logger.LogTrace("Creating {this}", nameof(PlayerDataFactory)); } @@ -229,11 +234,69 @@ public class PlayerDataFactory } } + if (objectKind == ObjectKind.Player) + { + await VerifyPlayerAnimationBones(previousData, objectKind, charaPointer).ConfigureAwait(false); + } + _logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(DateTime.UtcNow.Ticks - start.Ticks).TotalMilliseconds); return previousData; } + private async Task VerifyPlayerAnimationBones(CharacterData previousData, ObjectKind objectKind, nint charaPointer) + { + var boneIndices = _modelAnalyzer.GetSkeletonBoneIndices(charaPointer); + if (boneIndices != null) + { + _logger.LogDebug("Found {idx} bone indices on player: {bones}", boneIndices.Count, string.Join(',', boneIndices)); + + int noValidationFailed = 0; + foreach (var file in previousData.FileReplacements[objectKind].Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList()) + { + var animationIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetBoneIndicesFromPap(file.Hash)).ConfigureAwait(false); + bool validationFailed = false; + if (animationIndices != null) + { + _logger.LogDebug("Verifying bone indices for {path}, found {x} animations", file.ResolvedPath, animationIndices.Count); + for (int i = 0; i < animationIndices.Count; i++) + { + _logger.LogTrace("Verifying animation set {i}, found {idx} bone indeces", i, animationIndices[i].Count); + if (animationIndices[i].Count > boneIndices.Count) + { + _logger.LogWarning("Found more bone indeces on the animation {path} ({idx}) than on player skeleton ({idx2})", file.ResolvedPath, animationIndices[i].Count, boneIndices.Count); + validationFailed = true; + } + else if (animationIndices[i].Exists(idx => !boneIndices.Contains(idx))) + { + _logger.LogWarning("Found bone indices referred on animation {path} that are not on the player skeleton", file.ResolvedPath); + validationFailed = true; + } + } + } + + if (validationFailed) + { + noValidationFailed++; + _logger.LogDebug("Removing {file} from sent file replacements and transient data", file.ResolvedPath); + previousData.FileReplacements[objectKind].Remove(file); + foreach (var gamePath in file.GamePaths) + { + _transientResourceManager.RemoveTransientResource(objectKind, gamePath); + } + } + + } + if (noValidationFailed > 0) + { + _mareMediator.Publish(new NotificationMessage("Invalid Skeleton Setup", + $"Your client is attempting to send {noValidationFailed} animation files with invalid bone data. Those animation files have been removed from your sent data. " + + $"Verify that you are using the correct skeleton for those animation files (Check /xllog for more information).", + Dalamud.Interface.Internal.Notifications.NotificationType.Warning, TimeSpan.FromSeconds(10))); + } + } + } + private async Task> GetFileReplacementsFromPaths(HashSet forwardResolve, HashSet reverseResolve) { var forwardPaths = forwardResolve.ToArray(); diff --git a/MareSynchronos/PlayerData/Handlers/PairHandler.cs b/MareSynchronos/PlayerData/Handlers/PairHandler.cs index cb99b60..ecdb5d1 100644 --- a/MareSynchronos/PlayerData/Handlers/PairHandler.cs +++ b/MareSynchronos/PlayerData/Handlers/PairHandler.cs @@ -24,7 +24,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase private readonly DalamudUtilService _dalamudUtil; private readonly FileDownloadManager _downloadManager; private readonly FileCacheManager _fileDbManager; - private readonly ModelAnalyzer _modelAnalyzer; + private readonly XivDataAnalyzer _xivDataAnalyzer; private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; private readonly IpcManager _ipcManager; private readonly IHostApplicationLifetime _lifetime; @@ -49,7 +49,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase PluginWarningNotificationService pluginWarningNotificationManager, DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime, FileCacheManager fileDbManager, MareMediator mediator, - ModelAnalyzer modelAnalyzer) : base(logger, mediator) + XivDataAnalyzer modelAnalyzer) : base(logger, mediator) { OnlineUser = onlineUser; _gameObjectHandlerFactory = gameObjectHandlerFactory; @@ -59,7 +59,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase _dalamudUtil = dalamudUtil; _lifetime = lifetime; _fileDbManager = fileDbManager; - _modelAnalyzer = modelAnalyzer; + _xivDataAnalyzer = modelAnalyzer; _penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, OnlineUser.User.UID).ConfigureAwait(false).GetAwaiter().GetResult(); Mediator.Subscribe(this, (_) => FrameworkUpdate()); @@ -454,7 +454,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase foreach (var key in moddedPaths.Keys.Where(k => !string.IsNullOrEmpty(k.Hash))) { if (LastAppliedDataTris == -1) LastAppliedDataTris = 0; - LastAppliedDataTris += await _modelAnalyzer.GetTrianglesByHash(key.Hash!).ConfigureAwait(false); + LastAppliedDataTris += await _xivDataAnalyzer.GetTrianglesByHash(key.Hash!).ConfigureAwait(false); } } diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 25bcb92..5954a70 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -68,8 +68,8 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); - collection.AddSingleton(s => new(s.GetRequiredService>(), s.GetRequiredService(), - s.GetRequiredService(), gameData)); + collection.AddSingleton(s => new(s.GetRequiredService>(), s.GetRequiredService(), + s.GetRequiredService(), gameData)); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); @@ -113,7 +113,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ServerTagConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new TransientConfigService(pluginInterface.ConfigDirectory.FullName)); - collection.AddSingleton((s) => new TriangleCalculationConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton((s) => new XivDataStorageService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ConfigurationMigrator(s.GetRequiredService>(), pluginInterface)); collection.AddSingleton(); diff --git a/MareSynchronos/Services/CharacterAnalyzer.cs b/MareSynchronos/Services/CharacterAnalyzer.cs index 2f609fa..74aa791 100644 --- a/MareSynchronos/Services/CharacterAnalyzer.cs +++ b/MareSynchronos/Services/CharacterAnalyzer.cs @@ -12,12 +12,12 @@ namespace MareSynchronos.Services; public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable { private readonly FileCacheManager _fileCacheManager; - private readonly ModelAnalyzer _modelAnalyzer; + private readonly XivDataAnalyzer _xivDataAnalyzer; private CancellationTokenSource? _analysisCts; private CancellationTokenSource _baseAnalysisCts = new(); private string _lastDataHash = string.Empty; - public CharacterAnalyzer(ILogger logger, MareMediator mediator, FileCacheManager fileCacheManager, ModelAnalyzer modelAnalyzer) + public CharacterAnalyzer(ILogger logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer) : base(logger, mediator) { Mediator.Subscribe(this, (msg) => @@ -27,7 +27,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable _ = BaseAnalysis(msg.CharacterData, token); }); _fileCacheManager = fileCacheManager; - _modelAnalyzer = modelAnalyzer; + _xivDataAnalyzer = modelAnalyzer; } public int CurrentFile { get; internal set; } @@ -121,7 +121,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable Logger.LogWarning(ex, "Could not identify extension for {path}", filePath); } - var tris = await _modelAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false); + var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false); foreach (var entry in fileCacheEntries) { diff --git a/MareSynchronos/Services/ModelAnalyzer.cs b/MareSynchronos/Services/ModelAnalyzer.cs deleted file mode 100644 index 20dbcc4..0000000 --- a/MareSynchronos/Services/ModelAnalyzer.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Dalamud.Plugin.Services; -using Lumina; -using Lumina.Data.Files; -using MareSynchronos.FileCache; -using MareSynchronos.MareConfiguration; -using Microsoft.Extensions.Logging; - -namespace MareSynchronos.Services; - -public sealed class ModelAnalyzer -{ - private readonly ILogger _logger; - private readonly FileCacheManager _fileCacheManager; - private readonly TriangleCalculationConfigService _configService; - private readonly GameData _luminaGameData; - - public ModelAnalyzer(ILogger logger, FileCacheManager fileCacheManager, - TriangleCalculationConfigService configService, IDataManager gameData) - { - _logger = logger; - _fileCacheManager = fileCacheManager; - _configService = configService; - _luminaGameData = new GameData(gameData.GameData.DataPath.FullName); - } - - public Task GetTrianglesFromGamePath(string gamePath) - { - if (_configService.Current.TriangleDictionary.TryGetValue(gamePath, out var cachedTris)) - return Task.FromResult(cachedTris); - - _logger.LogInformation("Detected Model File {path}, calculating Tris", gamePath); - var file = _luminaGameData.GetFile(gamePath); - if (file == null) - return Task.FromResult((long)0); - - if (file.FileHeader.LodCount <= 0) - return Task.FromResult((long)0); - var meshIdx = file.Lods[0].MeshIndex; - var meshCnt = file.Lods[0].MeshCount; - var tris = file.Meshes.Skip(meshIdx).Take(meshCnt).Sum(p => p.IndexCount) / 3; - - _logger.LogInformation("{filePath} => {tris} triangles", gamePath, tris); - _configService.Current.TriangleDictionary[gamePath] = tris; - _configService.Save(); - return Task.FromResult(tris); - } - - public Task GetTrianglesByHash(string hash) - { - if (_configService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris)) - return Task.FromResult(cachedTris); - - var path = _fileCacheManager.GetFileCacheByHash(hash); - if (path == null || !path.ResolvedFilepath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)) - return Task.FromResult((long)0); - - var filePath = path.ResolvedFilepath; - - _logger.LogInformation("Detected Model File {path}, calculating Tris", filePath); - var file = _luminaGameData.GetFileFromDisk(filePath); - if (file.FileHeader.LodCount <= 0) - return Task.FromResult((long)0); - var meshIdx = file.Lods[0].MeshIndex; - var meshCnt = file.Lods[0].MeshCount; - var tris = file.Meshes.Skip(meshIdx).Take(meshCnt).Sum(p => p.IndexCount) / 3; - - _logger.LogInformation("{filePath} => {tris} triangles", filePath, tris); - _configService.Current.TriangleDictionary[hash] = tris; - _configService.Save(); - return Task.FromResult(tris); - } -} diff --git a/MareSynchronos/Services/XivDataAnalyzer.cs b/MareSynchronos/Services/XivDataAnalyzer.cs new file mode 100644 index 0000000..6d845a1 --- /dev/null +++ b/MareSynchronos/Services/XivDataAnalyzer.cs @@ -0,0 +1,191 @@ +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.Havok; +using Lumina; +using Lumina.Data.Files; +using MareSynchronos.FileCache; +using MareSynchronos.MareConfiguration; +using Microsoft.Extensions.Logging; +using System.Runtime.InteropServices; + +namespace MareSynchronos.Services; + +public sealed class XivDataAnalyzer +{ + private readonly ILogger _logger; + private readonly FileCacheManager _fileCacheManager; + private readonly XivDataStorageService _configService; + private readonly GameData _luminaGameData; + + public XivDataAnalyzer(ILogger logger, FileCacheManager fileCacheManager, + XivDataStorageService configService, IDataManager gameData) + { + _logger = logger; + _fileCacheManager = fileCacheManager; + _configService = configService; + _luminaGameData = new GameData(gameData.GameData.DataPath.FullName); + } + + public unsafe List? GetSkeletonBoneIndices(nint charaPtr) + { + if (charaPtr == nint.Zero) return null; + var chara = (CharacterBase*)(((Character*)charaPtr)->GameObject.DrawObject); + var resHandles = chara->Skeleton->SkeletonResourceHandles; + int i = -1; + uint maxBones = 0; + List outputIndices = new(); + while (*(resHandles + ++i) != null) + { + var handle = *(resHandles + i); + var curBones = handle->BoneCount; + List indices = new(); + for (ushort boneIdx = 0; boneIdx < curBones; boneIdx++) + { + var boneName = handle->HavokSkeleton->Bones[boneIdx].Name.String; + if (boneName == null) continue; + indices.Add(boneIdx); + } + if (curBones > maxBones) + { + maxBones = curBones; + outputIndices = indices; + } + } + + return outputIndices; + } + + public unsafe List>? GetBoneIndicesFromPap(string hash) + { + if (_configService.Current.BoneDictionary.TryGetValue(hash, out var bones)) return bones; + + var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash); + if (cacheEntity == null) return null; + + using BinaryReader reader = new BinaryReader(File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read)); + + // most of this shit is from vfxeditor, surely nothing will change in the pap format :copium: + reader.ReadInt32(); // ignore + reader.ReadInt32(); // ignore + reader.ReadInt16(); // read 2 (num animations) + reader.ReadInt16(); // read 2 (modelid) + var type = reader.ReadByte();// read 1 (type) + if (type != 0) return null; // it's not human, just ignore it, whatever + + reader.ReadByte(); // read 1 (variant) + reader.ReadInt32(); // ignore + var havokPosition = reader.ReadInt32(); + var footerPosition = reader.ReadInt32(); + var havokDataSize = footerPosition - havokPosition; + reader.BaseStream.Position = havokPosition; + var havokData = reader.ReadBytes(havokDataSize); + if (havokData.Length <= 8) return null; // no havok data + + var output = new List>(); + var tempHavokDataPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + ".hkx"; + var tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath); + + try + { + File.WriteAllBytes(tempHavokDataPath, havokData); + + var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1]; + loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry(); + loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry(); + loadoptions->Flags = new hkFlags + { + Storage = (int)(hkSerializeUtil.LoadOptionBits.Default) + }; + + var resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions); + if (resource == null) + { + throw new InvalidOperationException("Resource was null after loading"); + } + + var rootLevelName = @"hkRootLevelContainer"u8; + fixed (byte* n1 = rootLevelName) + { + var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry()); + var animationName = @"hkaAnimationContainer"u8; + fixed (byte* n2 = animationName) + { + var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null); + for (int i = 0; i < animContainer->Bindings.Length; i++) + { + var boneTransform = animContainer->Bindings[i].ptr->TransformTrackToBoneIndices; + List boneIndices = []; + for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++) + { + boneIndices.Add((ushort)boneTransform[boneIdx]); + } + + output.Add(boneIndices); + } + + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not load havok file in {path}", tempHavokDataPath); + } + finally + { + Marshal.FreeHGlobal(tempHavokDataPathAnsi); + File.Delete(tempHavokDataPath); + } + + _configService.Current.BoneDictionary[hash] = output; + _configService.Save(); + return output; + } + + public Task GetTrianglesFromGamePath(string gamePath) + { + if (_configService.Current.TriangleDictionary.TryGetValue(gamePath, out var cachedTris)) + return Task.FromResult(cachedTris); + + _logger.LogInformation("Detected Model File {path}, calculating Tris", gamePath); + var file = _luminaGameData.GetFile(gamePath); + if (file == null) + return Task.FromResult((long)0); + + if (file.FileHeader.LodCount <= 0) + return Task.FromResult((long)0); + var meshIdx = file.Lods[0].MeshIndex; + var meshCnt = file.Lods[0].MeshCount; + var tris = file.Meshes.Skip(meshIdx).Take(meshCnt).Sum(p => p.IndexCount) / 3; + + _logger.LogInformation("{filePath} => {tris} triangles", gamePath, tris); + _configService.Current.TriangleDictionary[gamePath] = tris; + _configService.Save(); + return Task.FromResult(tris); + } + + public Task GetTrianglesByHash(string hash) + { + if (_configService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris)) + return Task.FromResult(cachedTris); + + var path = _fileCacheManager.GetFileCacheByHash(hash); + if (path == null || !path.ResolvedFilepath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)) + return Task.FromResult((long)0); + + var filePath = path.ResolvedFilepath; + + _logger.LogInformation("Detected Model File {path}, calculating Tris", filePath); + var file = _luminaGameData.GetFileFromDisk(filePath); + if (file.FileHeader.LodCount <= 0) + return Task.FromResult((long)0); + var meshIdx = file.Lods[0].MeshIndex; + var meshCnt = file.Lods[0].MeshCount; + var tris = file.Meshes.Skip(meshIdx).Take(meshCnt).Sum(p => p.IndexCount) / 3; + + _logger.LogInformation("{filePath} => {tris} triangles", filePath, tris); + _configService.Current.TriangleDictionary[hash] = tris; + _configService.Save(); + return Task.FromResult(tris); + } +}