fix some shit, add triangle count

check for invalid animations

I hate animations

ignore broken bones from god knows what

fix more idiotic mod things

fully ignore garbage skeletons that fail to process properly

fix my own mistakes

fix more bullshit

check for filename length and continue

idk some cleanup

fix spoopy skellingtons

change loglevel of tris
This commit is contained in:
rootdarkarchon
2024-03-20 15:12:19 +01:00
committed by Loporrit
parent 5642d354e0
commit 324288652d
24 changed files with 432 additions and 40 deletions

View File

@@ -100,3 +100,12 @@ dotnet_diagnostic.MA0075.severity = silent
# S3358: Ternary operators should not be nested
dotnet_diagnostic.S3358.severity = suggestion
# S6678: Use PascalCase for named placeholders
dotnet_diagnostic.S6678.severity = suggestion
# S6605: Collection-specific "Exists" method should be used instead of the "Any" extension
dotnet_diagnostic.S6605.severity = suggestion
# S6667: Logging in a catch clause should pass the caught exception as a parameter.
dotnet_diagnostic.S6667.severity = suggestion

View File

@@ -5,6 +5,7 @@ using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Diagnostics;
namespace MareSynchronos.FileCache;
@@ -25,6 +26,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
FileCacheManager fileDbManager, MareMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
FileCompactor fileCompactor) : base(logger, mediator)
{
Logger.LogInformation("Creating CacheMonitor from {trace}", Environment.StackTrace);
_ipcManager = ipcManager;
_configService = configService;
_fileDbManager = fileDbManager;

View File

@@ -208,7 +208,7 @@ public sealed class FileCacheManager : IHostedService
foreach (var entry in cleanedPaths)
{
_logger.LogDebug("Checking {path}", entry.Value);
//_logger.LogDebug("Checking {path}", entry.Value);
if (dict.TryGetValue(entry.Value, out var entity))
{
@@ -336,7 +336,7 @@ public sealed class FileCacheManager : IHostedService
if (!entries.Exists(u => string.Equals(u.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.OrdinalIgnoreCase)))
{
_logger.LogTrace("Adding to DB: {hash} => {path}", fileCache.Hash, fileCache.PrefixedFilePath);
//_logger.LogTrace("Adding to DB: {hash} => {path}", fileCache.Hash, fileCache.PrefixedFilePath);
entries.Add(fileCache);
}
}
@@ -359,7 +359,7 @@ public sealed class FileCacheManager : IHostedService
private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache)
{
var resultingFileCache = ReplacePathPrefixes(fileCache);
_logger.LogTrace("Validating {path}", fileCache.PrefixedFilePath);
//_logger.LogTrace("Validating {path}", fileCache.PrefixedFilePath);
resultingFileCache = Validate(resultingFileCache);
return resultingFileCache;
}

View File

@@ -277,4 +277,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();
}
}
}

View File

@@ -40,6 +40,7 @@ public abstract class ConfigurationServiceBase<T> : IDisposable where T : IMareC
{
_periodicCheckCts.Cancel();
_periodicCheckCts.Dispose();
if (_configIsDirty) SaveDirtyConfig();
}
protected T LoadConfig()
@@ -94,10 +95,12 @@ public abstract class ConfigurationServiceBase<T> : 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;
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Concurrent;
namespace MareSynchronos.MareConfiguration.Configurations;
public class XivDataStorageConfig : IMareConfiguration
{
public ConcurrentDictionary<string, long> TriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public ConcurrentDictionary<string, Dictionary<string, List<ushort>>> BonesDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public int Version { get; set; } = 0;
}

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

@@ -74,6 +74,7 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly IServiceScopeFactory _serviceScopeFactory;
private IServiceScope? _runtimeServiceScope;
private Task? _launchTask = null;
public MarePlugin(ILogger<MarePlugin> logger, MareConfigService mareConfigService,
ServerConfigurationManager serverConfigurationManager,
@@ -93,7 +94,7 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(MarePlugin), Services.Events.EventSeverity.Informational,
$"Starting Loporrit Sync {version.Major}.{version.Minor}.{version.Build}-lop{version.Revision}")));
Mediator.Subscribe<SwitchToMainUiMessage>(this, (msg) => _ = Task.Run(WaitForPlayerAndLaunchCharacterManager));
Mediator.Subscribe<SwitchToMainUiMessage>(this, (msg) => { if (_launchTask == null || _launchTask.IsCompleted) _launchTask = Task.Run(WaitForPlayerAndLaunchCharacterManager); });
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut());
@@ -116,8 +117,7 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService
private void DalamudUtilOnLogIn()
{
Logger?.LogDebug("Client login");
_ = Task.Run(WaitForPlayerAndLaunchCharacterManager);
if (_launchTask == null || _launchTask.IsCompleted) _launchTask = Task.Run(WaitForPlayerAndLaunchCharacterManager);
}
private void DalamudUtilOnLogOut()

View File

@@ -20,13 +20,14 @@ public class PairHandlerFactory
private readonly IpcManager _ipcManager;
private readonly ILoggerFactory _loggerFactory;
private readonly MareMediator _mareMediator;
private readonly XivDataAnalyzer _xivDataAnalyzer;
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
private readonly ServerConfigurationManager _serverConfigurationManager;
public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager,
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
PluginWarningNotificationService pluginWarningNotificationManager, ServerConfigurationManager serverConfigurationManager,
CancellationToken dalamudLifetime, FileCacheManager fileCacheManager, MareMediator mareMediator)
CancellationToken dalamudLifetime, FileCacheManager fileCacheManager, MareMediator mareMediator, XivDataAnalyzer modelAnalyzer)
{
_loggerFactory = loggerFactory;
_gameObjectHandlerFactory = gameObjectHandlerFactory;
@@ -38,12 +39,13 @@ public class PairHandlerFactory
_dalamudLifetimeToken = dalamudLifetime;
_fileCacheManager = fileCacheManager;
_mareMediator = mareMediator;
_xivDataAnalyzer = modelAnalyzer;
}
public PairHandler Create(OnlineUserIdentDto onlineUserIdentDto)
{
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), onlineUserIdentDto, _gameObjectHandlerFactory,
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _serverConfigurationManager, _dalamudUtilService,
_dalamudLifetimeToken, _fileCacheManager, _mareMediator);
_dalamudLifetimeToken, _fileCacheManager, _mareMediator, _xivDataAnalyzer);
}
}

View File

@@ -2,9 +2,11 @@
using MareSynchronos.API.Data.Enum;
using MareSynchronos.FileCache;
using MareSynchronos.Interop.Ipc;
using MareSynchronos.MareConfiguration.Models;
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 +20,13 @@ public class PlayerDataFactory
private readonly IpcManager _ipcManager;
private readonly ILogger<PlayerDataFactory> _logger;
private readonly PerformanceCollectorService _performanceCollector;
private readonly XivDataAnalyzer _modelAnalyzer;
private readonly MareMediator _mareMediator;
private readonly TransientResourceManager _transientResourceManager;
public PlayerDataFactory(ILogger<PlayerDataFactory> logger, DalamudUtilService dalamudUtil, IpcManager ipcManager,
TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory,
PerformanceCollectorService performanceCollector)
PerformanceCollectorService performanceCollector, XivDataAnalyzer modelAnalyzer, MareMediator mareMediator)
{
_logger = logger;
_dalamudUtil = dalamudUtil;
@@ -30,7 +34,9 @@ public class PlayerDataFactory
_transientResourceManager = transientResourceManager;
_fileCacheManager = fileReplacementFactory;
_performanceCollector = performanceCollector;
_logger.LogTrace("Creating " + nameof(PlayerDataFactory));
_modelAnalyzer = modelAnalyzer;
_mareMediator = mareMediator;
_logger.LogTrace("Creating {this}", nameof(PlayerDataFactory));
}
public async Task BuildCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token)
@@ -128,12 +134,16 @@ public class PlayerDataFactory
// wait until chara is not drawing and present so nothing spontaneously explodes
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: token).ConfigureAwait(false);
int totalWaitTime = 10000;
while (!await _dalamudUtil.IsObjectPresentAsync(await _dalamudUtil.CreateGameObjectAsync(charaPointer).ConfigureAwait(false)).ConfigureAwait(false) && totalWaitTime > 0)
while (!await _dalamudUtil.IsObjectPresentAsync(await _dalamudUtil.CreateGameObjectAsync(playerRelatedObject.Address).ConfigureAwait(false)).ConfigureAwait(false) && totalWaitTime > 0)
{
_logger.LogTrace("Character is null but it shouldn't be, waiting");
await Task.Delay(50, token).ConfigureAwait(false);
totalWaitTime -= 50;
}
Dictionary<string, List<ushort>>? boneIndices =
objectKind != ObjectKind.Player
? null
: await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
DateTime start = DateTime.UtcNow;
@@ -226,11 +236,84 @@ public class PlayerDataFactory
}
}
if (objectKind == ObjectKind.Player)
{
try
{
await VerifyPlayerAnimationBones(boneIndices, previousData, objectKind).ConfigureAwait(false);
}
catch (Exception e)
{
_logger.LogWarning(e, "Failed to verify player animations, continuing without further verification");
}
}
_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(Dictionary<string, List<ushort>>? boneIndices, CharacterData previousData, ObjectKind objectKind)
{
if (boneIndices == null) return;
foreach (var kvp in boneIndices)
{
_logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
}
if (boneIndices.All(u => u.Value.Count == 0)) return;
int noValidationFailed = 0;
foreach (var file in previousData.FileReplacements[objectKind].Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
{
var skeletonIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetBoneIndicesFromPap(file.Hash)).ConfigureAwait(false);
bool validationFailed = false;
if (skeletonIndices != null)
{
// 105 is the maximum vanilla skellington spoopy bone index
if (skeletonIndices.All(k => k.Value.Max() <= 105))
{
_logger.LogTrace("All indices of {path} are <= 105, ignoring", file.ResolvedPath);
continue;
}
_logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
foreach (var boneCount in skeletonIndices.Select(k => k).ToList())
{
if (boneCount.Value.Max() > boneIndices.SelectMany(b => b.Value).Max())
{
_logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})",
file.ResolvedPath, boneCount.Key, boneCount.Value.Max(), boneIndices.SelectMany(b => b.Value).Max());
validationFailed = true;
break;
}
}
}
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).",
NotificationType.Warning, TimeSpan.FromSeconds(10)));
}
}
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(HashSet<string> forwardResolve, HashSet<string> reverseResolve)
{
var forwardPaths = forwardResolve.ToArray();

View File

@@ -24,6 +24,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
private readonly DalamudUtilService _dalamudUtil;
private readonly FileDownloadManager _downloadManager;
private readonly FileCacheManager _fileDbManager;
private readonly XivDataAnalyzer _xivDataAnalyzer;
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
private readonly IpcManager _ipcManager;
private readonly CancellationToken _lifetime;
@@ -42,13 +43,15 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
private bool _redrawOnNextApplication = false;
private CombatData? _dataReceivedInCombat;
public long LastAppliedDataSize { get; private set; }
public long LastAppliedDataTris { get; private set; }
public PairHandler(ILogger<PairHandler> logger, OnlineUserIdentDto onlineUser,
GameObjectHandlerFactory gameObjectHandlerFactory,
IpcManager ipcManager, FileDownloadManager transferManager,
PluginWarningNotificationService pluginWarningNotificationManager, ServerConfigurationManager serverConfigurationManager,
DalamudUtilService dalamudUtil, CancellationToken lifetime,
FileCacheManager fileDbManager, MareMediator mediator) : base(logger, mediator)
FileCacheManager fileDbManager, MareMediator mediator,
XivDataAnalyzer modelAnalyzer) : base(logger, mediator)
{
OnlineUser = onlineUser;
_gameObjectHandlerFactory = gameObjectHandlerFactory;
@@ -59,6 +62,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
_dalamudUtil = dalamudUtil;
_lifetime = lifetime;
_fileDbManager = fileDbManager;
_xivDataAnalyzer = modelAnalyzer;
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) =>
@@ -100,6 +104,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
});
LastAppliedDataSize = -1;
LastAppliedDataTris = -1;
}
public bool IsVisible
@@ -377,7 +382,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
_ = Task.Run(async () =>
{
Dictionary<string, string> moddedPaths = new(StringComparer.Ordinal);
Dictionary<(string GamePath, string? Hash), string> moddedPaths = new();
if (updateModdedPaths)
{
@@ -444,12 +449,20 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
if (updateModdedPaths)
{
await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, _penumbraCollection, moddedPaths).ConfigureAwait(false);
await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, _penumbraCollection,
moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false);
LastAppliedDataSize = -1;
LastAppliedDataTris = -1;
foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists))
{
if (LastAppliedDataSize == -1) LastAppliedDataSize = 0;
LastAppliedDataSize += path.Length;
}
foreach (var key in moddedPaths.Keys.Where(k => !string.IsNullOrEmpty(k.Hash)))
{
if (LastAppliedDataTris == -1) LastAppliedDataTris = 0;
LastAppliedDataTris += await _xivDataAnalyzer.GetTrianglesByHash(key.Hash!).ConfigureAwait(false);
}
}
if (updateManip)
@@ -619,12 +632,12 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
}
}
private List<FileReplacementData> TryCalculateModdedDictionary(Guid applicationBase, CharacterData charaData, out Dictionary<string, string> moddedDictionary, CancellationToken token)
private List<FileReplacementData> TryCalculateModdedDictionary(Guid applicationBase, CharacterData charaData, out Dictionary<(string GamePath, string? Hash), string> moddedDictionary, CancellationToken token)
{
Stopwatch st = Stopwatch.StartNew();
ConcurrentBag<FileReplacementData> missingFiles = [];
moddedDictionary = new Dictionary<string, string>(StringComparer.Ordinal);
ConcurrentDictionary<string, string> outputDict = new(StringComparer.Ordinal);
moddedDictionary = new Dictionary<(string GamePath, string? Hash), string>();
ConcurrentDictionary<(string GamePath, string? Hash), string> outputDict = new();
bool hasMigrationChanges = false;
try
@@ -649,7 +662,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
foreach (var gamePath in item.GamePaths)
{
outputDict[gamePath] = fileCache.ResolvedFilepath;
outputDict[(gamePath, item.Hash)] = fileCache.ResolvedFilepath;
}
}
else
@@ -659,14 +672,14 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
}
});
moddedDictionary = outputDict.ToDictionary(k => k.Key, k => k.Value, StringComparer.Ordinal);
moddedDictionary = outputDict.ToDictionary(k => k.Key, k => k.Value);
foreach (var item in charaData.FileReplacements.SelectMany(k => k.Value.Where(v => !string.IsNullOrEmpty(v.FileSwapPath))).ToList())
{
foreach (var gamePath in item.GamePaths)
{
Logger.LogTrace("[BASE-{appBase}] Adding file swap for {path}: {fileSwap}", applicationBase, gamePath, item.FileSwapPath);
moddedDictionary[gamePath] = item.FileSwapPath;
moddedDictionary[(gamePath, null)] = item.FileSwapPath;
}
}
}

View File

@@ -50,6 +50,7 @@ public class Pair
public string? PlayerName => GetPlayerName();
public uint PlayerCharacterId => GetPlayerCharacterId();
public long LastAppliedDataSize => CachedPlayer?.LastAppliedDataSize ?? -1;
public long LastAppliedDataTris => CachedPlayer?.LastAppliedDataTris ?? -1;
public UserData UserData => UserPair?.User ?? GroupPair.First().Value.User;

View File

@@ -85,8 +85,10 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<IpcManager>(), s.GetRequiredService<FileDownloadManagerFactory>(), s.GetRequiredService<DalamudUtilService>(),
s.GetRequiredService<PluginWarningNotificationService>(), s.GetRequiredService<ServerConfigurationManager>(),
CancellationTokenSource.CreateLinkedTokenSource(addonLifecycle.GameShuttingDownToken, addonLifecycle.DalamudUnloadingToken).Token,
s.GetRequiredService<FileCacheManager>(), s.GetRequiredService<MareMediator>()));
s.GetRequiredService<FileCacheManager>(), s.GetRequiredService<MareMediator>(), s.GetRequiredService<XivDataAnalyzer>()));
collection.AddSingleton<PairFactory>();
collection.AddSingleton<XivDataAnalyzer>(s => new(s.GetRequiredService<ILogger<XivDataAnalyzer>>(), s.GetRequiredService<FileCacheManager>(),
s.GetRequiredService<XivDataStorageService>(), gameData));
collection.AddSingleton<CharacterAnalyzer>();
collection.AddSingleton<TokenProvider>();
collection.AddSingleton<PluginWarningNotificationService>();
@@ -124,6 +126,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton((s) => new ServerTagConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new SyncshellConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new TransientConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new XivDataStorageService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ConfigurationMigrator(s.GetRequiredService<ILogger<ConfigurationMigrator>>(), pluginInterface, s.GetRequiredService<NotesConfigService>()));
collection.AddSingleton<HubFactory>();

View File

@@ -12,16 +12,22 @@ namespace MareSynchronos.Services;
public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
{
private readonly FileCacheManager _fileCacheManager;
private readonly XivDataAnalyzer _xivDataAnalyzer;
private CancellationTokenSource? _analysisCts;
private CancellationTokenSource _baseAnalysisCts = new();
private string _lastDataHash = string.Empty;
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager) : base(logger, mediator)
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
: base(logger, mediator)
{
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
{
_ = Task.Run(() => BaseAnalysis(msg.CharacterData.DeepClone()));
_baseAnalysisCts = _baseAnalysisCts.CancelRecreate();
var token = _baseAnalysisCts.Token;
_ = BaseAnalysis(msg.CharacterData, token);
});
_fileCacheManager = fileCacheManager;
_xivDataAnalyzer = modelAnalyzer;
}
public int CurrentFile { get; internal set; }
@@ -87,7 +93,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
_analysisCts.CancelDispose();
}
private void BaseAnalysis(CharacterData charaData)
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
{
if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return;
@@ -98,6 +104,8 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
Dictionary<string, FileDataEntry> data = new(StringComparer.OrdinalIgnoreCase);
foreach (var fileEntry in obj.Value)
{
token.ThrowIfCancellationRequested();
var fileCacheEntries = _fileCacheManager.GetAllFileCachesByHash(fileEntry.Hash, ignoreCacheEntries: true, validate: false).ToList();
if (fileCacheEntries.Count == 0) continue;
@@ -113,12 +121,16 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
Logger.LogWarning(ex, "Could not identify extension for {path}", filePath);
}
var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false);
foreach (var entry in fileCacheEntries)
{
data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext,
[.. fileEntry.GamePaths],
fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct().ToList(),
entry.Size > 0 ? entry.Size.Value : 0, entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0);
entry.Size > 0 ? entry.Size.Value : 0,
entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0,
tris);
}
}
@@ -176,7 +188,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
Logger.LogInformation("IMPORTANT NOTES:\n\r- For uploads and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly.");
}
internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize)
internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles)
{
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token)
@@ -194,6 +206,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
}
public long OriginalSize { get; private set; } = OriginalSize;
public long CompressedSize { get; private set; } = CompressedSize;
public long Triangles { get; private set; } = Triangles;
public Lazy<string> Format = new(() =>
{

View File

@@ -72,7 +72,7 @@ public sealed class CommandManagerService : IDisposable
{
var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
if (splitArgs == null || splitArgs.Length == 0)
if (splitArgs.Length == 0)
{
// Interpret this as toggling the UI
if (_mareConfigService.Current.HasValidSetup())

View File

@@ -0,0 +1,202 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.Havok.Animation;
using FFXIVClientStructs.Havok.Common.Base.Types;
using FFXIVClientStructs.Havok.Common.Serialize.Util;
using Lumina;
using Lumina.Data.Files;
using MareSynchronos.FileCache;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Handlers;
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 Dictionary<string, List<ushort>>? GetSkeletonBoneIndices(GameObjectHandler handler)
{
if (handler.Address == nint.Zero) return null;
var chara = (CharacterBase*)(((Character*)handler.Address)->GameObject.DrawObject);
if (chara->GetModelType() != CharacterBase.ModelType.Human) return null;
var resHandles = chara->Skeleton->SkeletonResourceHandles;
Dictionary<string, List<ushort>> outputIndices = [];
try
{
for (int i = 0; i < chara->Skeleton->PartialSkeletonCount; i++)
{
var handle = *(resHandles + i);
_logger.LogTrace("Iterating over SkeletonResourceHandle #{i}:{x}", i, ((nint)handle).ToString("X"));
if ((nint)handle == nint.Zero) continue;
var curBones = handle->BoneCount;
// this is unrealistic, the filename shouldn't ever be that long
if (handle->ResourceHandle.FileName.Length > 1024) continue;
var skeletonName = handle->ResourceHandle.FileName.ToString();
if (string.IsNullOrEmpty(skeletonName)) continue;
outputIndices[skeletonName] = new();
for (ushort boneIdx = 0; boneIdx < curBones; boneIdx++)
{
var boneName = handle->HavokSkeleton->Bones[boneIdx].Name.String;
if (boneName == null) continue;
outputIndices[skeletonName].Add((ushort)(boneIdx + 1));
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not process skeleton data");
}
return (outputIndices.Count != 0 && outputIndices.Values.All(u => u.Count > 0)) ? outputIndices : null;
}
public unsafe Dictionary<string, List<ushort>>? GetBoneIndicesFromPap(string hash)
{
if (_configService.Current.BonesDictionary.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 Dictionary<string, List<ushort>>(StringComparer.OrdinalIgnoreCase);
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 binding = animContainer->Bindings[i].ptr;
var boneTransform = binding->TransformTrackToBoneIndices;
string name = binding->OriginalSkeletonName.String! + "_" + i;
output[name] = [];
for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++)
{
output[name].Add((ushort)boneTransform[boneIdx]);
}
output[name].Sort();
}
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not load havok file in {path}", tempHavokDataPath);
}
finally
{
Marshal.FreeHGlobal(tempHavokDataPathAnsi);
File.Delete(tempHavokDataPath);
}
_configService.Current.BonesDictionary[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.LogDebug("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.LogDebug("{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.LogDebug("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.LogDebug("{filePath} => {tris} triangles", filePath, tris);
_configService.Current.TriangleDictionary[hash] = tris;
_configService.Save();
return Task.FromResult(tris);
}
}

View File

@@ -76,8 +76,14 @@ public class DrawGroupPair : DrawPairBase
}
if (_pair.LastAppliedDataSize >= 0)
{
presenceText += UiSharedService.TooltipSeparator +
"Loaded Mods Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataSize, true);
presenceText += UiSharedService.TooltipSeparator;
presenceText += ((!_pair.IsVisible) ? "(Last) " : string.Empty) + "Mods Info" + Environment.NewLine;
presenceText += "Files Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataSize, true);
if (_pair.LastAppliedDataTris >= 0)
{
presenceText += Environment.NewLine + "Triangle Count (excl. Vanilla): "
+ (_pair.LastAppliedDataTris > 1000 ? (_pair.LastAppliedDataTris / 1000d).ToString("0.0'k'") : _pair.LastAppliedDataTris);
}
}
}
UiSharedService.AttachToolTip(presenceText);

View File

@@ -74,8 +74,14 @@ public class DrawUserPair : DrawPairBase
var visibleTooltip = _pair.UserData.AliasOrUID + " is visible: " + _pair.PlayerName! + Environment.NewLine + "Click to target this player";
if (_pair.LastAppliedDataSize >= 0)
{
visibleTooltip += UiSharedService.TooltipSeparator +
"Loaded Mods Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataSize, true);
visibleTooltip += UiSharedService.TooltipSeparator;
visibleTooltip += ((!_pair.IsVisible) ? "(Last) " : string.Empty) + "Mods Info" + Environment.NewLine;
visibleTooltip += "Files Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataSize, true);
if (_pair.LastAppliedDataTris >= 0)
{
visibleTooltip += Environment.NewLine + "Triangle Count (excl. Vanilla): "
+ (_pair.LastAppliedDataTris > 1000 ? (_pair.LastAppliedDataTris / 1000d).ToString("0.0'k'") : _pair.LastAppliedDataTris);
}
}
UiSharedService.AttachToolTip(visibleTooltip);

View File

@@ -15,7 +15,8 @@ public class PopupHandler : WindowMediatorSubscriberBase
private readonly HashSet<IPopupHandler> _handlers;
private IPopupHandler? _currentHandler = null;
public PopupHandler(ILogger<PopupHandler> logger, MareMediator mediator, IEnumerable<IPopupHandler> popupHandlers, PerformanceCollectorService performanceCollectorService)
public PopupHandler(ILogger<PopupHandler> logger, MareMediator mediator, IEnumerable<IPopupHandler> popupHandlers,
PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "MarePopupHandler", performanceCollectorService)
{
Flags = ImGuiWindowFlags.NoBringToFrontOnFocus

View File

@@ -158,7 +158,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.TextUnformatted("Total size (compressed):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.CompressedSize))));
ImGui.TextUnformatted($"Total modded model triangles: {_cachedAnalysis.Sum(c => c.Value.Sum(f => f.Value.Triangles))}");
ImGui.Separator();
using var tabbar = ImRaii.TabBar("objectSelection");
@@ -195,6 +195,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.TextUnformatted($"{kvp.Key} size (compressed):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize)));
ImGui.TextUnformatted($"{kvp.Key} modded model triangles: {kvp.Value.Sum(f => f.Value.Triangles)}");
ImGui.Separator();
if (_selectedObjectTab != kvp.Key)
@@ -334,8 +335,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private void DrawTable(IGrouping<string, CharacterAnalyzer.FileDataEntry> fileGroup)
{
using var table = ImRaii.Table("Analysis", string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) ?
(_enableBc7ConversionMode ? 7 : 6) : 5, ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit,
var tableColumns = string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal)
? (_enableBc7ConversionMode ? 7 : 6)
: (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) ? 6 : 5);
using var table = ImRaii.Table("Analysis", tableColumns, ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit,
new Vector2(0, 300));
if (!table.Success) return;
ImGui.TableSetupColumn("Hash");
@@ -348,6 +351,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.TableSetupColumn("Format");
if (_enableBc7ConversionMode) ImGui.TableSetupColumn("Convert to BC7");
}
if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal))
{
ImGui.TableSetupColumn("Triangles");
}
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
@@ -441,6 +448,12 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
}
}
}
if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal))
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(item.Triangles.ToString());
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
}
}
}
}

View File

@@ -27,7 +27,6 @@ public class GposeUi : WindowMediatorSubscriberBase
_dalamudUtil = dalamudUtil;
_fileDialogManager = fileDialogManager;
_configService = configService;
Mediator.Subscribe<GposeStartMessage>(this, (_) => StartGpose());
Mediator.Subscribe<GposeEndMessage>(this, (_) => EndGpose());
IsOpen = _dalamudUtil.IsInGpose;

View File

@@ -83,7 +83,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
{
stream.Dispose();
}
catch { }
catch
{
// do nothing
//
}
}
base.Dispose(disposing);
}