check for invalid animations

This commit is contained in:
rootdarkarchon
2024-03-29 16:04:31 +01:00
parent 8338be4815
commit e85ff0ba83
12 changed files with 298 additions and 102 deletions

View File

@@ -279,4 +279,14 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
Mediator.Publish(new TransientResourceChangedMessage(gameObject)); 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();
}
}
} }

View File

@@ -40,6 +40,7 @@ public abstract class ConfigurationServiceBase<T> : IDisposable where T : IMareC
{ {
_periodicCheckCts.Cancel(); _periodicCheckCts.Cancel();
_periodicCheckCts.Dispose(); _periodicCheckCts.Dispose();
if (_configIsDirty) SaveDirtyConfig();
} }
protected T LoadConfig() protected T LoadConfig()
@@ -94,10 +95,12 @@ public abstract class ConfigurationServiceBase<T> : IDisposable where T : IMareC
// ignore if file cannot be backupped once // 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 WriteIndented = true
})); }));
File.Move(temp, ConfigurationPath, true);
_configLastWriteTime = new FileInfo(ConfigurationPath).LastWriteTimeUtc; _configLastWriteTime = new FileInfo(ConfigurationPath).LastWriteTimeUtc;
} }

View File

@@ -2,8 +2,9 @@
namespace MareSynchronos.MareConfiguration.Configurations; namespace MareSynchronos.MareConfiguration.Configurations;
public class TriangleCalculationConfig : IMareConfiguration public class XivDataStorageConfig : IMareConfiguration
{ {
public ConcurrentDictionary<string, long> TriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase); public ConcurrentDictionary<string, long> TriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public ConcurrentDictionary<string, List<List<ushort>>> BoneDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public int Version { get; set; } = 0; public int Version { get; set; } = 0;
} }

View File

@@ -1,12 +0,0 @@
using MareSynchronos.MareConfiguration.Configurations;
namespace MareSynchronos.MareConfiguration;
public class TriangleCalculationConfigService : ConfigurationServiceBase<TriangleCalculationConfig>
{
public const string ConfigName = "trianglecache.json";
public TriangleCalculationConfigService(string configDir) : base(configDir) { }
protected override string ConfigurationName => ConfigName;
}

View File

@@ -0,0 +1,12 @@
using MareSynchronos.MareConfiguration.Configurations;
namespace MareSynchronos.MareConfiguration;
public class XivDataStorageService : ConfigurationServiceBase<XivDataStorageConfig>
{
public const string ConfigName = "xivdatastorage.json";
public XivDataStorageService(string configDir) : base(configDir) { }
protected override string ConfigurationName => ConfigName;
}

View File

@@ -20,13 +20,13 @@ public class PairHandlerFactory
private readonly IpcManager _ipcManager; private readonly IpcManager _ipcManager;
private readonly ILoggerFactory _loggerFactory; private readonly ILoggerFactory _loggerFactory;
private readonly MareMediator _mareMediator; private readonly MareMediator _mareMediator;
private readonly ModelAnalyzer _modelAnalyzer; private readonly XivDataAnalyzer _xivDataAnalyzer;
private readonly PluginWarningNotificationService _pluginWarningNotificationManager; private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager, public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager,
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService, FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime, PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
FileCacheManager fileCacheManager, MareMediator mareMediator, ModelAnalyzer modelAnalyzer) FileCacheManager fileCacheManager, MareMediator mareMediator, XivDataAnalyzer modelAnalyzer)
{ {
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_gameObjectHandlerFactory = gameObjectHandlerFactory; _gameObjectHandlerFactory = gameObjectHandlerFactory;
@@ -37,13 +37,13 @@ public class PairHandlerFactory
_hostApplicationLifetime = hostApplicationLifetime; _hostApplicationLifetime = hostApplicationLifetime;
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_mareMediator = mareMediator; _mareMediator = mareMediator;
_modelAnalyzer = modelAnalyzer; _xivDataAnalyzer = modelAnalyzer;
} }
public PairHandler Create(OnlineUserIdentDto onlineUserIdentDto) public PairHandler Create(OnlineUserIdentDto onlineUserIdentDto)
{ {
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), onlineUserIdentDto, _gameObjectHandlerFactory, return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), onlineUserIdentDto, _gameObjectHandlerFactory,
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime, _ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
_fileCacheManager, _mareMediator, _modelAnalyzer); _fileCacheManager, _mareMediator, _xivDataAnalyzer);
} }
} }

View File

@@ -5,6 +5,7 @@ using MareSynchronos.Interop.Ipc;
using MareSynchronos.PlayerData.Data; using MareSynchronos.PlayerData.Data;
using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services; using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using CharacterData = MareSynchronos.PlayerData.Data.CharacterData; using CharacterData = MareSynchronos.PlayerData.Data.CharacterData;
@@ -18,11 +19,13 @@ public class PlayerDataFactory
private readonly IpcManager _ipcManager; private readonly IpcManager _ipcManager;
private readonly ILogger<PlayerDataFactory> _logger; private readonly ILogger<PlayerDataFactory> _logger;
private readonly PerformanceCollectorService _performanceCollector; private readonly PerformanceCollectorService _performanceCollector;
private readonly XivDataAnalyzer _modelAnalyzer;
private readonly MareMediator _mareMediator;
private readonly TransientResourceManager _transientResourceManager; private readonly TransientResourceManager _transientResourceManager;
public PlayerDataFactory(ILogger<PlayerDataFactory> logger, DalamudUtilService dalamudUtil, IpcManager ipcManager, public PlayerDataFactory(ILogger<PlayerDataFactory> logger, DalamudUtilService dalamudUtil, IpcManager ipcManager,
TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory, TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory,
PerformanceCollectorService performanceCollector) PerformanceCollectorService performanceCollector, XivDataAnalyzer modelAnalyzer, MareMediator mareMediator)
{ {
_logger = logger; _logger = logger;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
@@ -30,6 +33,8 @@ public class PlayerDataFactory
_transientResourceManager = transientResourceManager; _transientResourceManager = transientResourceManager;
_fileCacheManager = fileReplacementFactory; _fileCacheManager = fileReplacementFactory;
_performanceCollector = performanceCollector; _performanceCollector = performanceCollector;
_modelAnalyzer = modelAnalyzer;
_mareMediator = mareMediator;
_logger.LogTrace("Creating {this}", nameof(PlayerDataFactory)); _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); _logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(DateTime.UtcNow.Ticks - start.Ticks).TotalMilliseconds);
return previousData; 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<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(HashSet<string> forwardResolve, HashSet<string> reverseResolve) private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(HashSet<string> forwardResolve, HashSet<string> reverseResolve)
{ {
var forwardPaths = forwardResolve.ToArray(); var forwardPaths = forwardResolve.ToArray();

View File

@@ -24,7 +24,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
private readonly FileDownloadManager _downloadManager; private readonly FileDownloadManager _downloadManager;
private readonly FileCacheManager _fileDbManager; private readonly FileCacheManager _fileDbManager;
private readonly ModelAnalyzer _modelAnalyzer; private readonly XivDataAnalyzer _xivDataAnalyzer;
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
private readonly IpcManager _ipcManager; private readonly IpcManager _ipcManager;
private readonly IHostApplicationLifetime _lifetime; private readonly IHostApplicationLifetime _lifetime;
@@ -49,7 +49,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
PluginWarningNotificationService pluginWarningNotificationManager, PluginWarningNotificationService pluginWarningNotificationManager,
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime, DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
FileCacheManager fileDbManager, MareMediator mediator, FileCacheManager fileDbManager, MareMediator mediator,
ModelAnalyzer modelAnalyzer) : base(logger, mediator) XivDataAnalyzer modelAnalyzer) : base(logger, mediator)
{ {
OnlineUser = onlineUser; OnlineUser = onlineUser;
_gameObjectHandlerFactory = gameObjectHandlerFactory; _gameObjectHandlerFactory = gameObjectHandlerFactory;
@@ -59,7 +59,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
_lifetime = lifetime; _lifetime = lifetime;
_fileDbManager = fileDbManager; _fileDbManager = fileDbManager;
_modelAnalyzer = modelAnalyzer; _xivDataAnalyzer = modelAnalyzer;
_penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, OnlineUser.User.UID).ConfigureAwait(false).GetAwaiter().GetResult(); _penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, OnlineUser.User.UID).ConfigureAwait(false).GetAwaiter().GetResult();
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate()); Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
@@ -454,7 +454,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
foreach (var key in moddedPaths.Keys.Where(k => !string.IsNullOrEmpty(k.Hash))) foreach (var key in moddedPaths.Keys.Where(k => !string.IsNullOrEmpty(k.Hash)))
{ {
if (LastAppliedDataTris == -1) LastAppliedDataTris = 0; if (LastAppliedDataTris == -1) LastAppliedDataTris = 0;
LastAppliedDataTris += await _modelAnalyzer.GetTrianglesByHash(key.Hash!).ConfigureAwait(false); LastAppliedDataTris += await _xivDataAnalyzer.GetTrianglesByHash(key.Hash!).ConfigureAwait(false);
} }
} }

View File

@@ -68,8 +68,8 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<FileDownloadManagerFactory>(); collection.AddSingleton<FileDownloadManagerFactory>();
collection.AddSingleton<PairHandlerFactory>(); collection.AddSingleton<PairHandlerFactory>();
collection.AddSingleton<PairFactory>(); collection.AddSingleton<PairFactory>();
collection.AddSingleton<ModelAnalyzer>(s => new(s.GetRequiredService<ILogger<ModelAnalyzer>>(), s.GetRequiredService<FileCacheManager>(), collection.AddSingleton<XivDataAnalyzer>(s => new(s.GetRequiredService<ILogger<XivDataAnalyzer>>(), s.GetRequiredService<FileCacheManager>(),
s.GetRequiredService<TriangleCalculationConfigService>(), gameData)); s.GetRequiredService<XivDataStorageService>(), gameData));
collection.AddSingleton<CharacterAnalyzer>(); collection.AddSingleton<CharacterAnalyzer>();
collection.AddSingleton<TokenProvider>(); collection.AddSingleton<TokenProvider>();
collection.AddSingleton<PluginWarningNotificationService>(); collection.AddSingleton<PluginWarningNotificationService>();
@@ -113,7 +113,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ServerTagConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ServerTagConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new TransientConfigService(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<ILogger<ConfigurationMigrator>>(), pluginInterface)); collection.AddSingleton((s) => new ConfigurationMigrator(s.GetRequiredService<ILogger<ConfigurationMigrator>>(), pluginInterface));
collection.AddSingleton<HubFactory>(); collection.AddSingleton<HubFactory>();

View File

@@ -12,12 +12,12 @@ namespace MareSynchronos.Services;
public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
{ {
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private readonly ModelAnalyzer _modelAnalyzer; private readonly XivDataAnalyzer _xivDataAnalyzer;
private CancellationTokenSource? _analysisCts; private CancellationTokenSource? _analysisCts;
private CancellationTokenSource _baseAnalysisCts = new(); private CancellationTokenSource _baseAnalysisCts = new();
private string _lastDataHash = string.Empty; private string _lastDataHash = string.Empty;
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager, ModelAnalyzer modelAnalyzer) public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
: base(logger, mediator) : base(logger, mediator)
{ {
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) => Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
@@ -27,7 +27,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
_ = BaseAnalysis(msg.CharacterData, token); _ = BaseAnalysis(msg.CharacterData, token);
}); });
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_modelAnalyzer = modelAnalyzer; _xivDataAnalyzer = modelAnalyzer;
} }
public int CurrentFile { get; internal set; } 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); 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) foreach (var entry in fileCacheEntries)
{ {

View File

@@ -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<ModelAnalyzer> _logger;
private readonly FileCacheManager _fileCacheManager;
private readonly TriangleCalculationConfigService _configService;
private readonly GameData _luminaGameData;
public ModelAnalyzer(ILogger<ModelAnalyzer> logger, FileCacheManager fileCacheManager,
TriangleCalculationConfigService configService, IDataManager gameData)
{
_logger = logger;
_fileCacheManager = fileCacheManager;
_configService = configService;
_luminaGameData = new GameData(gameData.GameData.DataPath.FullName);
}
public Task<long> 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<MdlFile>(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<long> 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<MdlFile>(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);
}
}

View File

@@ -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<XivDataAnalyzer> _logger;
private readonly FileCacheManager _fileCacheManager;
private readonly XivDataStorageService _configService;
private readonly GameData _luminaGameData;
public XivDataAnalyzer(ILogger<XivDataAnalyzer> logger, FileCacheManager fileCacheManager,
XivDataStorageService configService, IDataManager gameData)
{
_logger = logger;
_fileCacheManager = fileCacheManager;
_configService = configService;
_luminaGameData = new GameData(gameData.GameData.DataPath.FullName);
}
public unsafe List<ushort>? 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<ushort> outputIndices = new();
while (*(resHandles + ++i) != null)
{
var handle = *(resHandles + i);
var curBones = handle->BoneCount;
List<ushort> 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<List<ushort>>? 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<List<ushort>>();
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<hkSerializeUtil.LoadOptionBits, int>
{
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<ushort> 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<long> 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<MdlFile>(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<long> 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<MdlFile>(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);
}
}