Testing PAP handling changes.

This commit is contained in:
cake
2026-01-02 03:56:59 +01:00
parent 7e61954541
commit d6fe09ba8e
7 changed files with 708 additions and 169 deletions

View File

@@ -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);