Testing PAP handling changes.
This commit is contained in:
@@ -21,6 +21,7 @@ public class PlayerDataFactory
|
||||
private readonly XivDataAnalyzer _modelAnalyzer;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly TransientResourceManager _transientResourceManager;
|
||||
private static readonly SemaphoreSlim _papParseLimiter = new(1, 1);
|
||||
|
||||
public PlayerDataFactory(ILogger<PlayerDataFactory> logger, DalamudUtilService dalamudUtil, IpcManager ipcManager,
|
||||
TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory,
|
||||
@@ -121,7 +122,6 @@ public class PlayerDataFactory
|
||||
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
|
||||
var logDebug = _logger.IsEnabled(LogLevel.Debug);
|
||||
|
||||
// wait until chara is not drawing and present so nothing spontaneously explodes
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
|
||||
int totalWaitTime = 10000;
|
||||
while (!await _dalamudUtil.IsObjectPresentAsync(await _dalamudUtil.CreateGameObjectAsync(playerRelatedObject.Address).ConfigureAwait(false)).ConfigureAwait(false) && totalWaitTime > 0)
|
||||
@@ -135,7 +135,6 @@ public class PlayerDataFactory
|
||||
|
||||
DateTime start = DateTime.UtcNow;
|
||||
|
||||
// penumbra call, it's currently broken
|
||||
Dictionary<string, HashSet<string>>? resolvedPaths;
|
||||
|
||||
resolvedPaths = (await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false));
|
||||
@@ -144,8 +143,7 @@ public class PlayerDataFactory
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
fragment.FileReplacements =
|
||||
new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance)
|
||||
.Where(p => p.HasFileReplacement).ToHashSet();
|
||||
[.. new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance).Where(p => p.HasFileReplacement)];
|
||||
fragment.FileReplacements.RemoveWhere(c => c.GamePaths.Any(g => !CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
@@ -169,8 +167,6 @@ public class PlayerDataFactory
|
||||
|
||||
await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
|
||||
|
||||
// if it's pet then it's summoner, if it's summoner we actually want to keep all filereplacements alive at all times
|
||||
// or we get into redraw city for every change and nothing works properly
|
||||
if (objectKind == ObjectKind.Pet)
|
||||
{
|
||||
foreach (var item in fragment.FileReplacements.Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
|
||||
@@ -189,10 +185,8 @@ public class PlayerDataFactory
|
||||
|
||||
_logger.LogDebug("Handling transient update for {obj}", playerRelatedObject);
|
||||
|
||||
// remove all potentially gathered paths from the transient resource manager that are resolved through static resolving
|
||||
_transientResourceManager.ClearTransientPaths(objectKind, fragment.FileReplacements.SelectMany(c => c.GamePaths).ToList());
|
||||
_transientResourceManager.ClearTransientPaths(objectKind, [.. fragment.FileReplacements.SelectMany(c => c.GamePaths)]);
|
||||
|
||||
// get all remaining paths and resolve them
|
||||
var transientPaths = ManageSemiTransientData(objectKind);
|
||||
var resolvedTransientPaths = await GetFileReplacementsFromPaths(playerRelatedObject, transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
||||
|
||||
@@ -213,12 +207,10 @@ public class PlayerDataFactory
|
||||
}
|
||||
}
|
||||
|
||||
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
|
||||
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]);
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
// make sure we only return data that actually has file replacements
|
||||
fragment.FileReplacements = new HashSet<FileReplacement>(fragment.FileReplacements.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance);
|
||||
|
||||
// gather up data from ipc
|
||||
@@ -270,13 +262,17 @@ public class PlayerDataFactory
|
||||
|
||||
Dictionary<string, List<ushort>>? boneIndices = null;
|
||||
var hasPapFiles = false;
|
||||
|
||||
if (objectKind == ObjectKind.Player)
|
||||
{
|
||||
hasPapFiles = fragment.FileReplacements.Any(f =>
|
||||
!f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (hasPapFiles)
|
||||
{
|
||||
boneIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
|
||||
boneIndices = await _dalamudUtil
|
||||
.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,9 +280,16 @@ public class PlayerDataFactory
|
||||
{
|
||||
try
|
||||
{
|
||||
#if DEBUG
|
||||
if (hasPapFiles && boneIndices != null)
|
||||
{
|
||||
_modelAnalyzer.DumpLocalSkeletonIndices(playerRelatedObject);
|
||||
}
|
||||
#endif
|
||||
if (hasPapFiles)
|
||||
{
|
||||
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
|
||||
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
@@ -305,74 +308,174 @@ public class PlayerDataFactory
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private async Task VerifyPlayerAnimationBones(Dictionary<string, List<ushort>>? boneIndices, CharacterDataFragmentPlayer fragment, CancellationToken ct)
|
||||
private async Task VerifyPlayerAnimationBones(
|
||||
Dictionary<string, List<ushort>>? playerBoneIndices,
|
||||
CharacterDataFragmentPlayer fragment,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (boneIndices == null) return;
|
||||
if (playerBoneIndices == null || playerBoneIndices.Count == 0)
|
||||
return;
|
||||
|
||||
var playerBoneSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var (rawLocalKey, indices) in playerBoneIndices)
|
||||
{
|
||||
if (indices == null || indices.Count == 0)
|
||||
continue;
|
||||
var key = XivDataAnalyzer.CanonicalizeSkeletonKey(rawLocalKey);
|
||||
if (string.IsNullOrEmpty(key))
|
||||
continue;
|
||||
|
||||
if (!playerBoneSets.TryGetValue(key, out var set))
|
||||
playerBoneSets[key] = set = new HashSet<ushort>();
|
||||
|
||||
foreach (var idx in indices)
|
||||
set.Add(idx);
|
||||
}
|
||||
|
||||
if (playerBoneSets.Count == 0)
|
||||
return;
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
foreach (var kvp in boneIndices)
|
||||
foreach (var kvp in playerBoneSets)
|
||||
{
|
||||
_logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
|
||||
_logger.LogDebug(
|
||||
"Found local skeleton bucket '{bucket}' ({count} indices, max {max})",
|
||||
kvp.Key,
|
||||
kvp.Value.Count,
|
||||
kvp.Value.Count > 0 ? kvp.Value.Max() : 0);
|
||||
}
|
||||
}
|
||||
|
||||
var maxPlayerBoneIndex = boneIndices.SelectMany(kvp => kvp.Value).DefaultIfEmpty().Max();
|
||||
if (maxPlayerBoneIndex <= 0) return;
|
||||
var papFiles = fragment.FileReplacements
|
||||
.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
if (papFiles.Count == 0)
|
||||
return;
|
||||
|
||||
var papGroupsByHash = papFiles
|
||||
.Where(f => !string.IsNullOrEmpty(f.Hash))
|
||||
.GroupBy(f => f.Hash, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
int noValidationFailed = 0;
|
||||
foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
|
||||
|
||||
static ushort MaxIndex(List<ushort> list)
|
||||
{
|
||||
if (list == null || list.Count == 0) return 0;
|
||||
ushort max = 0;
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
if (list[i] > max) max = list[i];
|
||||
return max;
|
||||
}
|
||||
|
||||
static bool ShouldIgnorePap(Dictionary<string, List<ushort>> pap)
|
||||
{
|
||||
foreach (var kv in pap)
|
||||
{
|
||||
if (kv.Value == null || kv.Value.Count == 0)
|
||||
continue;
|
||||
|
||||
if (MaxIndex(kv.Value) > 105)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var group in papGroupsByHash)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var skeletonIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetBoneIndicesFromPap(file.Hash)).ConfigureAwait(false);
|
||||
bool validationFailed = false;
|
||||
if (skeletonIndices != null)
|
||||
var hash = group.Key;
|
||||
|
||||
Dictionary<string, List<ushort>>? papSkeletonIndices;
|
||||
|
||||
await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// 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);
|
||||
papSkeletonIndices = await Task.Run(() => _modelAnalyzer.GetBoneIndicesFromPap(hash), ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_papParseLimiter.Release();
|
||||
}
|
||||
|
||||
if (papSkeletonIndices == null || papSkeletonIndices.Count == 0)
|
||||
continue;
|
||||
|
||||
if (ShouldIgnorePap(papSkeletonIndices))
|
||||
{
|
||||
_logger.LogTrace("All indices of PAP hash {hash} are <= 105, ignoring", hash);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool invalid = false;
|
||||
string? reason = null;
|
||||
|
||||
foreach (var (rawPapName, usedIndices) in papSkeletonIndices)
|
||||
{
|
||||
var papKey = XivDataAnalyzer.CanonicalizeSkeletonKey(rawPapName);
|
||||
if (string.IsNullOrEmpty(papKey))
|
||||
continue;
|
||||
|
||||
if (!playerBoneSets.TryGetValue(papKey, out var available))
|
||||
{
|
||||
invalid = true;
|
||||
reason = $"Missing skeleton bucket '{papKey}' (raw '{rawPapName}') on local player.";
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
|
||||
|
||||
foreach (var boneCount in skeletonIndices)
|
||||
for (int i = 0; i < usedIndices.Count; i++)
|
||||
{
|
||||
var maxAnimationIndex = boneCount.Value.DefaultIfEmpty().Max();
|
||||
if (maxAnimationIndex > maxPlayerBoneIndex)
|
||||
var idx = usedIndices[i];
|
||||
if (!available.Contains(idx))
|
||||
{
|
||||
_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, maxAnimationIndex, maxPlayerBoneIndex);
|
||||
validationFailed = true;
|
||||
invalid = true;
|
||||
reason = $"Skeleton '{papKey}' missing bone index {idx} (raw '{rawPapName}').";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (invalid)
|
||||
break;
|
||||
}
|
||||
|
||||
if (validationFailed)
|
||||
if (!invalid)
|
||||
continue;
|
||||
|
||||
noValidationFailed++;
|
||||
|
||||
_logger.LogWarning(
|
||||
"Animation PAP hash {hash} is not compatible with local skeletons; dropping all mappings for this hash. Reason: {reason}",
|
||||
hash,
|
||||
reason);
|
||||
|
||||
foreach (var file in group.ToList())
|
||||
{
|
||||
noValidationFailed++;
|
||||
_logger.LogDebug("Removing {file} from sent file replacements and transient data", file.ResolvedPath);
|
||||
fragment.FileReplacements.Remove(file);
|
||||
foreach (var gamePath in file.GamePaths)
|
||||
{
|
||||
_transientResourceManager.RemoveTransientResource(ObjectKind.Player, gamePath);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var gamePath in file.GamePaths)
|
||||
_transientResourceManager.RemoveTransientResource(ObjectKind.Player, gamePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (noValidationFailed > 0)
|
||||
{
|
||||
_lightlessMediator.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)));
|
||||
_lightlessMediator.Publish(new NotificationMessage(
|
||||
"Invalid Skeleton Setup",
|
||||
$"Your client is attempting to send {noValidationFailed} animation file groups with bone indices not present on your current skeleton. " +
|
||||
"Those animation files have been removed from your sent data. Verify that you are using the correct skeleton for those animations " +
|
||||
"(Check /xllog for more information).",
|
||||
NotificationType.Warning,
|
||||
TimeSpan.FromSeconds(10)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(GameObjectHandler handler, HashSet<string> forwardResolve, HashSet<string> reverseResolve)
|
||||
{
|
||||
var forwardPaths = forwardResolve.ToArray();
|
||||
|
||||
@@ -22,6 +22,7 @@ using Microsoft.Extensions.Logging;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
@@ -46,6 +47,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
private readonly TextureDownscaleService _textureDownscaleService;
|
||||
private readonly PairStateCache _pairStateCache;
|
||||
private readonly PairPerformanceMetricsCache _performanceMetricsCache;
|
||||
private readonly XivDataAnalyzer _modelAnalyzer;
|
||||
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
||||
private readonly PairManager _pairManager;
|
||||
private CancellationTokenSource? _applicationCancellationTokenSource;
|
||||
@@ -90,6 +92,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
".avfx",
|
||||
".scd"
|
||||
};
|
||||
private readonly ConcurrentDictionary<string, byte> _blockedPapHashes = new(StringComparer.OrdinalIgnoreCase);
|
||||
private DateTime? _invisibleSinceUtc;
|
||||
private DateTime? _visibilityEvictionDueAtUtc;
|
||||
private DateTime _nextActorLookupUtc = DateTime.MinValue;
|
||||
@@ -184,7 +187,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
TextureDownscaleService textureDownscaleService,
|
||||
PairStateCache pairStateCache,
|
||||
PairPerformanceMetricsCache performanceMetricsCache,
|
||||
PenumbraTempCollectionJanitor tempCollectionJanitor) : base(logger, mediator)
|
||||
PenumbraTempCollectionJanitor tempCollectionJanitor,
|
||||
XivDataAnalyzer modelAnalyzer) : base(logger, mediator)
|
||||
{
|
||||
_pairManager = pairManager;
|
||||
Ident = ident;
|
||||
@@ -203,6 +207,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_pairStateCache = pairStateCache;
|
||||
_performanceMetricsCache = performanceMetricsCache;
|
||||
_tempCollectionJanitor = tempCollectionJanitor;
|
||||
_modelAnalyzer = modelAnalyzer;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
@@ -1669,11 +1674,36 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
return;
|
||||
}
|
||||
|
||||
SplitPapMappings(moddedPaths, out var withoutPap, out var papOnly);
|
||||
|
||||
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, penumbraCollection, objIndex.Value).ConfigureAwait(false);
|
||||
|
||||
await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, penumbraCollection,
|
||||
moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false);
|
||||
_lastAppliedModdedPaths = new Dictionary<(string GamePath, string? Hash), string>(moddedPaths, moddedPaths.Comparer);
|
||||
await _ipcManager.Penumbra.SetTemporaryModsAsync(
|
||||
Logger, _applicationId, penumbraCollection,
|
||||
withoutPap.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, handlerForApply, _applicationId, token).ConfigureAwait(false);
|
||||
if (handlerForApply.Address != nint.Zero)
|
||||
await _actorObjectService.WaitForFullyLoadedAsync(handlerForApply.Address, token).ConfigureAwait(false);
|
||||
|
||||
var removedPap = await StripIncompatiblePapAsync(handlerForApply, charaData, papOnly, token).ConfigureAwait(false);
|
||||
if (removedPap > 0)
|
||||
{
|
||||
Logger.LogTrace("[{applicationId}] Removed {removedPap} incompatible PAP mappings found for {handler}", _applicationId, removedPap, GetLogIdentifier());
|
||||
}
|
||||
|
||||
var merged = new Dictionary<(string GamePath, string? Hash), string>(withoutPap, withoutPap.Comparer);
|
||||
foreach (var kv in papOnly)
|
||||
merged[kv.Key] = kv.Value;
|
||||
|
||||
await _ipcManager.Penumbra.SetTemporaryModsAsync(
|
||||
Logger, _applicationId, penumbraCollection,
|
||||
merged.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_lastAppliedModdedPaths = new Dictionary<(string GamePath, string? Hash), string>(merged, merged.Comparer);
|
||||
|
||||
LastAppliedDataBytes = -1;
|
||||
foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists))
|
||||
{
|
||||
@@ -1983,9 +2013,17 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
|
||||
foreach (var gamePath in item.GamePaths)
|
||||
{
|
||||
if (gamePath.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.IsNullOrEmpty(item.Hash)
|
||||
&& _blockedPapHashes.ContainsKey(item.Hash))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var preferredPath = skipDownscaleForPair
|
||||
? fileCache.ResolvedFilepath
|
||||
: _textureDownscaleService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath);
|
||||
|
||||
outputDict[(gamePath, item.Hash)] = preferredPath;
|
||||
}
|
||||
}
|
||||
@@ -2295,7 +2333,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
HandleVisibilityLoss(logChange: false);
|
||||
}
|
||||
|
||||
private bool TryResolveDescriptorHash(ActorObjectService.ActorDescriptor descriptor, out string hashedCid)
|
||||
private static bool TryResolveDescriptorHash(ActorObjectService.ActorDescriptor descriptor, out string hashedCid)
|
||||
{
|
||||
hashedCid = descriptor.HashedContentId ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(hashedCid))
|
||||
@@ -2308,6 +2346,194 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
return !string.IsNullOrEmpty(hashedCid);
|
||||
}
|
||||
|
||||
private static bool ContainsIndexCompat(HashSet<ushort> available, ushort idx)
|
||||
{
|
||||
if (available.Contains(idx)) return true;
|
||||
|
||||
if (idx > 0 && available.Contains((ushort)(idx - 1))) return true;
|
||||
if (idx < ushort.MaxValue && available.Contains((ushort)(idx + 1))) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPapCompatible(
|
||||
IReadOnlyDictionary<string, HashSet<ushort>> localBoneSets,
|
||||
IReadOnlyDictionary<string, List<ushort>> papBoneIndices,
|
||||
out string reason)
|
||||
{
|
||||
var groups = papBoneIndices
|
||||
.Select(kvp => new
|
||||
{
|
||||
Raw = kvp.Key,
|
||||
Key = XivDataAnalyzer.CanonicalizeSkeletonKey(kvp.Key),
|
||||
Indices = kvp.Value
|
||||
})
|
||||
.Where(x => !string.IsNullOrEmpty(x.Key) && x.Indices is { Count: > 0 })
|
||||
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (groups.Count == 0)
|
||||
{
|
||||
reason = "No bindings found in the PAP";
|
||||
return false;
|
||||
}
|
||||
|
||||
var relevant = groups.Where(g => localBoneSets.ContainsKey(g.Key)).ToList();
|
||||
|
||||
if (relevant.Count == 0)
|
||||
{
|
||||
var papKeys = string.Join(", ", groups.Select(g => g.Key).Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
var localKeys = string.Join(", ", localBoneSets.Keys);
|
||||
reason = $"No matching skeleton bucket between PAP [{papKeys}] and local [{localKeys}].";
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var g in relevant)
|
||||
{
|
||||
var available = localBoneSets[g.Key];
|
||||
|
||||
bool anyVariantOk = false;
|
||||
foreach (var variant in g)
|
||||
{
|
||||
bool ok = true;
|
||||
foreach (var idx in variant.Indices)
|
||||
{
|
||||
if (!ContainsIndexCompat(available, idx))
|
||||
{
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok)
|
||||
{
|
||||
anyVariantOk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyVariantOk)
|
||||
{
|
||||
var first = g.First();
|
||||
ushort? missing = null;
|
||||
foreach (var idx in first.Indices)
|
||||
{
|
||||
if (!ContainsIndexCompat(available, idx))
|
||||
{
|
||||
missing = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
reason = missing.HasValue
|
||||
? $"Skeleton '{g.Key}' missing bone index {missing.Value}. (raw '{first.Raw}')"
|
||||
: $"Skeleton '{g.Key}' missing required bone indices. (raw '{first.Raw}')";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
reason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void SplitPapMappings(
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths,
|
||||
out Dictionary<(string GamePath, string? Hash), string> withoutPap,
|
||||
out Dictionary<(string GamePath, string? Hash), string> papOnly)
|
||||
{
|
||||
withoutPap = new(moddedPaths.Comparer);
|
||||
papOnly = new(moddedPaths.Comparer);
|
||||
|
||||
foreach (var kv in moddedPaths)
|
||||
{
|
||||
var gamePath = kv.Key.GamePath;
|
||||
if (gamePath.EndsWith(".pap", StringComparison.OrdinalIgnoreCase))
|
||||
papOnly[kv.Key] = kv.Value;
|
||||
else
|
||||
withoutPap[kv.Key] = kv.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int> StripIncompatiblePapAsync(
|
||||
GameObjectHandler handlerForApply,
|
||||
CharacterData charaData,
|
||||
Dictionary<(string GamePath, string? Hash), string> papOnly,
|
||||
CancellationToken token)
|
||||
{
|
||||
if (papOnly.Count == 0)
|
||||
return 0;
|
||||
|
||||
var boneIndices = await _dalamudUtil.RunOnFrameworkThread(
|
||||
() => _modelAnalyzer.GetSkeletonBoneIndices(handlerForApply))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (boneIndices == null || boneIndices.Count == 0)
|
||||
return papOnly.Count;
|
||||
|
||||
var localBoneSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var (rawKey, list) in boneIndices)
|
||||
{
|
||||
var key = XivDataAnalyzer.CanonicalizeSkeletonKey(rawKey);
|
||||
if (string.IsNullOrEmpty(key) || list is null || list.Count == 0)
|
||||
continue;
|
||||
|
||||
if (!localBoneSets.TryGetValue(key, out var set))
|
||||
localBoneSets[key] = set = new HashSet<ushort>();
|
||||
|
||||
foreach (var v in list)
|
||||
set.Add(v);
|
||||
}
|
||||
|
||||
int removed = 0;
|
||||
|
||||
foreach (var hash in papOnly.Keys.Select(k => k.Hash).Where(h => !string.IsNullOrEmpty(h)).Distinct(StringComparer.OrdinalIgnoreCase).ToList())
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var papIndices = await _dalamudUtil.RunOnFrameworkThread(
|
||||
() => _modelAnalyzer.GetBoneIndicesFromPap(hash!))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (papIndices == null || papIndices.Count == 0)
|
||||
continue;
|
||||
|
||||
if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105))
|
||||
continue;
|
||||
|
||||
if (!IsPapCompatible(localBoneSets, papIndices, out var reason))
|
||||
{
|
||||
var keysToRemove = papOnly.Keys.Where(k => string.Equals(k.Hash, hash, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
foreach (var k in keysToRemove)
|
||||
papOnly.Remove(k);
|
||||
|
||||
removed += keysToRemove.Count;
|
||||
if (hash == null)
|
||||
continue;
|
||||
|
||||
if (_blockedPapHashes.TryAdd(hash, 0))
|
||||
{
|
||||
Logger.LogWarning("Blocked remote object PAP (hash {hash}) for {handler}: {reason}", hash, GetLogIdentifier(), reason);
|
||||
}
|
||||
|
||||
if (charaData.FileReplacements.TryGetValue(ObjectKind.Player, out var list))
|
||||
{
|
||||
list.RemoveAll(r => string.Equals(r.Hash, hash, StringComparison.OrdinalIgnoreCase)
|
||||
&& r.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var nullHashKeys = papOnly.Keys.Where(k => string.IsNullOrEmpty(k.Hash)).ToList();
|
||||
foreach (var k in nullHashKeys)
|
||||
{
|
||||
papOnly.Remove(k);
|
||||
removed++;
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind)
|
||||
{
|
||||
_customizeIds[kind] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);
|
||||
|
||||
@@ -32,6 +32,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
||||
private readonly PairStateCache _pairStateCache;
|
||||
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
||||
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
||||
private readonly XivDataAnalyzer _modelAnalyzer;
|
||||
|
||||
public PairHandlerAdapterFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
@@ -50,7 +51,8 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
||||
TextureDownscaleService textureDownscaleService,
|
||||
PairStateCache pairStateCache,
|
||||
PairPerformanceMetricsCache pairPerformanceMetricsCache,
|
||||
PenumbraTempCollectionJanitor tempCollectionJanitor)
|
||||
PenumbraTempCollectionJanitor tempCollectionJanitor,
|
||||
XivDataAnalyzer modelAnalyzer)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_mediator = mediator;
|
||||
@@ -69,6 +71,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
||||
_pairStateCache = pairStateCache;
|
||||
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
||||
_tempCollectionJanitor = tempCollectionJanitor;
|
||||
_modelAnalyzer = modelAnalyzer;
|
||||
}
|
||||
|
||||
public IPairHandlerAdapter Create(string ident)
|
||||
@@ -95,6 +98,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
||||
_textureDownscaleService,
|
||||
_pairStateCache,
|
||||
_pairPerformanceMetricsCache,
|
||||
_tempCollectionJanitor);
|
||||
_tempCollectionJanitor,
|
||||
_modelAnalyzer);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user