Compare commits

..

4 Commits

Author SHA1 Message Date
defnotken
de2b537fb1 bump
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m7s
2025-12-27 22:03:19 -06:00
defnotken
838f4d1b1b Merge branch '2.0.2' into dev 2025-12-27 22:02:45 -06:00
defnotken
3a838077ac Merge branch '2.0.2' into dev
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m10s
2025-12-27 21:40:07 -06:00
defnotken
fe9122e0d2 build out dev 2025-12-27 20:50:09 -06:00
29 changed files with 511 additions and 1931 deletions

View File

@@ -27,7 +27,6 @@ public sealed class FileCacheManager : IHostedService
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, FileCacheEntity> _fileCachesByPrefixedPath = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, FileCacheEntity> _fileCachesByPrefixedPath = new(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1); private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1);
private readonly SemaphoreSlim _evictSemaphore = new(1, 1);
private readonly Lock _fileWriteLock = new(); private readonly Lock _fileWriteLock = new();
private readonly IpcManager _ipcManager; private readonly IpcManager _ipcManager;
private readonly ILogger<FileCacheManager> _logger; private readonly ILogger<FileCacheManager> _logger;
@@ -227,23 +226,13 @@ public sealed class FileCacheManager : IHostedService
var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length); var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length);
var tmpPath = compressedPath + ".tmp"; var tmpPath = compressedPath + ".tmp";
try await File.WriteAllBytesAsync(tmpPath, compressed, token).ConfigureAwait(false);
{ File.Move(tmpPath, compressedPath, overwrite: true);
await File.WriteAllBytesAsync(tmpPath, compressed, token).ConfigureAwait(false);
File.Move(tmpPath, compressedPath, overwrite: true);
}
finally
{
try { if (File.Exists(tmpPath)) File.Delete(tmpPath); } catch { /* ignore */ }
}
var compressedSize = new FileInfo(compressedPath).Length; var compressedSize = compressed.LongLength;
SetSizeInfo(hash, originalSize, compressedSize); SetSizeInfo(hash, originalSize, compressedSize);
UpdateEntitiesSizes(hash, originalSize, compressedSize); UpdateEntitiesSizes(hash, originalSize, compressedSize);
var maxBytes = GiBToBytes(_configService.Current.MaxLocalCacheInGiB);
await EnforceCacheLimitAsync(maxBytes, token).ConfigureAwait(false);
return compressed; return compressed;
} }
finally finally
@@ -888,83 +877,6 @@ public sealed class FileCacheManager : IHostedService
}, token).ConfigureAwait(false); }, token).ConfigureAwait(false);
} }
private async Task EnforceCacheLimitAsync(long maxBytes, CancellationToken token)
{
if (string.IsNullOrWhiteSpace(CacheFolder) || maxBytes <= 0) return;
await _evictSemaphore.WaitAsync(token).ConfigureAwait(false);
try
{
Directory.CreateDirectory(CacheFolder);
foreach (var tmp in Directory.EnumerateFiles(CacheFolder, "*" + _compressedCacheExtension + ".tmp"))
{
try { File.Delete(tmp); } catch { /* ignore */ }
}
var files = Directory.EnumerateFiles(CacheFolder, "*" + _compressedCacheExtension, SearchOption.TopDirectoryOnly)
.Select(p => new FileInfo(p))
.Where(fi => fi.Exists)
.OrderBy(fi => fi.LastWriteTimeUtc)
.ToList();
long total = files.Sum(f => f.Length);
if (total <= maxBytes) return;
foreach (var fi in files)
{
token.ThrowIfCancellationRequested();
if (total <= maxBytes) break;
var hash = Path.GetFileNameWithoutExtension(fi.Name);
try
{
var len = fi.Length;
fi.Delete();
total -= len;
_sizeCache.TryRemove(hash, out _);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to evict cache file {file}", fi.FullName);
}
}
}
finally
{
_evictSemaphore.Release();
}
}
private static long GiBToBytes(double gib)
{
if (double.IsNaN(gib) || double.IsInfinity(gib) || gib <= 0)
return 0;
var bytes = gib * 1024d * 1024d * 1024d;
if (bytes >= long.MaxValue) return long.MaxValue;
return (long)Math.Round(bytes, MidpointRounding.AwayFromZero);
}
private void CleanupOrphanCompressedCache()
{
if (string.IsNullOrWhiteSpace(CacheFolder) || !Directory.Exists(CacheFolder))
return;
foreach (var path in Directory.EnumerateFiles(CacheFolder, "*" + _compressedCacheExtension))
{
var hash = Path.GetFileNameWithoutExtension(path);
if (!_fileCaches.ContainsKey(hash))
{
try { File.Delete(path); }
catch (Exception ex) { _logger.LogWarning(ex, "Failed deleting orphan {file}", path); }
}
}
}
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
_logger.LogInformation("Starting FileCacheManager"); _logger.LogInformation("Starting FileCacheManager");
@@ -1148,8 +1060,6 @@ public sealed class FileCacheManager : IHostedService
{ {
await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false); await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false);
} }
CleanupOrphanCompressedCache();
} }
_logger.LogInformation("Started FileCacheManager"); _logger.LogInformation("Started FileCacheManager");

View File

@@ -297,7 +297,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
private void DalamudUtil_FrameworkUpdate() private void DalamudUtil_FrameworkUpdate()
{ {
_ = Task.Run(() => RefreshPlayerRelatedAddressMap()); RefreshPlayerRelatedAddressMap();
lock (_cacheAdditionLock) lock (_cacheAdditionLock)
{ {
@@ -306,64 +306,20 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
if (_lastClassJobId != _dalamudUtil.ClassJobId) if (_lastClassJobId != _dalamudUtil.ClassJobId)
{ {
UpdateClassJobCache(); _lastClassJobId = _dalamudUtil.ClassJobId;
} if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet<string>? value))
CleanupAbsentObjects();
}
private void RefreshPlayerRelatedAddressMap()
{
var tempMap = new ConcurrentDictionary<nint, GameObjectHandler>();
var updatedFrameAddresses = new ConcurrentDictionary<nint, ObjectKind>();
lock (_playerRelatedLock)
{
foreach (var handler in _playerRelatedPointers)
{ {
var address = (nint)handler.Address; value?.Clear();
if (address != nint.Zero)
{
tempMap[address] = handler;
updatedFrameAddresses[address] = handler.ObjectKind;
}
} }
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.OrdinalIgnoreCase);
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
SemiTransientResources[ObjectKind.Pet] = new HashSet<string>(
petSpecificData ?? [],
StringComparer.OrdinalIgnoreCase);
} }
_playerRelatedByAddress.Clear();
foreach (var kvp in tempMap)
{
_playerRelatedByAddress[kvp.Key] = kvp.Value;
}
_cachedFrameAddresses.Clear();
foreach (var kvp in updatedFrameAddresses)
{
_cachedFrameAddresses[kvp.Key] = kvp.Value;
}
}
private void UpdateClassJobCache()
{
_lastClassJobId = _dalamudUtil.ClassJobId;
if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet<string>? value))
{
value?.Clear();
}
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache
.Concat(jobSpecificData ?? [])
.ToHashSet(StringComparer.OrdinalIgnoreCase);
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
SemiTransientResources[ObjectKind.Pet] = new HashSet<string>(
petSpecificData ?? [],
StringComparer.OrdinalIgnoreCase);
}
private void CleanupAbsentObjects()
{
foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast<ObjectKind>()) foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast<ObjectKind>())
{ {
if (!_cachedFrameAddresses.Any(k => k.Value == kind) && TransientResources.Remove(kind, out _)) if (!_cachedFrameAddresses.Any(k => k.Value == kind) && TransientResources.Remove(kind, out _))
@@ -393,6 +349,26 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
_semiTransientResources = null; _semiTransientResources = null;
} }
private void RefreshPlayerRelatedAddressMap()
{
_playerRelatedByAddress.Clear();
var updatedFrameAddresses = new ConcurrentDictionary<nint, ObjectKind>();
lock (_playerRelatedLock)
{
foreach (var handler in _playerRelatedPointers)
{
var address = (nint)handler.Address;
if (address != nint.Zero)
{
_playerRelatedByAddress[address] = handler;
updatedFrameAddresses[address] = handler.ObjectKind;
}
}
}
_cachedFrameAddresses = updatedFrameAddresses;
}
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor) private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
{ {
if (descriptor.IsInGpose) if (descriptor.IsInGpose)

View File

@@ -4,7 +4,6 @@ using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.UI; using LightlessSync.UI;
using LightlessSync.UI.Models; using LightlessSync.UI.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using LightlessSync.PlayerData.Factories;
namespace LightlessSync.LightlessConfiguration.Configurations; namespace LightlessSync.LightlessConfiguration.Configurations;
@@ -157,8 +156,4 @@ public class LightlessConfig : ILightlessConfiguration
public string? SelectedFinderSyncshell { get; set; } = null; public string? SelectedFinderSyncshell { get; set; } = null;
public string LastSeenVersion { get; set; } = string.Empty; public string LastSeenVersion { get; set; } = string.Empty;
public HashSet<Guid> OrphanableTempCollections { get; set; } = []; public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Safe;
public bool AnimationAllowOneBasedShift { get; set; } = true;
public bool AnimationAllowNeighborIndexTolerance { get; set; } = false;
} }

View File

@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<Authors></Authors> <Authors></Authors>
<Company></Company> <Company></Company>
<Version>2.0.3</Version> <Version>2.0.1.70</Version>
<Description></Description> <Description></Description>
<Copyright></Copyright> <Copyright></Copyright>
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl> <PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
@@ -37,7 +37,6 @@
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
<PackageReference Include="Glamourer.Api" Version="2.8.0" /> <PackageReference Include="Glamourer.Api" Version="2.8.0" />
<PackageReference Include="NReco.Logging.File" Version="1.3.1" /> <PackageReference Include="NReco.Logging.File" Version="1.3.1" />

View File

@@ -1,9 +0,0 @@
namespace LightlessSync.PlayerData.Factories
{
public enum AnimationValidationMode
{
Unsafe = 0,
Safe = 1,
Safest = 2,
}
}

View File

@@ -2,15 +2,12 @@
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc; using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models; using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Data; using LightlessSync.PlayerData.Data;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Diagnostics;
namespace LightlessSync.PlayerData.Factories; namespace LightlessSync.PlayerData.Factories;
@@ -21,34 +18,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 LightlessConfigService _configService;
private readonly XivDataAnalyzer _modelAnalyzer; private readonly XivDataAnalyzer _modelAnalyzer;
private readonly LightlessMediator _lightlessMediator; private readonly LightlessMediator _lightlessMediator;
private readonly TransientResourceManager _transientResourceManager; private readonly TransientResourceManager _transientResourceManager;
private static readonly SemaphoreSlim _papParseLimiter = new(1, 1);
// Transient resolved entries threshold public PlayerDataFactory(ILogger<PlayerDataFactory> logger, DalamudUtilService dalamudUtil, IpcManager ipcManager,
private const int _maxTransientResolvedEntries = 1000; TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory,
PerformanceCollectorService performanceCollector, XivDataAnalyzer modelAnalyzer, LightlessMediator lightlessMediator)
// Character build caches
private readonly ConcurrentDictionary<nint, Task<CharacterDataFragment>> _characterBuildInflight = new();
private readonly ConcurrentDictionary<nint, CacheEntry> _characterBuildCache = new();
// Time out thresholds
private static readonly TimeSpan _characterCacheTtl = TimeSpan.FromMilliseconds(750);
private static readonly TimeSpan _softReturnIfBusyAfter = TimeSpan.FromMilliseconds(250);
private static readonly TimeSpan _hardBuildTimeout = TimeSpan.FromSeconds(30);
public PlayerDataFactory(
ILogger<PlayerDataFactory> logger,
DalamudUtilService dalamudUtil,
IpcManager ipcManager,
TransientResourceManager transientResourceManager,
FileCacheManager fileReplacementFactory,
PerformanceCollectorService performanceCollector,
XivDataAnalyzer modelAnalyzer,
LightlessMediator lightlessMediator,
LightlessConfigService configService)
{ {
_logger = logger; _logger = logger;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
@@ -58,15 +34,15 @@ public class PlayerDataFactory
_performanceCollector = performanceCollector; _performanceCollector = performanceCollector;
_modelAnalyzer = modelAnalyzer; _modelAnalyzer = modelAnalyzer;
_lightlessMediator = lightlessMediator; _lightlessMediator = lightlessMediator;
_configService = configService;
_logger.LogTrace("Creating {this}", nameof(PlayerDataFactory)); _logger.LogTrace("Creating {this}", nameof(PlayerDataFactory));
} }
private sealed record CacheEntry(CharacterDataFragment Fragment, DateTime CreatedUtc);
public async Task<CharacterDataFragment?> BuildCharacterData(GameObjectHandler playerRelatedObject, CancellationToken token) public async Task<CharacterDataFragment?> BuildCharacterData(GameObjectHandler playerRelatedObject, CancellationToken token)
{ {
if (!_ipcManager.Initialized) if (!_ipcManager.Initialized)
{
throw new InvalidOperationException("Penumbra or Glamourer is not connected"); throw new InvalidOperationException("Penumbra or Glamourer is not connected");
}
if (playerRelatedObject == null) return null; if (playerRelatedObject == null) return null;
@@ -91,17 +67,16 @@ public class PlayerDataFactory
if (pointerIsZero) if (pointerIsZero)
{ {
_logger.LogTrace("Pointer was zero for {objectKind}; couldn't build character", playerRelatedObject.ObjectKind); _logger.LogTrace("Pointer was zero for {objectKind}", playerRelatedObject.ObjectKind);
return null; return null;
} }
try try
{ {
return await _performanceCollector.LogPerformance( return await _performanceCollector.LogPerformance(this, $"CreateCharacterData>{playerRelatedObject.ObjectKind}", async () =>
this, {
$"CreateCharacterData>{playerRelatedObject.ObjectKind}", return await CreateCharacterData(playerRelatedObject, token).ConfigureAwait(false);
async () => await CreateCharacterData(playerRelatedObject, token).ConfigureAwait(false) }).ConfigureAwait(true);
).ConfigureAwait(false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -117,14 +92,17 @@ public class PlayerDataFactory
} }
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer) private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
=> await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false); {
return await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false);
}
private unsafe static bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer) private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
{ {
if (playerPointer == IntPtr.Zero) if (playerPointer == IntPtr.Zero)
return true; return true;
var character = (Character*)playerPointer; var character = (Character*)playerPointer;
if (character == null) if (character == null)
return true; return true;
@@ -135,167 +113,93 @@ public class PlayerDataFactory
return gameObject->DrawObject == null; return gameObject->DrawObject == null;
} }
private static bool IsCacheFresh(CacheEntry entry) private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;
private Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
=> CreateCharacterDataCoalesced(playerRelatedObject, ct);
private async Task<CharacterDataFragment> CreateCharacterDataCoalesced(GameObjectHandler obj, CancellationToken ct)
{
var key = obj.Address;
if (_characterBuildCache.TryGetValue(key, out var cached) && IsCacheFresh(cached) && !_characterBuildInflight.ContainsKey(key))
return cached.Fragment;
var buildTask = _characterBuildInflight.GetOrAdd(key, _ => BuildAndCacheAsync(obj, key));
if (_characterBuildCache.TryGetValue(key, out cached))
{
var completed = await Task.WhenAny(buildTask, Task.Delay(_softReturnIfBusyAfter, ct)).ConfigureAwait(false);
if (completed != buildTask && (DateTime.UtcNow - cached.CreatedUtc) <= TimeSpan.FromSeconds(5))
{
return cached.Fragment;
}
}
return await WithCancellation(buildTask, ct).ConfigureAwait(false);
}
private async Task<CharacterDataFragment> BuildAndCacheAsync(GameObjectHandler obj, nint key)
{
try
{
using var cts = new CancellationTokenSource(_hardBuildTimeout);
var fragment = await CreateCharacterDataInternal(obj, cts.Token).ConfigureAwait(false);
_characterBuildCache[key] = new CacheEntry(fragment, DateTime.UtcNow);
PruneCharacterCacheIfNeeded();
return fragment;
}
finally
{
_characterBuildInflight.TryRemove(key, out _);
}
}
private void PruneCharacterCacheIfNeeded()
{
if (_characterBuildCache.Count < 2048) return;
var cutoff = DateTime.UtcNow - TimeSpan.FromSeconds(10);
foreach (var kv in _characterBuildCache)
{
if (kv.Value.CreatedUtc < cutoff)
_characterBuildCache.TryRemove(kv.Key, out _);
}
}
private static async Task<T> WithCancellation<T>(Task<T> task, CancellationToken ct)
=> await task.WaitAsync(ct).ConfigureAwait(false);
private async Task<CharacterDataFragment> CreateCharacterDataInternal(GameObjectHandler playerRelatedObject, CancellationToken ct)
{ {
var objectKind = playerRelatedObject.ObjectKind; var objectKind = playerRelatedObject.ObjectKind;
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new(); CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
var logDebug = _logger.IsEnabled(LogLevel.Debug);
var sw = Stopwatch.StartNew();
_logger.LogDebug("Building character data for {obj}", playerRelatedObject); _logger.LogDebug("Building character data for {obj}", playerRelatedObject);
var logDebug = _logger.IsEnabled(LogLevel.Debug);
await EnsureObjectPresentAsync(playerRelatedObject, ct).ConfigureAwait(false); // wait until chara is not drawing and present so nothing spontaneously explodes
ct.ThrowIfCancellationRequested(); await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
int totalWaitTime = 10000;
var waitRecordingTask = _transientResourceManager.WaitForRecording(ct); while (!await _dalamudUtil.IsObjectPresentAsync(await _dalamudUtil.CreateGameObjectAsync(playerRelatedObject.Address).ConfigureAwait(false)).ConfigureAwait(false) && totalWaitTime > 0)
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct)
.ConfigureAwait(false);
ct.ThrowIfCancellationRequested();
if (await CheckForNullDrawObject(playerRelatedObject.Address).ConfigureAwait(false))
throw new InvalidOperationException("DrawObject became null during build (actor despawned)");
Task<string> getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address);
Task<string?> getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address);
Task<string?>? getMoodlesData = null;
Task<string>? getHeelsOffset = null;
Task<string>? getHonorificTitle = null;
if (objectKind == ObjectKind.Player)
{ {
getHeelsOffset = _ipcManager.Heels.GetOffsetAsync(); _logger.LogTrace("Character is null but it shouldn't be, waiting");
getHonorificTitle = _ipcManager.Honorific.GetTitle(); await Task.Delay(50, ct).ConfigureAwait(false);
getMoodlesData = _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address); totalWaitTime -= 50;
} }
var resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false) ?? throw new InvalidOperationException("Penumbra returned null data; couldn't proceed with character");
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
var staticBuildTask = Task.Run(() => BuildStaticReplacements(resolvedPaths), ct); DateTime start = DateTime.UtcNow;
fragment.FileReplacements = await staticBuildTask.ConfigureAwait(false); // penumbra call, it's currently broken
Dictionary<string, HashSet<string>>? resolvedPaths;
resolvedPaths = (await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false));
if (resolvedPaths == null) throw new InvalidOperationException("Penumbra returned null data");
ct.ThrowIfCancellationRequested();
fragment.FileReplacements =
new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance)
.Where(p => p.HasFileReplacement).ToHashSet();
fragment.FileReplacements.RemoveWhere(c => c.GamePaths.Any(g => !CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
ct.ThrowIfCancellationRequested();
if (logDebug) if (logDebug)
{ {
_logger.LogDebug("== Static Replacements =="); _logger.LogDebug("== Static Replacements ==");
foreach (var replacement in fragment.FileReplacements foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
.Where(i => i.HasFileReplacement)
.OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
{ {
_logger.LogDebug("=> {repl}", replacement); _logger.LogDebug("=> {repl}", replacement);
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
} }
} }
else
var staticReplacements = new HashSet<FileReplacement>(fragment.FileReplacements, FileReplacementComparer.Instance);
var transientTask = ResolveTransientReplacementsAsync(
playerRelatedObject,
objectKind,
staticReplacements,
waitRecordingTask,
ct);
fragment.GlamourerString = await getGlamourerData.ConfigureAwait(false);
_logger.LogDebug("Glamourer is now: {data}", fragment.GlamourerString);
var customizeScale = await getCustomizeData.ConfigureAwait(false);
fragment.CustomizePlusScale = customizeScale ?? string.Empty;
_logger.LogDebug("Customize is now: {data}", fragment.CustomizePlusScale);
if (objectKind == ObjectKind.Player)
{ {
CharacterDataFragmentPlayer? playerFragment = fragment as CharacterDataFragmentPlayer ?? throw new InvalidOperationException("Failed to cast CharacterDataFragment to Player variant"); foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement))
{
ct.ThrowIfCancellationRequested();
}
}
playerFragment.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations(); await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
playerFragment.HonorificData = await getHonorificTitle!.ConfigureAwait(false);
_logger.LogDebug("Honorific is now: {data}", playerFragment!.HonorificData);
playerFragment.PetNamesData = _ipcManager.PetNames.GetLocalNames(); // if it's pet then it's summoner, if it's summoner we actually want to keep all filereplacements alive at all times
_logger.LogDebug("Pet Nicknames is now: {petnames}", playerFragment!.PetNamesData); // 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))
{
if (_transientResourceManager.AddTransientResource(objectKind, item))
{
_logger.LogDebug("Marking static {item} for Pet as transient", item);
}
}
playerFragment.HeelsData = await getHeelsOffset!.ConfigureAwait(false); _logger.LogTrace("Clearing {count} Static Replacements for Pet", fragment.FileReplacements.Count);
_logger.LogDebug("Heels is now: {heels}", playerFragment!.HeelsData); fragment.FileReplacements.Clear();
playerFragment.MoodlesData = (await getMoodlesData!.ConfigureAwait(false)) ?? string.Empty;
_logger.LogDebug("Moodles is now: {moodles}", playerFragment!.MoodlesData);
} }
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
var (resolvedTransientPaths, clearedForPet) = await transientTask.ConfigureAwait(false); _logger.LogDebug("Handling transient update for {obj}", playerRelatedObject);
if (clearedForPet != null)
fragment.FileReplacements.Clear(); // 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());
// get all remaining paths and resolve them
var transientPaths = ManageSemiTransientData(objectKind);
var resolvedTransientPaths = await GetFileReplacementsFromPaths(playerRelatedObject, transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
if (logDebug) if (logDebug)
{ {
_logger.LogDebug("== Transient Replacements =="); _logger.LogDebug("== Transient Replacements ==");
foreach (var replacement in resolvedTransientPaths foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
.Select(c => new FileReplacement([.. c.Value], c.Key))
.OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
{ {
_logger.LogDebug("=> {repl}", replacement); _logger.LogDebug("=> {repl}", replacement);
fragment.FileReplacements.Add(replacement); fragment.FileReplacements.Add(replacement);
@@ -304,64 +208,85 @@ public class PlayerDataFactory
else else
{ {
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key))) foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)))
{
fragment.FileReplacements.Add(replacement); fragment.FileReplacements.Add(replacement);
}
} }
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]); _transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]);
fragment.FileReplacements = new HashSet<FileReplacement>( ct.ThrowIfCancellationRequested();
fragment.FileReplacements
.Where(v => v.HasFileReplacement) // make sure we only return data that actually has file replacements
.OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), fragment.FileReplacements = new HashSet<FileReplacement>(fragment.FileReplacements.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance);
FileReplacementComparer.Instance);
// gather up data from ipc
Task<string> getHeelsOffset = _ipcManager.Heels.GetOffsetAsync();
Task<string> getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address);
Task<string?> getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address);
Task<string> getHonorificTitle = _ipcManager.Honorific.GetTitle();
fragment.GlamourerString = await getGlamourerData.ConfigureAwait(false);
_logger.LogDebug("Glamourer is now: {data}", fragment.GlamourerString);
var customizeScale = await getCustomizeData.ConfigureAwait(false);
fragment.CustomizePlusScale = customizeScale ?? string.Empty;
_logger.LogDebug("Customize is now: {data}", fragment.CustomizePlusScale);
if (objectKind == ObjectKind.Player)
{
var playerFragment = (fragment as CharacterDataFragmentPlayer)!;
playerFragment.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations();
playerFragment!.HonorificData = await getHonorificTitle.ConfigureAwait(false);
_logger.LogDebug("Honorific is now: {data}", playerFragment!.HonorificData);
playerFragment!.HeelsData = await getHeelsOffset.ConfigureAwait(false);
_logger.LogDebug("Heels is now: {heels}", playerFragment!.HeelsData);
playerFragment!.MoodlesData = await _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address).ConfigureAwait(false) ?? string.Empty;
_logger.LogDebug("Moodles is now: {moodles}", playerFragment!.MoodlesData);
playerFragment!.PetNamesData = _ipcManager.PetNames.GetLocalNames();
_logger.LogDebug("Pet Nicknames is now: {petnames}", playerFragment!.PetNamesData);
}
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
var toCompute = fragment.FileReplacements.Where(f => !f.IsFileSwap).ToArray(); var toCompute = fragment.FileReplacements.Where(f => !f.IsFileSwap).ToArray();
_logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length); _logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length);
var computedPaths = _fileCacheManager.GetFileCachesByPaths(toCompute.Select(c => c.ResolvedPath).ToArray());
await Task.Run(() => foreach (var file in toCompute)
{ {
var computedPaths = _fileCacheManager.GetFileCachesByPaths([.. toCompute.Select(c => c.ResolvedPath)]); ct.ThrowIfCancellationRequested();
foreach (var file in toCompute) file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty;
{ }
ct.ThrowIfCancellationRequested();
file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty;
}
}, ct).ConfigureAwait(false);
var removed = fragment.FileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash)); var removed = fragment.FileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash));
if (removed > 0) if (removed > 0)
{
_logger.LogDebug("Removed {amount} of invalid files", removed); _logger.LogDebug("Removed {amount} of invalid files", removed);
}
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
Dictionary<string, List<ushort>>? boneIndices = null; Dictionary<string, List<ushort>>? boneIndices = null;
var hasPapFiles = false; var hasPapFiles = false;
if (objectKind == ObjectKind.Player) if (objectKind == ObjectKind.Player)
{ {
hasPapFiles = fragment.FileReplacements.Any(f => hasPapFiles = fragment.FileReplacements.Any(f =>
!f.IsFileSwap && f.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase))); !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase));
if (hasPapFiles) if (hasPapFiles)
{ {
boneIndices = await _dalamudUtil boneIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject))
.ConfigureAwait(false);
} }
}
if (objectKind == ObjectKind.Player)
{
try try
{ {
#if DEBUG
if (hasPapFiles && boneIndices != null)
_modelAnalyzer.DumpLocalSkeletonIndices(playerRelatedObject);
#endif
if (hasPapFiles) if (hasPapFiles)
{ {
await VerifyPlayerAnimationBones(boneIndices, (CharacterDataFragmentPlayer)fragment, ct) await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
.ConfigureAwait(false);
} }
} }
catch (OperationCanceledException e) catch (OperationCanceledException e)
@@ -375,270 +300,105 @@ public class PlayerDataFactory
} }
} }
_logger.LogInformation("Building character data for {obj} took {time}ms", _logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(DateTime.UtcNow.Ticks - start.Ticks).TotalMilliseconds);
objectKind, sw.Elapsed.TotalMilliseconds);
return fragment; return fragment;
} }
private async Task EnsureObjectPresentAsync(GameObjectHandler handler, CancellationToken ct) private async Task VerifyPlayerAnimationBones(Dictionary<string, List<ushort>>? boneIndices, CharacterDataFragmentPlayer fragment, CancellationToken ct)
{ {
var remaining = 10000; if (boneIndices == null) return;
while (remaining > 0)
{
ct.ThrowIfCancellationRequested();
var obj = await _dalamudUtil.CreateGameObjectAsync(handler.Address).ConfigureAwait(false);
if (await _dalamudUtil.IsObjectPresentAsync(obj).ConfigureAwait(false))
return;
_logger.LogTrace("Character is null but it shouldn't be, waiting");
await Task.Delay(50, ct).ConfigureAwait(false);
remaining -= 50;
}
}
private static HashSet<FileReplacement> BuildStaticReplacements(Dictionary<string, HashSet<string>> resolvedPaths)
{
var set = new HashSet<FileReplacement>(FileReplacementComparer.Instance);
foreach (var kvp in resolvedPaths)
{
var fr = new FileReplacement([.. kvp.Value], kvp.Key);
if (!fr.HasFileReplacement) continue;
var allAllowed = fr.GamePaths.All(g =>
CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase)));
if (!allAllowed) continue;
set.Add(fr);
}
return set;
}
private async Task<(IReadOnlyDictionary<string, string[]> ResolvedPaths, HashSet<FileReplacement>? ClearedReplacements)>
ResolveTransientReplacementsAsync(
GameObjectHandler obj,
ObjectKind objectKind,
HashSet<FileReplacement> staticReplacements,
Task waitRecordingTask,
CancellationToken ct)
{
await waitRecordingTask.ConfigureAwait(false);
HashSet<FileReplacement>? clearedReplacements = null;
if (objectKind == ObjectKind.Pet)
{
foreach (var item in staticReplacements.Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
{
if (_transientResourceManager.AddTransientResource(objectKind, item))
_logger.LogDebug("Marking static {item} for Pet as transient", item);
}
_logger.LogTrace("Clearing {count} Static Replacements for Pet", staticReplacements.Count);
clearedReplacements = staticReplacements;
}
ct.ThrowIfCancellationRequested();
_transientResourceManager.ClearTransientPaths(objectKind, [.. staticReplacements.SelectMany(c => c.GamePaths)]);
var transientPaths = ManageSemiTransientData(objectKind);
if (transientPaths.Count == 0)
return (new Dictionary<string, string[]>(StringComparer.Ordinal), clearedReplacements);
var resolved = await GetFileReplacementsFromPaths(obj, transientPaths, new HashSet<string>(StringComparer.Ordinal))
.ConfigureAwait(false);
if (_maxTransientResolvedEntries > 0 && resolved.Count > _maxTransientResolvedEntries)
{
_logger.LogWarning("Transient entries ({resolved}) are above the threshold {max}; Please consider disable some mods (VFX have heavy load) to reduce transient load",
resolved.Count,
_maxTransientResolvedEntries);
}
return (resolved, clearedReplacements);
}
private async Task VerifyPlayerAnimationBones(
Dictionary<string, List<ushort>>? playerBoneIndices,
CharacterDataFragmentPlayer fragment,
CancellationToken ct)
{
var mode = _configService.Current.AnimationValidationMode;
var allowBasedShift = _configService.Current.AnimationAllowOneBasedShift;
var allownNightIndex = _configService.Current.AnimationAllowNeighborIndexTolerance;
if (mode == AnimationValidationMode.Unsafe)
return;
if (playerBoneIndices == null || playerBoneIndices.Count == 0)
return;
var localBoneSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
foreach (var (rawLocalKey, indices) in playerBoneIndices)
{
if (indices is not { Count: > 0 })
continue;
var key = XivDataAnalyzer.CanonicalizeSkeletonKey(rawLocalKey);
if (string.IsNullOrEmpty(key))
continue;
if (!localBoneSets.TryGetValue(key, out var set))
localBoneSets[key] = set = [];
foreach (var idx in indices)
set.Add(idx);
}
if (localBoneSets.Count == 0)
return;
if (_logger.IsEnabled(LogLevel.Debug)) if (_logger.IsEnabled(LogLevel.Debug))
{ {
_logger.LogDebug("SEND local buckets: {b}", foreach (var kvp in boneIndices)
string.Join(", ", localBoneSets.Keys.Order(StringComparer.Ordinal)));
foreach (var kvp in localBoneSets.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
{ {
var min = kvp.Value.Count > 0 ? kvp.Value.Min() : 0; _logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
var max = kvp.Value.Count > 0 ? kvp.Value.Max() : 0;
_logger.LogDebug("Local bucket {bucket}: count={count} min={min} max={max}",
kvp.Key, kvp.Value.Count, min, max);
} }
} }
var papGroups = fragment.FileReplacements var maxPlayerBoneIndex = boneIndices.SelectMany(kvp => kvp.Value).DefaultIfEmpty().Max();
.Where(f => !f.IsFileSwap if (maxPlayerBoneIndex <= 0) return;
&& !string.IsNullOrEmpty(f.Hash)
&& f.GamePaths is { Count: > 0 }
&& f.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
.GroupBy(f => f.Hash!, StringComparer.OrdinalIgnoreCase)
.ToList();
int noValidationFailed = 0; int noValidationFailed = 0;
foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
foreach (var g in papGroups)
{ {
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
var hash = g.Key; var skeletonIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetBoneIndicesFromPap(file.Hash)).ConfigureAwait(false);
bool validationFailed = false;
Dictionary<string, List<ushort>>? papIndices = null; if (skeletonIndices != null)
await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false);
try
{ {
papIndices = await Task.Run(() => _modelAnalyzer.GetBoneIndicesFromPap(hash), ct) // 105 is the maximum vanilla skellington spoopy bone index
.ConfigureAwait(false); if (skeletonIndices.All(k => k.Value.Max() <= 105))
} {
finally _logger.LogTrace("All indices of {path} are <= 105, ignoring", file.ResolvedPath);
{ continue;
_papParseLimiter.Release(); }
}
if (papIndices == null || papIndices.Count == 0) _logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
continue;
if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105)) foreach (var boneCount in skeletonIndices)
continue; {
var maxAnimationIndex = boneCount.Value.DefaultIfEmpty().Max();
if (_logger.IsEnabled(LogLevel.Debug)) if (maxAnimationIndex > maxPlayerBoneIndex)
{
var papBuckets = papIndices
.Select(kvp => new
{ {
Raw = kvp.Key, _logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})",
Key = XivDataAnalyzer.CanonicalizeSkeletonKey(kvp.Key), file.ResolvedPath, boneCount.Key, maxAnimationIndex, maxPlayerBoneIndex);
Indices = kvp.Value validationFailed = true;
}) break;
.Where(x => x.Indices is { Count: > 0 }) }
.GroupBy(x => string.IsNullOrEmpty(x.Key) ? x.Raw : x.Key!, StringComparer.OrdinalIgnoreCase) }
.Select(grp =>
{
var all = grp.SelectMany(v => v.Indices).ToList();
var min = all.Count > 0 ? all.Min() : 0;
var max = all.Count > 0 ? all.Max() : 0;
var raws = string.Join(',', grp.Select(v => v.Raw).Distinct(StringComparer.OrdinalIgnoreCase));
return $"{grp.Key}(min={min},max={max},raw=[{raws}])";
})
.ToList();
_logger.LogDebug("SEND pap buckets for hash={hash}: {b}",
hash,
string.Join(" | ", papBuckets));
} }
if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, allowBasedShift, allownNightIndex, out var reason)) if (validationFailed)
continue; {
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);
}
}
noValidationFailed++;
_logger.LogWarning(
"Animation PAP hash {hash} is not compatible with local skeletons; dropping all mappings for this hash. Reason: {reason}",
hash,
reason);
var removedGamePaths = fragment.FileReplacements
.Where(fr => !fr.IsFileSwap
&& string.Equals(fr.Hash, hash, StringComparison.OrdinalIgnoreCase)
&& fr.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
.SelectMany(fr => fr.GamePaths.Where(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
fragment.FileReplacements.RemoveWhere(fr =>
!fr.IsFileSwap
&& string.Equals(fr.Hash, hash, StringComparison.OrdinalIgnoreCase)
&& fr.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)));
foreach (var gp in removedGamePaths)
_transientResourceManager.RemoveTransientResource(ObjectKind.Player, gp);
} }
if (noValidationFailed > 0) if (noValidationFailed > 0)
{ {
_lightlessMediator.Publish(new NotificationMessage( _lightlessMediator.Publish(new NotificationMessage("Invalid Skeleton Setup",
"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. " +
$"Your client is attempting to send {noValidationFailed} animation files that don't match your current skeleton validation mode ({mode}). " + $"Verify that you are using the correct skeleton for those animation files (Check /xllog for more information).",
"Please adjust your skeleton/mods or change the validation mode if this is unexpected. " + NotificationType.Warning, TimeSpan.FromSeconds(10)));
"Those animation files have been removed from your sent (player) data. (Check /xllog for details).",
NotificationType.Warning,
TimeSpan.FromSeconds(10)));
} }
} }
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(GameObjectHandler handler, HashSet<string> forwardResolve, HashSet<string> reverseResolve)
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(
GameObjectHandler handler,
HashSet<string> forwardResolve,
HashSet<string> reverseResolve)
{ {
var forwardPaths = forwardResolve.ToArray(); var forwardPaths = forwardResolve.ToArray();
var reversePaths = reverseResolve.ToArray(); var reversePaths = reverseResolve.ToArray();
Dictionary<string, List<string>> resolvedPaths = new(StringComparer.Ordinal); Dictionary<string, List<string>> resolvedPaths = new(StringComparer.Ordinal);
if (handler.ObjectKind != ObjectKind.Player) if (handler.ObjectKind != ObjectKind.Player)
{ {
var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() => var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() =>
{ {
var idx = handler.GetGameObject()?.ObjectIndex; var idx = handler.GetGameObject()?.ObjectIndex;
if (!idx.HasValue) if (!idx.HasValue)
{
return ((int?)null, Array.Empty<string>(), Array.Empty<string[]>()); return ((int?)null, Array.Empty<string>(), Array.Empty<string[]>());
}
var resolvedForward = new string[forwardPaths.Length]; var resolvedForward = new string[forwardPaths.Length];
for (int i = 0; i < forwardPaths.Length; i++) for (int i = 0; i < forwardPaths.Length; i++)
{
resolvedForward[i] = _ipcManager.Penumbra.ResolveGameObjectPath(forwardPaths[i], idx.Value); resolvedForward[i] = _ipcManager.Penumbra.ResolveGameObjectPath(forwardPaths[i], idx.Value);
}
var resolvedReverse = new string[reversePaths.Length][]; var resolvedReverse = new string[reversePaths.Length][];
for (int i = 0; i < reversePaths.Length; i++) for (int i = 0; i < reversePaths.Length; i++)
{
resolvedReverse[i] = _ipcManager.Penumbra.ReverseResolveGameObjectPath(reversePaths[i], idx.Value); resolvedReverse[i] = _ipcManager.Penumbra.ReverseResolveGameObjectPath(reversePaths[i], idx.Value);
}
return (idx, resolvedForward, resolvedReverse); return (idx, resolvedForward, resolvedReverse);
}).ConfigureAwait(false); }).ConfigureAwait(false);
@@ -649,21 +409,31 @@ public class PlayerDataFactory
{ {
var filePath = forwardResolved[i]?.ToLowerInvariant(); var filePath = forwardResolved[i]?.ToLowerInvariant();
if (string.IsNullOrEmpty(filePath)) if (string.IsNullOrEmpty(filePath))
{
continue; continue;
}
if (resolvedPaths.TryGetValue(filePath, out var list)) if (resolvedPaths.TryGetValue(filePath, out var list))
{
list.Add(forwardPaths[i].ToLowerInvariant()); list.Add(forwardPaths[i].ToLowerInvariant());
}
else else
{
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()]; resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
}
} }
for (int i = 0; i < reversePaths.Length; i++) for (int i = 0; i < reversePaths.Length; i++)
{ {
var filePath = reversePaths[i].ToLowerInvariant(); var filePath = reversePaths[i].ToLowerInvariant();
if (resolvedPaths.TryGetValue(filePath, out var list)) if (resolvedPaths.TryGetValue(filePath, out var list))
{
list.AddRange(reverseResolved[i].Select(c => c.ToLowerInvariant())); list.AddRange(reverseResolved[i].Select(c => c.ToLowerInvariant()));
}
else else
resolvedPaths[filePath] = [.. reverseResolved[i].Select(c => c.ToLowerInvariant()).ToList()]; {
resolvedPaths[filePath] = new List<string>(reverseResolved[i].Select(c => c.ToLowerInvariant()).ToList());
}
} }
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly(); return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
@@ -671,23 +441,30 @@ public class PlayerDataFactory
} }
var (forward, reverse) = await _ipcManager.Penumbra.ResolvePathsAsync(forwardPaths, reversePaths).ConfigureAwait(false); var (forward, reverse) = await _ipcManager.Penumbra.ResolvePathsAsync(forwardPaths, reversePaths).ConfigureAwait(false);
for (int i = 0; i < forwardPaths.Length; i++) for (int i = 0; i < forwardPaths.Length; i++)
{ {
var filePath = forward[i].ToLowerInvariant(); var filePath = forward[i].ToLowerInvariant();
if (resolvedPaths.TryGetValue(filePath, out var list)) if (resolvedPaths.TryGetValue(filePath, out var list))
{
list.Add(forwardPaths[i].ToLowerInvariant()); list.Add(forwardPaths[i].ToLowerInvariant());
}
else else
{
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()]; resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
}
} }
for (int i = 0; i < reversePaths.Length; i++) for (int i = 0; i < reversePaths.Length; i++)
{ {
var filePath = reversePaths[i].ToLowerInvariant(); var filePath = reversePaths[i].ToLowerInvariant();
if (resolvedPaths.TryGetValue(filePath, out var list)) if (resolvedPaths.TryGetValue(filePath, out var list))
{
list.AddRange(reverse[i].Select(c => c.ToLowerInvariant())); list.AddRange(reverse[i].Select(c => c.ToLowerInvariant()));
}
else else
resolvedPaths[filePath] = [.. reverse[i].Select(c => c.ToLowerInvariant()).ToList()]; {
resolvedPaths[filePath] = new List<string>(reverse[i].Select(c => c.ToLowerInvariant()).ToList());
}
} }
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly(); return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
@@ -698,29 +475,11 @@ public class PlayerDataFactory
_transientResourceManager.PersistTransientResources(objectKind); _transientResourceManager.PersistTransientResources(objectKind);
HashSet<string> pathsToResolve = new(StringComparer.Ordinal); HashSet<string> pathsToResolve = new(StringComparer.Ordinal);
foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind).Where(path => !string.IsNullOrEmpty(path)))
int scanned = 0, skippedEmpty = 0, skippedVfx = 0;
foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind))
{ {
scanned++;
if (string.IsNullOrEmpty(path))
{
skippedEmpty++;
continue;
}
pathsToResolve.Add(path); pathsToResolve.Add(path);
} }
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(
"ManageSemiTransientData({kind}): scanned={scanned}, added={added}, skippedEmpty={skippedEmpty}, skippedVfx={skippedVfx}",
objectKind, scanned, pathsToResolve.Count, skippedEmpty, skippedVfx);
}
return pathsToResolve; return pathsToResolve;
} }
} }

View File

@@ -22,7 +22,6 @@ using Microsoft.Extensions.Logging;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind; using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer; using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
using LightlessSync.LightlessConfiguration;
namespace LightlessSync.PlayerData.Pairs; namespace LightlessSync.PlayerData.Pairs;
@@ -47,9 +46,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private readonly TextureDownscaleService _textureDownscaleService; private readonly TextureDownscaleService _textureDownscaleService;
private readonly PairStateCache _pairStateCache; private readonly PairStateCache _pairStateCache;
private readonly PairPerformanceMetricsCache _performanceMetricsCache; private readonly PairPerformanceMetricsCache _performanceMetricsCache;
private readonly XivDataAnalyzer _modelAnalyzer;
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor; private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
private readonly LightlessConfigService _configService;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private CancellationTokenSource? _applicationCancellationTokenSource; private CancellationTokenSource? _applicationCancellationTokenSource;
private Guid _applicationId; private Guid _applicationId;
@@ -93,10 +90,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
".avfx", ".avfx",
".scd" ".scd"
}; };
private readonly ConcurrentDictionary<string, byte> _blockedPapHashes = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, byte> _dumpedRemoteSkeletonForHash = new(StringComparer.OrdinalIgnoreCase);
private DateTime? _invisibleSinceUtc; private DateTime? _invisibleSinceUtc;
private DateTime? _visibilityEvictionDueAtUtc; private DateTime? _visibilityEvictionDueAtUtc;
private DateTime _nextActorLookupUtc = DateTime.MinValue; private DateTime _nextActorLookupUtc = DateTime.MinValue;
@@ -191,9 +184,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
TextureDownscaleService textureDownscaleService, TextureDownscaleService textureDownscaleService,
PairStateCache pairStateCache, PairStateCache pairStateCache,
PairPerformanceMetricsCache performanceMetricsCache, PairPerformanceMetricsCache performanceMetricsCache,
PenumbraTempCollectionJanitor tempCollectionJanitor, PenumbraTempCollectionJanitor tempCollectionJanitor) : base(logger, mediator)
XivDataAnalyzer modelAnalyzer,
LightlessConfigService configService) : base(logger, mediator)
{ {
_pairManager = pairManager; _pairManager = pairManager;
Ident = ident; Ident = ident;
@@ -212,8 +203,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_pairStateCache = pairStateCache; _pairStateCache = pairStateCache;
_performanceMetricsCache = performanceMetricsCache; _performanceMetricsCache = performanceMetricsCache;
_tempCollectionJanitor = tempCollectionJanitor; _tempCollectionJanitor = tempCollectionJanitor;
_modelAnalyzer = modelAnalyzer;
_configService = configService;
} }
public void Initialize() public void Initialize()
@@ -1434,7 +1423,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private Task _visibilityGraceTask; private Task _visibilityGraceTask;
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken) bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken)
{ {
var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false); var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false);
try try
@@ -1588,37 +1577,24 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
RecordFailure("Handler not available for application", "HandlerUnavailable"); RecordFailure("Handler not available for application", "HandlerUnavailable");
return; return;
} }
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
if (_applicationTask != null && !_applicationTask.IsCompleted) var appToken = _applicationCancellationTokenSource?.Token;
while ((!_applicationTask?.IsCompleted ?? false)
&& !downloadToken.IsCancellationRequested
&& (!appToken?.IsCancellationRequested ?? false))
{ {
Logger.LogDebug("[BASE-{appBase}] Cancelling current data application (Id: {id}) for player ({handler})", applicationBase, _applicationId, PlayerName); Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName);
await Task.Delay(250).ConfigureAwait(false);
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(downloadToken, timeoutCts.Token);
try
{
await _applicationTask.WaitAsync(combinedCts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
Logger.LogWarning("[BASE-{appBase}] Timeout waiting for application task {id} to complete, proceeding anyway", applicationBase, _applicationId);
}
finally
{
timeoutCts.Dispose();
combinedCts.Dispose();
}
} }
if (downloadToken.IsCancellationRequested) if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false))
{ {
_forceFullReapply = true; _forceFullReapply = true;
RecordFailure("Application cancelled", "Cancellation"); RecordFailure("Application cancelled", "Cancellation");
return; return;
} }
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
var token = _applicationCancellationTokenSource.Token; var token = _applicationCancellationTokenSource.Token;
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, wantsModApply, pendingModReapply, token); _applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, wantsModApply, pendingModReapply, token);
@@ -1680,36 +1656,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return; return;
} }
SplitPapMappings(moddedPaths, out var withoutPap, out var papOnly);
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, penumbraCollection, objIndex.Value).ConfigureAwait(false); await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, penumbraCollection, objIndex.Value).ConfigureAwait(false);
await _ipcManager.Penumbra.SetTemporaryModsAsync( await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, penumbraCollection,
Logger, _applicationId, penumbraCollection, moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false);
withoutPap.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)) _lastAppliedModdedPaths = new Dictionary<(string GamePath, string? Hash), string>(moddedPaths, moddedPaths.Comparer);
.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; LastAppliedDataBytes = -1;
foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists)) foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists))
{ {
@@ -1742,45 +1693,45 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_needsCollectionRebuild = false; _needsCollectionRebuild = false;
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0) if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
{ {
_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List<DownloadFileTransfer>()); _playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List<DownloadFileTransfer>());
}
if (LastAppliedDataTris < 0)
{
await _playerPerformanceService.CheckTriangleUsageThresholds(this, charaData).ConfigureAwait(false);
}
StorePerformanceMetrics(charaData);
_lastSuccessfulApplyAt = DateTime.UtcNow;
ClearFailureState();
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
} }
catch (OperationCanceledException) if (LastAppliedDataTris < 0)
{ {
Logger.LogDebug("[{applicationId}] Application cancelled for {handler}", _applicationId, GetLogIdentifier()); await _playerPerformanceService.CheckTriangleUsageThresholds(this, charaData).ConfigureAwait(false);
}
StorePerformanceMetrics(charaData);
_lastSuccessfulApplyAt = DateTime.UtcNow;
ClearFailureState();
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
}
catch (OperationCanceledException)
{
Logger.LogDebug("[{applicationId}] Application cancelled for {handler}", _applicationId, GetLogIdentifier());
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
_forceFullReapply = true;
RecordFailure("Application cancelled", "Cancellation");
}
catch (Exception ex)
{
if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException))
{
IsVisible = false;
_forceApplyMods = true;
_cachedData = charaData; _cachedData = charaData;
_pairStateCache.Store(Ident, charaData); _pairStateCache.Store(Ident, charaData);
_forceFullReapply = true; _forceFullReapply = true;
RecordFailure("Application cancelled", "Cancellation"); Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId);
} }
catch (Exception ex) else
{ {
if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException)) Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId);
{ _forceFullReapply = true;
IsVisible = false;
_forceApplyMods = true;
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
_forceFullReapply = true;
Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId);
}
else
{
Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId);
_forceFullReapply = true;
}
RecordFailure($"Application failed: {ex.Message}", "Exception");
} }
RecordFailure($"Application failed: {ex.Message}", "Exception");
} }
}
private void FrameworkUpdate() private void FrameworkUpdate()
{ {
@@ -2014,37 +1965,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
if (string.IsNullOrEmpty(new FileInfo(fileCache.ResolvedFilepath).Extension)) if (string.IsNullOrEmpty(new FileInfo(fileCache.ResolvedFilepath).Extension))
{ {
hasMigrationChanges = true; hasMigrationChanges = true;
var anyGamePath = item.GamePaths.FirstOrDefault(); fileCache = _fileDbManager.MigrateFileHashToExtension(fileCache, item.GamePaths[0].Split(".")[^1]);
if (!string.IsNullOrEmpty(anyGamePath))
{
var ext = Path.GetExtension(anyGamePath);
var extNoDot = ext.StartsWith('.') ? ext[1..] : ext;
if (!string.IsNullOrEmpty(extNoDot))
{
hasMigrationChanges = true;
fileCache = _fileDbManager.MigrateFileHashToExtension(fileCache, extNoDot);
}
}
} }
foreach (var gamePath in item.GamePaths) foreach (var gamePath in item.GamePaths)
{ {
var mode = _configService.Current.AnimationValidationMode;
if (mode != AnimationValidationMode.Unsafe
&& gamePath.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrEmpty(item.Hash)
&& _blockedPapHashes.ContainsKey(item.Hash))
{
continue;
}
var preferredPath = skipDownscaleForPair var preferredPath = skipDownscaleForPair
? fileCache.ResolvedFilepath ? fileCache.ResolvedFilepath
: _textureDownscaleService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath); : _textureDownscaleService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath);
outputDict[(gamePath, item.Hash)] = preferredPath; outputDict[(gamePath, item.Hash)] = preferredPath;
} }
} }
@@ -2354,7 +2282,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
HandleVisibilityLoss(logChange: false); HandleVisibilityLoss(logChange: false);
} }
private static bool TryResolveDescriptorHash(ActorObjectService.ActorDescriptor descriptor, out string hashedCid) private bool TryResolveDescriptorHash(ActorObjectService.ActorDescriptor descriptor, out string hashedCid)
{ {
hashedCid = descriptor.HashedContentId ?? string.Empty; hashedCid = descriptor.HashedContentId ?? string.Empty;
if (!string.IsNullOrEmpty(hashedCid)) if (!string.IsNullOrEmpty(hashedCid))
@@ -2367,106 +2295,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return !string.IsNullOrEmpty(hashedCid); return !string.IsNullOrEmpty(hashedCid);
} }
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)
{
var mode = _configService.Current.AnimationValidationMode;
var allowBasedShift = _configService.Current.AnimationAllowOneBasedShift;
var allownNightIndex = _configService.Current.AnimationAllowNeighborIndexTolerance;
if (mode == AnimationValidationMode.Unsafe || papOnly.Count == 0)
return 0;
var boneIndices = await _dalamudUtil.RunOnFrameworkThread(
() => _modelAnalyzer.GetSkeletonBoneIndices(handlerForApply))
.ConfigureAwait(false);
if (boneIndices == null || boneIndices.Count == 0)
{
var removedCount = papOnly.Count;
papOnly.Clear();
return removedCount;
}
var localBoneSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
foreach (var (rawKey, list) in boneIndices)
{
var key = XivDataAnalyzer.CanonicalizeSkeletonKey(rawKey);
if (string.IsNullOrEmpty(key)) continue;
if (!localBoneSets.TryGetValue(key, out var set))
localBoneSets[key] = set = [];
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 (XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, allowBasedShift, allownNightIndex, out var reason))
continue;
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 (_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) private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind)
{ {
_customizeIds[kind] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false); _customizeIds[kind] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);

View File

@@ -1,6 +1,5 @@
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc; using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Factories;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.ActorTracking; using LightlessSync.Services.ActorTracking;
@@ -33,8 +32,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
private readonly PairStateCache _pairStateCache; private readonly PairStateCache _pairStateCache;
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache; private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor; private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
private readonly LightlessConfigService _configService;
private readonly XivDataAnalyzer _modelAnalyzer;
public PairHandlerAdapterFactory( public PairHandlerAdapterFactory(
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
@@ -53,9 +50,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
TextureDownscaleService textureDownscaleService, TextureDownscaleService textureDownscaleService,
PairStateCache pairStateCache, PairStateCache pairStateCache,
PairPerformanceMetricsCache pairPerformanceMetricsCache, PairPerformanceMetricsCache pairPerformanceMetricsCache,
PenumbraTempCollectionJanitor tempCollectionJanitor, PenumbraTempCollectionJanitor tempCollectionJanitor)
XivDataAnalyzer modelAnalyzer,
LightlessConfigService configService)
{ {
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_mediator = mediator; _mediator = mediator;
@@ -74,8 +69,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_pairStateCache = pairStateCache; _pairStateCache = pairStateCache;
_pairPerformanceMetricsCache = pairPerformanceMetricsCache; _pairPerformanceMetricsCache = pairPerformanceMetricsCache;
_tempCollectionJanitor = tempCollectionJanitor; _tempCollectionJanitor = tempCollectionJanitor;
_modelAnalyzer = modelAnalyzer;
_configService = configService;
} }
public IPairHandlerAdapter Create(string ident) public IPairHandlerAdapter Create(string ident)
@@ -102,8 +95,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_textureDownscaleService, _textureDownscaleService,
_pairStateCache, _pairStateCache,
_pairPerformanceMetricsCache, _pairPerformanceMetricsCache,
_tempCollectionJanitor, _tempCollectionJanitor);
_modelAnalyzer,
_configService);
} }
} }

View File

@@ -140,7 +140,6 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<IdDisplayHandler>(); services.AddSingleton<IdDisplayHandler>();
services.AddSingleton<PlayerPerformanceService>(); services.AddSingleton<PlayerPerformanceService>();
services.AddSingleton<PenumbraTempCollectionJanitor>(); services.AddSingleton<PenumbraTempCollectionJanitor>();
services.AddSingleton<LocationShareService>();
services.AddSingleton<TextureMetadataHelper>(sp => services.AddSingleton<TextureMetadataHelper>(sp =>
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData)); new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));

View File

@@ -28,7 +28,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
{ {
_baseAnalysisCts = _baseAnalysisCts.CancelRecreate(); _baseAnalysisCts = _baseAnalysisCts.CancelRecreate();
var token = _baseAnalysisCts.Token; var token = _baseAnalysisCts.Token;
_ = Task.Run(async () => await BaseAnalysis(msg.CharacterData, token).ConfigureAwait(false), token); _ = BaseAnalysis(msg.CharacterData, token);
}); });
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_xivDataAnalyzer = modelAnalyzer; _xivDataAnalyzer = modelAnalyzer;

View File

@@ -10,6 +10,7 @@ using LightlessSync.UI;
using LightlessSync.UI.Services; using LightlessSync.UI.Services;
using LightlessSync.Utils; using LightlessSync.Utils;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -172,7 +173,8 @@ internal class ContextMenuService : IHostedService
return; return;
} }
if (!IsWorldValid(target.TargetHomeWorld.RowId)) var world = GetWorld(target.TargetHomeWorld.RowId);
if (!IsWorldValid(world))
{ {
_logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId); _logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId);
return; return;
@@ -225,7 +227,8 @@ internal class ContextMenuService : IHostedService
if (args.Target is not MenuTargetDefault target) if (args.Target is not MenuTargetDefault target)
return; return;
if (!target.TargetHomeWorld.IsValid || !IsWorldValid(target.TargetHomeWorld.RowId)) var world = GetWorld(target.TargetHomeWorld.RowId);
if (!IsWorldValid(world))
return; return;
try try
@@ -234,7 +237,7 @@ internal class ContextMenuService : IHostedService
if (targetData == null || targetData.Address == nint.Zero) if (targetData == null || targetData.Address == nint.Zero)
{ {
_logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, target.TargetHomeWorld.Value.Name); _logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name);
return; return;
} }
@@ -249,7 +252,7 @@ internal class ContextMenuService : IHostedService
} }
// Notify in chat when NotificationService is disabled // Notify in chat when NotificationService is disabled
NotifyInChat($"Pair request sent to {target.TargetName}@{target.TargetHomeWorld.Value.Name}.", NotificationType.Info); NotifyInChat($"Pair request sent to {target.TargetName}@{world.Name}.", NotificationType.Info);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -309,8 +312,37 @@ internal class ContextMenuService : IHostedService
p.HomeWorld.RowId == target.TargetHomeWorld.RowId); p.HomeWorld.RowId == target.TargetHomeWorld.RowId);
} }
private bool IsWorldValid(uint worldId) private World GetWorld(uint worldId)
{ {
return _dalamudUtil.WorldData.Value.ContainsKey((ushort)worldId); var sheet = _gameData.GetExcelSheet<World>()!;
var luminaWorlds = sheet.Where(x =>
{
var dc = x.DataCenter.ValueNullable;
var name = x.Name.ExtractText();
var internalName = x.InternalName.ExtractText();
if (dc == null || dc.Value.Region == 0 || string.IsNullOrWhiteSpace(dc.Value.Name.ExtractText()))
return false;
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(internalName))
return false;
if (name.Contains('-', StringComparison.Ordinal) || name.Contains('_', StringComparison.Ordinal))
return false;
return x.DataCenter.Value.Region != 5 || x.RowId > 3001 && x.RowId != 1200 && IsChineseJapaneseKoreanString(name);
});
return luminaWorlds.FirstOrDefault(x => x.RowId == worldId);
}
private static bool IsChineseJapaneseKoreanString(string text) => text.All(IsChineseJapaneseKoreanCharacter);
private static bool IsChineseJapaneseKoreanCharacter(char c) => c >= 0x4E00 && c <= 0x9FFF;
public static bool IsWorldValid(World world)
{
var name = world.Name.ToString();
return !string.IsNullOrWhiteSpace(name) && char.IsUpper(name[0]);
} }
} }

View File

@@ -1,13 +1,11 @@
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LightlessSync.API.Dto.CharaData; using LightlessSync.API.Dto.CharaData;
@@ -22,15 +20,12 @@ using LightlessSync.Utils;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind; using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using Map = Lumina.Excel.Sheets.Map;
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags; using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
namespace LightlessSync.Services; namespace LightlessSync.Services;
@@ -62,7 +57,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
private string _lastGlobalBlockReason = string.Empty; private string _lastGlobalBlockReason = string.Empty;
private ushort _lastZone = 0; private ushort _lastZone = 0;
private ushort _lastWorldId = 0; private ushort _lastWorldId = 0;
private uint _lastMapId = 0;
private bool _sentBetweenAreas = false; private bool _sentBetweenAreas = false;
private Lazy<ulong> _cid; private Lazy<ulong> _cid;
@@ -92,8 +86,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
WorldData = new(() => WorldData = new(() =>
{ {
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(clientLanguage)! return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(clientLanguage)!
.Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0]) .Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0])))
|| w is { RowId: > 1000, Region: 101 or 201 }))
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
}); });
JobData = new(() => JobData = new(() =>
@@ -666,7 +659,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
var location = new LocationInfo(); var location = new LocationInfo();
location.ServerId = _playerState.CurrentWorld.RowId; location.ServerId = _playerState.CurrentWorld.RowId;
location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; //location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; //TODO:Need API update first
location.TerritoryId = _clientState.TerritoryType; location.TerritoryId = _clientState.TerritoryType;
location.MapId = _clientState.MapId; location.MapId = _clientState.MapId;
if (houseMan != null) if (houseMan != null)
@@ -692,7 +685,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
var outside = houseMan->OutdoorTerritory; var outside = houseMan->OutdoorTerritory;
var house = outside->HouseId; var house = outside->HouseId;
location.WardId = house.WardIndex + 1u; location.WardId = house.WardIndex + 1u;
//location.HouseId = (uint)houseMan->GetCurrentPlot() + 1; location.HouseId = (uint)houseMan->GetCurrentPlot() + 1;
location.DivisionId = houseMan->GetCurrentDivision(); location.DivisionId = houseMan->GetCurrentDivision();
} }
//_logger.LogWarning(LocationToString(location)); //_logger.LogWarning(LocationToString(location));
@@ -720,10 +713,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
str += $" - {MapData.Value[(ushort)location.MapId].MapName}"; str += $" - {MapData.Value[(ushort)location.MapId].MapName}";
} }
if (location.InstanceId is not 0) // if (location.InstanceId is not 0)
{ // {
str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString(); // str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString();
} // }
if (location.WardId is not 0) if (location.WardId is not 0)
{ {
@@ -845,41 +838,31 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return Task.CompletedTask; return Task.CompletedTask;
} }
public async Task WaitWhileCharacterIsDrawing( public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null)
ILogger logger,
GameObjectHandler handler,
Guid redrawId,
int timeOut = 5000,
CancellationToken? ct = null)
{ {
if (!_clientState.IsLoggedIn) return; if (!_clientState.IsLoggedIn) return;
var token = ct ?? CancellationToken.None; if (ct == null)
ct = CancellationToken.None;
const int tick = 250; const int tick = 250;
const int initialSettle = 50; int curWaitTime = 0;
var sw = Stopwatch.StartNew();
try try
{ {
logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler); logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler);
await Task.Delay(tick, ct.Value).ConfigureAwait(true);
curWaitTime += tick;
await Task.Delay(initialSettle, token).ConfigureAwait(false); while ((!ct.Value.IsCancellationRequested)
&& curWaitTime < timeOut
while (!token.IsCancellationRequested && await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false)) // 0b100000000000 is "still rendering" or something
&& sw.ElapsedMilliseconds < timeOut
&& await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false))
{ {
logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler); logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler);
await Task.Delay(tick, token).ConfigureAwait(false); curWaitTime += tick;
await Task.Delay(tick, ct.Value).ConfigureAwait(true);
} }
logger.LogTrace("[{redrawId}] Finished drawing after {ms}ms", redrawId, sw.ElapsedMilliseconds); logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime);
}
catch (OperationCanceledException)
{
// ignore
} }
catch (AccessViolationException ex) catch (AccessViolationException ex)
{ {
@@ -1153,18 +1136,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas))); Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas)));
} }
//Map
if (!_sentBetweenAreas)
{
var mapid = _clientState.MapId;
if (mapid != _lastMapId)
{
_lastMapId = mapid;
Mediator.Publish(new MapChangedMessage(mapid));
}
}
var localPlayer = _objectTable.LocalPlayer; var localPlayer = _objectTable.LocalPlayer;
if (localPlayer != null) if (localPlayer != null)
{ {

View File

@@ -1,137 +0,0 @@
using LightlessSync.API.Data;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.API.Dto.User;
using LightlessSync.Services.Mediator;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
namespace LightlessSync.Services
{
public class LocationShareService : DisposableMediatorSubscriberBase
{
private readonly DalamudUtilService _dalamudUtilService;
private readonly ApiController _apiController;
private IMemoryCache _locations = new MemoryCache(new MemoryCacheOptions());
private IMemoryCache _sharingStatus = new MemoryCache(new MemoryCacheOptions());
private CancellationTokenSource _resetToken = new CancellationTokenSource();
public LocationShareService(ILogger<LocationShareService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator)
{
_dalamudUtilService = dalamudUtilService;
_apiController = apiController;
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
{
_resetToken.Cancel();
_resetToken.Dispose();
_resetToken = new CancellationTokenSource();
});
Mediator.Subscribe<ConnectedMessage>(this, (msg) =>
{
_ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, apiController.DisplayName), _dalamudUtilService.GetMapData()));
_ = RequestAllLocation();
} );
Mediator.Subscribe<LocationSharingMessage>(this, UpdateLocationList);
Mediator.Subscribe<MapChangedMessage>(this,
msg => _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, _apiController.DisplayName), _dalamudUtilService.GetMapData())));
}
private void UpdateLocationList(LocationSharingMessage msg)
{
if (_locations.TryGetValue(msg.User.UID, out _) && msg.LocationInfo.ServerId is 0)
{
_locations.Remove(msg.User.UID);
return;
}
if ( msg.LocationInfo.ServerId is not 0 && msg.ExpireAt > DateTime.UtcNow)
{
AddLocationInfo(msg.User.UID, msg.LocationInfo, msg.ExpireAt);
}
}
private void AddLocationInfo(string uid, LocationInfo location, DateTimeOffset expireAt)
{
var options = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(expireAt)
.AddExpirationToken(new CancellationChangeToken(_resetToken.Token));
_locations.Set(uid, location, options);
}
private async Task RequestAllLocation()
{
try
{
var (data, status) = await _apiController.RequestAllLocationInfo().ConfigureAwait(false);
foreach (var dto in data)
{
AddLocationInfo(dto.LocationDto.User.UID, dto.LocationDto.Location, dto.ExpireAt);
}
foreach (var dto in status)
{
AddStatus(dto.User.UID, dto.ExpireAt);
}
}
catch (Exception e)
{
Logger.LogError(e,"RequestAllLocation error : ");
throw;
}
}
private void AddStatus(string uid, DateTimeOffset expireAt)
{
var options = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(expireAt)
.AddExpirationToken(new CancellationChangeToken(_resetToken.Token));
_sharingStatus.Set(uid, expireAt, options);
}
public string GetUserLocation(string uid)
{
try
{
if (_locations.TryGetValue<LocationInfo>(uid, out var location))
{
return _dalamudUtilService.LocationToString(location);
}
return String.Empty;
}
catch (Exception e)
{
Logger.LogError(e,"GetUserLocation error : ");
throw;
}
}
public DateTimeOffset GetSharingStatus(string uid)
{
try
{
if (_sharingStatus.TryGetValue<DateTimeOffset>(uid, out var expireAt))
{
return expireAt;
}
return DateTimeOffset.MinValue;
}
catch (Exception e)
{
Logger.LogError(e,"GetSharingStatus error : ");
throw;
}
}
public void UpdateSharingStatus(List<string> users, DateTimeOffset expireAt)
{
foreach (var user in users)
{
AddStatus(user, expireAt);
}
}
}
}

View File

@@ -135,7 +135,5 @@ public record ChatChannelsUpdated : MessageBase;
public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase; public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase;
public record GroupCollectionChangedMessage : MessageBase; public record GroupCollectionChangedMessage : MessageBase;
public record OpenUserProfileMessage(UserData User) : MessageBase; public record OpenUserProfileMessage(UserData User) : MessageBase;
public record LocationSharingMessage(UserData User, LocationInfo LocationInfo, DateTimeOffset ExpireAt) : MessageBase;
public record MapChangedMessage(uint MapId) : MessageBase;
#pragma warning restore S2094 #pragma warning restore S2094
#pragma warning restore MA0048 // File name must match type name #pragma warning restore MA0048 // File name must match type name

View File

@@ -105,7 +105,6 @@ public class UiFactory
groupData: groupData, groupData: groupData,
isLightfinderContext: isLightfinderContext, isLightfinderContext: isLightfinderContext,
lightfinderCid: lightfinderCid, lightfinderCid: lightfinderCid,
performanceCollector: _performanceCollectorService, performanceCollector: _performanceCollectorService);
_apiController);
} }
} }

View File

@@ -6,15 +6,13 @@ using FFXIVClientStructs.Havok.Common.Serialize.Util;
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.Interop.GameModel; using LightlessSync.Interop.GameModel;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
namespace LightlessSync.Services; namespace LightlessSync.Services;
public sealed partial class XivDataAnalyzer public sealed class XivDataAnalyzer
{ {
private readonly ILogger<XivDataAnalyzer> _logger; private readonly ILogger<XivDataAnalyzer> _logger;
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
@@ -31,441 +29,127 @@ public sealed partial class XivDataAnalyzer
public unsafe Dictionary<string, List<ushort>>? GetSkeletonBoneIndices(GameObjectHandler handler) public unsafe Dictionary<string, List<ushort>>? GetSkeletonBoneIndices(GameObjectHandler handler)
{ {
if (handler is null || handler.Address == nint.Zero) if (handler.Address == nint.Zero) return null;
return null; var chara = (CharacterBase*)(((Character*)handler.Address)->GameObject.DrawObject);
if (chara->GetModelType() != CharacterBase.ModelType.Human) return null;
Dictionary<string, HashSet<ushort>> sets = new(StringComparer.OrdinalIgnoreCase); var resHandles = chara->Skeleton->SkeletonResourceHandles;
Dictionary<string, List<ushort>> outputIndices = [];
try try
{ {
var drawObject = ((Character*)handler.Address)->GameObject.DrawObject; for (int i = 0; i < chara->Skeleton->PartialSkeletonCount; i++)
if (drawObject == null)
return null;
var chara = (CharacterBase*)drawObject;
if (chara->GetModelType() != CharacterBase.ModelType.Human)
return null;
var skeleton = chara->Skeleton;
if (skeleton == null)
return null;
var resHandles = skeleton->SkeletonResourceHandles;
var partialCount = skeleton->PartialSkeletonCount;
if (partialCount <= 0)
return null;
for (int i = 0; i < partialCount; i++)
{ {
var handle = *(resHandles + i); var handle = *(resHandles + i);
if ((nint)handle == nint.Zero) _logger.LogTrace("Iterating over SkeletonResourceHandle #{i}:{x}", i, ((nint)handle).ToString("X"));
continue; if ((nint)handle == nint.Zero) continue;
var curBones = handle->BoneCount;
if (handle->FileName.Length > 1024) // this is unrealistic, the filename shouldn't ever be that long
continue; if (handle->FileName.Length > 1024) continue;
var skeletonName = handle->FileName.ToString();
var rawName = handle->FileName.ToString(); if (string.IsNullOrEmpty(skeletonName)) continue;
if (string.IsNullOrWhiteSpace(rawName)) outputIndices[skeletonName] = [];
continue; for (ushort boneIdx = 0; boneIdx < curBones; boneIdx++)
var skeletonKey = CanonicalizeSkeletonKey(rawName);
if (string.IsNullOrEmpty(skeletonKey))
continue;
var boneCount = handle->BoneCount;
if (boneCount == 0)
continue;
var havokSkel = handle->HavokSkeleton;
if ((nint)havokSkel == nint.Zero)
continue;
if (!sets.TryGetValue(skeletonKey, out var set))
{ {
set = []; var boneName = handle->HavokSkeleton->Bones[boneIdx].Name.String;
sets[skeletonKey] = set; if (boneName == null) continue;
outputIndices[skeletonName].Add((ushort)(boneIdx + 1));
} }
uint maxExclusive = boneCount;
uint ushortExclusive = (uint)ushort.MaxValue + 1u;
if (maxExclusive > ushortExclusive)
maxExclusive = ushortExclusive;
for (uint boneIdx = 0; boneIdx < maxExclusive; boneIdx++)
{
var name = havokSkel->Bones[boneIdx].Name.String;
if (name == null)
continue;
set.Add((ushort)boneIdx);
}
_logger.LogTrace("Local skeleton raw file='{raw}', key='{key}', boneCount={count}",
rawName, skeletonKey, boneCount);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "Could not process skeleton data"); _logger.LogWarning(ex, "Could not process skeleton data");
return null;
} }
if (sets.Count == 0) return (outputIndices.Count != 0 && outputIndices.Values.All(u => u.Count > 0)) ? outputIndices : null;
return null;
var output = new Dictionary<string, List<ushort>>(sets.Count, StringComparer.OrdinalIgnoreCase);
foreach (var (key, set) in sets)
{
if (set.Count == 0)
continue;
var list = set.ToList();
list.Sort();
output[key] = list;
}
return (output.Count != 0 && output.Values.All(v => v.Count > 0)) ? output : null;
} }
public unsafe Dictionary<string, List<ushort>>? GetBoneIndicesFromPap(string hash, bool persistToConfig = true) public unsafe Dictionary<string, List<ushort>>? GetBoneIndicesFromPap(string hash)
{ {
if (string.IsNullOrWhiteSpace(hash)) if (_configService.Current.BonesDictionary.TryGetValue(hash, out var bones)) return bones;
return null;
if (_configService.Current.BonesDictionary.TryGetValue(hash, out var cached) && cached is not null)
return cached;
var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash); var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash);
if (cacheEntity == null || string.IsNullOrEmpty(cacheEntity.ResolvedFilepath) || !File.Exists(cacheEntity.ResolvedFilepath)) if (cacheEntity == null) return null;
return null;
using var fs = File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read); using BinaryReader reader = new(File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read));
using var reader = new BinaryReader(fs);
// PAP header (mostly from vfxeditor) // most of this shit is from vfxeditor, surely nothing will change in the pap format :copium:
_ = reader.ReadInt32(); // ignore reader.ReadInt32(); // ignore
_ = reader.ReadInt32(); // ignore reader.ReadInt32(); // ignore
_ = reader.ReadInt16(); // num animations reader.ReadInt16(); // read 2 (num animations)
_ = reader.ReadInt16(); // modelid reader.ReadInt16(); // read 2 (modelid)
var type = reader.ReadByte();// read 1 (type)
var type = reader.ReadByte(); // type if (type != 0) return null; // it's not human, just ignore it, whatever
if (type != 0)
return null; // not human
_ = reader.ReadByte(); // variant
_ = reader.ReadInt32(); // ignore
reader.ReadByte(); // read 1 (variant)
reader.ReadInt32(); // ignore
var havokPosition = reader.ReadInt32(); var havokPosition = reader.ReadInt32();
var footerPosition = reader.ReadInt32(); var footerPosition = reader.ReadInt32();
var havokDataSize = footerPosition - havokPosition;
// sanity checks
if (havokPosition <= 0 || footerPosition <= havokPosition || footerPosition > fs.Length)
return null;
var havokDataSizeLong = (long)footerPosition - havokPosition;
if (havokDataSizeLong <= 8 || havokDataSizeLong > int.MaxValue)
return null;
var havokDataSize = (int)havokDataSizeLong;
reader.BaseStream.Position = havokPosition; reader.BaseStream.Position = havokPosition;
var havokData = reader.ReadBytes(havokDataSize); var havokData = reader.ReadBytes(havokDataSize);
if (havokData.Length <= 8) if (havokData.Length <= 8) return null; // no havok data
return null;
var tempSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase); var output = new Dictionary<string, List<ushort>>(StringComparer.OrdinalIgnoreCase);
var tempHavokDataPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + ".hkx";
var tempHavokDataPath = Path.Combine(Path.GetTempPath(), $"lightless_{Guid.NewGuid():N}.hkx"); var tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath);
IntPtr tempHavokDataPathAnsi = IntPtr.Zero;
try try
{ {
File.WriteAllBytes(tempHavokDataPath, havokData); File.WriteAllBytes(tempHavokDataPath, havokData);
if (!File.Exists(tempHavokDataPath))
{
_logger.LogTrace("Temporary havok file did not exist when attempting to load: {path}", tempHavokDataPath);
return null;
}
tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath);
var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1]; var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1];
loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry(); loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry();
loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry(); loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry();
loadoptions->Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int> loadoptions->Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int>
{ {
Storage = (int)hkSerializeUtil.LoadOptionBits.Default Storage = (int)(hkSerializeUtil.LoadOptionBits.Default)
}; };
var resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions); var resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions);
if (resource == null) if (resource == null)
{ {
_logger.LogWarning("Havok resource was null after loading from {path}", tempHavokDataPath); throw new InvalidOperationException("Resource was null after loading");
return null;
} }
var rootLevelName = @"hkRootLevelContainer"u8; var rootLevelName = @"hkRootLevelContainer"u8;
fixed (byte* n1 = rootLevelName) fixed (byte* n1 = rootLevelName)
{ {
var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry()); var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry());
if (container == null)
return null;
var animationName = @"hkaAnimationContainer"u8; var animationName = @"hkaAnimationContainer"u8;
fixed (byte* n2 = animationName) fixed (byte* n2 = animationName)
{ {
var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null); var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null);
if (animContainer == null)
return null;
for (int i = 0; i < animContainer->Bindings.Length; i++) for (int i = 0; i < animContainer->Bindings.Length; i++)
{ {
var binding = animContainer->Bindings[i].ptr; var binding = animContainer->Bindings[i].ptr;
if (binding == null)
continue;
var rawSkel = binding->OriginalSkeletonName.String;
var skeletonKey = CanonicalizeSkeletonKey(rawSkel);
if (string.IsNullOrEmpty(skeletonKey))
continue;
var boneTransform = binding->TransformTrackToBoneIndices; var boneTransform = binding->TransformTrackToBoneIndices;
if (boneTransform.Length <= 0) string name = binding->OriginalSkeletonName.String! + "_" + i;
continue; output[name] = [];
if (!tempSets.TryGetValue(skeletonKey, out var set))
{
set = [];
tempSets[skeletonKey] = set;
}
for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++) for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++)
{ {
var v = boneTransform[boneIdx]; output[name].Add((ushort)boneTransform[boneIdx]);
if (v < 0) continue;
set.Add((ushort)v);
} }
output[name].Sort();
} }
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "Could not load havok file in {path}", tempHavokDataPath); _logger.LogWarning(ex, "Could not load havok file in {path}", tempHavokDataPath);
return null;
} }
finally finally
{ {
if (tempHavokDataPathAnsi != IntPtr.Zero) Marshal.FreeHGlobal(tempHavokDataPathAnsi);
Marshal.FreeHGlobal(tempHavokDataPathAnsi); File.Delete(tempHavokDataPath);
try
{
if (File.Exists(tempHavokDataPath))
File.Delete(tempHavokDataPath);
}
catch (Exception ex)
{
_logger.LogTrace(ex, "Could not delete temporary havok file: {path}", tempHavokDataPath);
}
} }
if (tempSets.Count == 0)
return null;
var output = new Dictionary<string, List<ushort>>(tempSets.Count, StringComparer.OrdinalIgnoreCase);
foreach (var (key, set) in tempSets)
{
if (set.Count == 0) continue;
var list = set.ToList();
list.Sort();
output[key] = list;
}
if (output.Count == 0)
return null;
_configService.Current.BonesDictionary[hash] = output; _configService.Current.BonesDictionary[hash] = output;
_configService.Save();
if (persistToConfig)
_configService.Save();
return output; return output;
} }
public static string CanonicalizeSkeletonKey(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
return string.Empty;
var s = raw.Replace('\\', '/').Trim();
var underscore = s.LastIndexOf('_');
if (underscore > 0 && underscore + 1 < s.Length && char.IsDigit(s[underscore + 1]))
s = s[..underscore];
if (s.StartsWith("skeleton", StringComparison.OrdinalIgnoreCase))
return "skeleton";
var m = _bucketPathRegex.Match(s);
if (m.Success)
return m.Groups["bucket"].Value.ToLowerInvariant();
m = _bucketSklRegex.Match(s);
if (m.Success)
return m.Groups["bucket"].Value.ToLowerInvariant();
m = _bucketLooseRegex.Match(s);
if (m.Success)
return m.Groups["bucket"].Value.ToLowerInvariant();
return string.Empty;
}
public static bool ContainsIndexCompat(
HashSet<ushort> available,
ushort idx,
bool papLikelyOneBased,
bool allowOneBasedShift,
bool allowNeighborTolerance)
{
Span<ushort> candidates = stackalloc ushort[2];
int count = 0;
candidates[count++] = idx;
if (allowOneBasedShift && papLikelyOneBased && idx > 0)
candidates[count++] = (ushort)(idx - 1);
for (int i = 0; i < count; i++)
{
var c = candidates[i];
if (available.Contains(c))
return true;
if (allowNeighborTolerance)
{
if (c > 0 && available.Contains((ushort)(c - 1)))
return true;
if (c < ushort.MaxValue && available.Contains((ushort)(c + 1)))
return true;
}
}
return false;
}
public static bool IsPapCompatible(
IReadOnlyDictionary<string, HashSet<ushort>> localBoneSets,
IReadOnlyDictionary<string, List<ushort>> papBoneIndices,
AnimationValidationMode mode,
bool allowOneBasedShift,
bool allowNeighborTolerance,
out string reason)
{
reason = string.Empty;
if (mode == AnimationValidationMode.Unsafe)
return true;
var papBuckets = papBoneIndices.Keys
.Select(CanonicalizeSkeletonKey)
.Where(k => !string.IsNullOrEmpty(k))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (papBuckets.Count == 0)
{
reason = "No skeleton bucket bindings found in the PAP";
return false;
}
if (mode == AnimationValidationMode.Safe)
{
if (papBuckets.Any(b => localBoneSets.ContainsKey(b)))
return true;
reason = $"No matching skeleton bucket between PAP [{string.Join(", ", papBuckets)}] and local [{string.Join(", ", localBoneSets.Keys.Order())}].";
return false;
}
foreach (var bucket in papBuckets)
{
if (!localBoneSets.TryGetValue(bucket, out var available))
{
reason = $"Missing skeleton bucket '{bucket}' on local actor.";
return false;
}
var indices = papBoneIndices
.Where(kvp => string.Equals(CanonicalizeSkeletonKey(kvp.Key), bucket, StringComparison.OrdinalIgnoreCase))
.SelectMany(kvp => kvp.Value ?? Enumerable.Empty<ushort>())
.Distinct()
.ToList();
if (indices.Count == 0)
continue;
bool has0 = false, has1 = false;
ushort min = ushort.MaxValue;
foreach (var v in indices)
{
if (v == 0) has0 = true;
if (v == 1) has1 = true;
if (v < min) min = v;
}
bool papLikelyOneBased = allowOneBasedShift && (min == 1) && has1 && !has0;
foreach (var idx in indices)
{
if (!ContainsIndexCompat(available, idx, papLikelyOneBased, allowOneBasedShift, allowNeighborTolerance))
{
reason = $"No compatible local skeleton for PAP '{bucket}': missing bone index {idx}.";
return false;
}
}
}
return true;
}
public void DumpLocalSkeletonIndices(GameObjectHandler handler, string? filter = null)
{
var skels = GetSkeletonBoneIndices(handler);
if (skels == null)
{
_logger.LogTrace("DumpLocalSkeletonIndices: local skeleton indices are null or not found");
return;
}
var keys = skels.Keys
.Order(StringComparer.OrdinalIgnoreCase)
.ToArray();
_logger.LogTrace("Local skeleton indices found ({count}): {keys}",
keys.Length,
string.Join(", ", keys));
if (!string.IsNullOrWhiteSpace(filter))
{
var hits = keys.Where(k =>
k.Equals(filter, StringComparison.OrdinalIgnoreCase) ||
k.StartsWith(filter + "_", StringComparison.OrdinalIgnoreCase) ||
filter.StartsWith(k + "_", StringComparison.OrdinalIgnoreCase) ||
k.Contains(filter, StringComparison.OrdinalIgnoreCase))
.ToArray();
_logger.LogTrace("Matches found for '{filter}': {hits}",
filter,
hits.Length == 0 ? "<none>" : string.Join(", ", hits));
}
}
public async Task<long> GetTrianglesByHash(string hash) public async Task<long> GetTrianglesByHash(string hash)
{ {
if (_configService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0) if (_configService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0)
@@ -528,23 +212,4 @@ public sealed partial class XivDataAnalyzer
return 0; return 0;
} }
} }
// Regexes for canonicalizing skeleton keys
private static readonly Regex _bucketPathRegex =
BucketRegex();
private static readonly Regex _bucketSklRegex =
SklRegex();
private static readonly Regex _bucketLooseRegex =
LooseBucketRegex();
[GeneratedRegex(@"(?i)(?:^|/)(?<bucket>c\d{4})(?:/|$)", RegexOptions.Compiled, "en-NL")]
private static partial Regex BucketRegex();
[GeneratedRegex(@"(?i)\bskl_(?<bucket>c\d{4})[a-z]\d{4}\b", RegexOptions.Compiled, "en-NL")]
private static partial Regex SklRegex();
[GeneratedRegex(@"(?i)(?<![a-z0-9])(?<bucket>c\d{4})(?!\d)", RegexOptions.Compiled, "en-NL")]
private static partial Regex LooseBucketRegex();
} }

View File

@@ -37,7 +37,6 @@ public class DrawUserPair
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _performanceConfigService; private readonly PlayerPerformanceConfigService _performanceConfigService;
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly LocationShareService _locationShareService;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly PairLedger _pairLedger; private readonly PairLedger _pairLedger;
private float _menuWidth = -1; private float _menuWidth = -1;
@@ -58,7 +57,6 @@ public class DrawUserPair
UiSharedService uiSharedService, UiSharedService uiSharedService,
PlayerPerformanceConfigService performanceConfigService, PlayerPerformanceConfigService performanceConfigService,
LightlessConfigService configService, LightlessConfigService configService,
LocationShareService locationShareService,
CharaDataManager charaDataManager, CharaDataManager charaDataManager,
PairLedger pairLedger) PairLedger pairLedger)
{ {
@@ -76,7 +74,6 @@ public class DrawUserPair
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_performanceConfigService = performanceConfigService; _performanceConfigService = performanceConfigService;
_configService = configService; _configService = configService;
_locationShareService = locationShareService;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_pairLedger = pairLedger; _pairLedger = pairLedger;
} }
@@ -219,48 +216,6 @@ public class DrawUserPair
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions));
} }
UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty)); UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty));
ImGui.SetCursorPosX(10f);
_uiSharedService.IconText(FontAwesomeIcon.Globe);
ImGui.SameLine();
if (ImGui.BeginMenu("Toggle Location sharing"))
{
if (ImGui.MenuItem("Share for 30 Mins"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddMinutes(30));
}
if (ImGui.MenuItem("Share for 1 Hour"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(1));
}
if (ImGui.MenuItem("Share for 3 Hours"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(3));
}
if (ImGui.MenuItem("Share until manually stop"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MaxValue);
}
ImGui.Separator();
if (ImGui.MenuItem("Stop Sharing"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MinValue);
}
ImGui.EndMenu();
}
}
private async Task ToggleLocationSharing(List<string> users, DateTimeOffset expireAt)
{
var updated = await _apiController.ToggleLocationSharing(new LocationSharingToggleDto(users, expireAt)).ConfigureAwait(false);
if (updated)
{
_locationShareService.UpdateSharingStatus(users, expireAt);
}
} }
private void DrawIndividualMenu() private void DrawIndividualMenu()
@@ -620,71 +575,6 @@ public class DrawUserPair
var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky(); var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky();
var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle; var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle;
var shareLocationIcon = FontAwesomeIcon.Globe;
var location = _locationShareService.GetUserLocation(_pair.UserPair!.User.UID);
var shareLocation = !string.IsNullOrEmpty(location);
var expireAt = _locationShareService.GetSharingStatus(_pair.UserPair!.User.UID);
var shareLocationToOther = expireAt > DateTimeOffset.UtcNow;
var shareColor = shareLocation switch
{
true when shareLocationToOther => UIColors.Get("LightlessGreen"),
true when !shareLocationToOther => UIColors.Get("LightlessBlue"),
_ => UIColors.Get("LightlessYellow"),
};
if (shareLocation || shareLocationToOther)
{
currentRightSide -= (_uiSharedService.GetIconSize(shareLocationIcon).X + spacingX);
ImGui.SameLine(currentRightSide);
using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther))
_uiSharedService.IconText(shareLocationIcon);
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
if (_pair.IsOnline)
{
if (shareLocation)
{
if (!string.IsNullOrEmpty(location))
{
_uiSharedService.IconText(FontAwesomeIcon.LocationArrow);
ImGui.SameLine();
ImGui.TextUnformatted(location);
}
else
{
ImGui.TextUnformatted("Location info not updated, reconnect or wait for update.");
}
}
else
{
ImGui.TextUnformatted("NOT Sharing location with you. o(TヘTo)");
}
}
else
{
ImGui.TextUnformatted("User not online. (´・ω・`)?");
}
ImGui.Separator();
if (shareLocationToOther)
{
ImGui.TextUnformatted("Sharing your location. ヾ(•ω•`)o");
if (expireAt != DateTimeOffset.MaxValue)
{
ImGui.TextUnformatted("Expires at " + expireAt.ToLocalTime().ToString("g"));
}
}
else
{
ImGui.TextUnformatted("NOT sharing your location.  ̄へ ̄");
}
ImGui.EndTooltip();
}
}
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky) if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)
{ {
currentRightSide -= (_uiSharedService.GetIconSize(individualIcon).X + spacingX); currentRightSide -= (_uiSharedService.GetIconSize(individualIcon).X + spacingX);

View File

@@ -2183,7 +2183,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
bool toggleClicked = false; bool toggleClicked = false;
if (showToggle) if (showToggle)
{ {
var icon = !isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft; var icon = isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft;
Vector2 iconSize; Vector2 iconSize;
using (_uiSharedService.IconFont.Push()) using (_uiSharedService.IconFont.Push())
{ {

View File

@@ -25,8 +25,6 @@ public class DownloadUi : WindowMediatorSubscriberBase
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new(); private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
private readonly Dictionary<GameObjectHandler, Vector2> _smoothed = []; private readonly Dictionary<GameObjectHandler, Vector2> _smoothed = [];
private readonly Dictionary<GameObjectHandler, DownloadSpeedTracker> _downloadSpeeds = []; private readonly Dictionary<GameObjectHandler, DownloadSpeedTracker> _downloadSpeeds = [];
private readonly Dictionary<GameObjectHandler, (int TotalFiles, long TotalBytes)> _downloadInitialTotals = [];
private byte _transferBoxTransparency = 100; private byte _transferBoxTransparency = 100;
private bool _notificationDismissed = true; private bool _notificationDismissed = true;
@@ -68,10 +66,6 @@ public class DownloadUi : WindowMediatorSubscriberBase
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => Mediator.Subscribe<DownloadStartedMessage>(this, (msg) =>
{ {
_currentDownloads[msg.DownloadId] = msg.DownloadStatus; _currentDownloads[msg.DownloadId] = msg.DownloadStatus;
// Capture initial totals when download starts
var totalFiles = msg.DownloadStatus.Values.Sum(s => s.TotalFiles);
var totalBytes = msg.DownloadStatus.Values.Sum(s => s.TotalBytes);
_downloadInitialTotals[msg.DownloadId] = (totalFiles, totalBytes);
_notificationDismissed = false; _notificationDismissed = false;
}); });
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) =>
@@ -173,7 +167,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
List<KeyValuePair<GameObjectHandler, Dictionary<string, FileDownloadStatus>>> transfers; List<KeyValuePair<GameObjectHandler, Dictionary<string, FileDownloadStatus>>> transfers;
try try
{ {
transfers = [.. _currentDownloads]; transfers = _currentDownloads.ToList();
} }
catch (ArgumentException) catch (ArgumentException)
{ {
@@ -441,13 +435,9 @@ public class DownloadUi : WindowMediatorSubscriberBase
var handler = transfer.Key; var handler = transfer.Key;
var statuses = transfer.Value.Values; var statuses = transfer.Value.Values;
var (playerTotalFiles, playerTotalBytes) = _downloadInitialTotals.TryGetValue(handler, out var totals) var playerTotalFiles = statuses.Sum(s => s.TotalFiles);
? totals var playerTransferredFiles = statuses.Sum(s => s.TransferredFiles);
: (statuses.Sum(s => s.TotalFiles), statuses.Sum(s => s.TotalBytes)); var playerTotalBytes = statuses.Sum(s => s.TotalBytes);
var playerTransferredFiles = statuses.Count(s =>
s.DownloadStatus == DownloadStatus.Decompressing ||
s.TransferredBytes >= s.TotalBytes);
var playerTransferredBytes = statuses.Sum(s => s.TransferredBytes); var playerTransferredBytes = statuses.Sum(s => s.TransferredBytes);
totalFiles += playerTotalFiles; totalFiles += playerTotalFiles;

View File

@@ -29,7 +29,6 @@ public class DrawEntityFactory
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly LocationShareService _locationShareService;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly SelectTagForPairUi _selectTagForPairUi; private readonly SelectTagForPairUi _selectTagForPairUi;
private readonly RenamePairTagUi _renamePairTagUi; private readonly RenamePairTagUi _renamePairTagUi;
@@ -54,7 +53,6 @@ public class DrawEntityFactory
LightlessConfigService configService, LightlessConfigService configService,
UiSharedService uiSharedService, UiSharedService uiSharedService,
PlayerPerformanceConfigService playerPerformanceConfigService, PlayerPerformanceConfigService playerPerformanceConfigService,
LocationShareService locationShareService,
CharaDataManager charaDataManager, CharaDataManager charaDataManager,
SelectTagForSyncshellUi selectTagForSyncshellUi, SelectTagForSyncshellUi selectTagForSyncshellUi,
RenameSyncshellTagUi renameSyncshellTagUi, RenameSyncshellTagUi renameSyncshellTagUi,
@@ -74,7 +72,6 @@ public class DrawEntityFactory
_configService = configService; _configService = configService;
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_playerPerformanceConfigService = playerPerformanceConfigService; _playerPerformanceConfigService = playerPerformanceConfigService;
_locationShareService = locationShareService;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_selectTagForSyncshellUi = selectTagForSyncshellUi; _selectTagForSyncshellUi = selectTagForSyncshellUi;
_renameSyncshellTagUi = renameSyncshellTagUi; _renameSyncshellTagUi = renameSyncshellTagUi;
@@ -165,7 +162,6 @@ public class DrawEntityFactory
_uiSharedService, _uiSharedService,
_playerPerformanceConfigService, _playerPerformanceConfigService,
_configService, _configService,
_locationShareService,
_charaDataManager, _charaDataManager,
_pairLedger); _pairLedger);
} }

View File

@@ -14,7 +14,6 @@ using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Configurations; using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.LightlessConfiguration.Models; using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using LightlessSync.PlayerData.Pairs; using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services; using LightlessSync.Services;
@@ -41,7 +40,6 @@ using System.Globalization;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@@ -86,8 +84,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
private bool _pairDiagnosticsEnabled; private bool _pairDiagnosticsEnabled;
private string? _selectedPairDebugUid = null; private string? _selectedPairDebugUid = null;
private string _lightfinderIconInput = string.Empty; private string _lightfinderIconInput = string.Empty;
private bool _showLightfinderRendererWarning = false;
private LightfinderLabelRenderer _pendingLightfinderRenderer = LightfinderLabelRenderer.Pictomancy;
private bool _lightfinderIconInputInitialized = false; private bool _lightfinderIconInputInitialized = false;
private int _lightfinderIconPresetIndex = -1; private int _lightfinderIconPresetIndex = -1;
private static readonly LightlessConfig DefaultConfig = new(); private static readonly LightlessConfig DefaultConfig = new();
@@ -107,8 +103,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
}; };
private readonly UiSharedService.TabOption<TransferSettingsTab>[] _transferTabOptions = new UiSharedService.TabOption<TransferSettingsTab>[2]; private readonly UiSharedService.TabOption<TransferSettingsTab>[] _transferTabOptions = new UiSharedService.TabOption<TransferSettingsTab>[2];
private readonly List<UiSharedService.TabOption<ServerSettingsTab>> _serverTabOptions = new(4); private readonly List<UiSharedService.TabOption<ServerSettingsTab>> _serverTabOptions = new(4);
private readonly string[] _generalTreeNavOrder = private readonly string[] _generalTreeNavOrder = new[]
[ {
"Import & Export", "Import & Export",
"Popup & Auto Fill", "Popup & Auto Fill",
"Behavior", "Behavior",
@@ -118,8 +114,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
"Colors", "Colors",
"Server Info Bar", "Server Info Bar",
"Nameplate", "Nameplate",
"Animation & Bones" };
];
private static readonly HashSet<string> _generalNavSeparatorAfter = new(StringComparer.Ordinal) private static readonly HashSet<string> _generalNavSeparatorAfter = new(StringComparer.Ordinal)
{ {
"Popup & Auto Fill", "Popup & Auto Fill",
@@ -1144,7 +1139,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
private async Task<List<string>?> RunSpeedTest(List<string> servers, CancellationToken token) private async Task<List<string>?> RunSpeedTest(List<string> servers, CancellationToken token)
{ {
List<string> speedTestResults = []; List<string> speedTestResults = new();
foreach (var server in servers) foreach (var server in servers)
{ {
HttpResponseMessage? result = null; HttpResponseMessage? result = null;
@@ -1928,25 +1923,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
{ {
using (ImRaii.PushIndent(20f)) using (ImRaii.PushIndent(20f))
{ {
if (_validationTask.IsCompletedSuccessfully) if (_validationTask.IsCompleted)
{ {
UiSharedService.TextWrapped( UiSharedService.TextWrapped(
$"The storage validation has completed and removed {_validationTask.Result.Count} invalid files from storage."); $"The storage validation has completed and removed {_validationTask.Result.Count} invalid files from storage.");
} }
else if (_validationTask.IsCanceled)
{
UiSharedService.ColorTextWrapped(
"Storage validation was cancelled.",
UIColors.Get("LightlessYellow"));
}
else if (_validationTask.IsFaulted)
{
UiSharedService.ColorTextWrapped(
"Storage validation failed with an error.",
UIColors.Get("DimRed"));
}
else else
{ {
UiSharedService.TextWrapped( UiSharedService.TextWrapped(
$"Storage validation is running: {_currentProgress.Item1}/{_currentProgress.Item2}"); $"Storage validation is running: {_currentProgress.Item1}/{_currentProgress.Item2}");
if (_currentProgress.Item3 != null) if (_currentProgress.Item3 != null)
@@ -2388,7 +2372,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
var labelRenderer = _configService.Current.LightfinderLabelRenderer; var labelRenderer = _configService.Current.LightfinderLabelRenderer;
var labelRendererLabel = labelRenderer switch var labelRendererLabel = labelRenderer switch
{ {
LightfinderLabelRenderer.SignatureHook => "Native Nameplate Rendering", LightfinderLabelRenderer.SignatureHook => "Native nameplate (sig hook)",
_ => "ImGui Overlay", _ => "ImGui Overlay",
}; };
@@ -2398,25 +2382,18 @@ public class SettingsUi : WindowMediatorSubscriberBase
{ {
var optionLabel = option switch var optionLabel = option switch
{ {
LightfinderLabelRenderer.SignatureHook => "Native Nameplate Rendering", LightfinderLabelRenderer.SignatureHook => "Native Nameplate (sig hook)",
_ => "ImGui Overlay", _ => "ImGui Overlay",
}; };
var selected = option == labelRenderer; var selected = option == labelRenderer;
if (ImGui.Selectable(optionLabel, selected)) if (ImGui.Selectable(optionLabel, selected))
{ {
if (option == LightfinderLabelRenderer.SignatureHook) _configService.Current.LightfinderLabelRenderer = option;
{ _configService.Save();
_pendingLightfinderRenderer = option; _nameplateService.RequestRedraw();
_showLightfinderRendererWarning = true;
}
else
{
_configService.Current.LightfinderLabelRenderer = option;
_configService.Save();
_nameplateService.RequestRedraw();
}
} }
if (selected) if (selected)
ImGui.SetItemDefaultFocus(); ImGui.SetItemDefaultFocus();
} }
@@ -2424,34 +2401,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.EndCombo(); ImGui.EndCombo();
} }
if (_showLightfinderRendererWarning)
{
ImGui.SetNextWindowSize(new Vector2(450f, 0f), ImGuiCond.Appearing);
ImGui.OpenPopup("Nameplate Warning");
}
if (ImGui.BeginPopupModal("Nameplate Warning", ref _showLightfinderRendererWarning, ImGuiWindowFlags.AlwaysAutoResize))
{
ImGui.TextColored(UIColors.Get("DimRed"), "USE AT YOUR RISK!");
ImGui.Spacing();
ImGui.TextWrapped("Writing on to the native Nameplates is known to be unstable and MAY cause crashes. DO NOT REPORT THOSE CRASHES TO DALAMUD. We will also not be supporting Nameplate crashes. You have been warned.");
ImGui.Spacing();
ImGui.TextWrapped("By accepting this warning, you understand that you are using this feature at risk of crashing.");
ImGui.Spacing();
var buttonWidth = ImGui.GetContentRegionAvail().X;
if (ImGui.Button("I Understand", new Vector2(buttonWidth, 0)))
{
_configService.Current.LightfinderLabelRenderer = _pendingLightfinderRenderer;
_configService.Save();
_nameplateService.RequestRedraw();
_showLightfinderRendererWarning = false;
ImGui.CloseCurrentPopup();
}
ImGui.EndPopup();
}
_uiShared.DrawHelpText("Choose how Lightfinder labels render: the default ImGui overlay or native nameplate nodes via signature hook."); _uiShared.DrawHelpText("Choose how Lightfinder labels render: the default ImGui overlay or native nameplate nodes via signature hook.");
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
@@ -3088,102 +3037,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
ImGui.Separator(); ImGui.Separator();
ImGui.Dummy(new Vector2(10));
_uiShared.BigText("Animation");
using (var animationTree = BeginGeneralTree("Animation & Bones", UIColors.Get("LightlessPurple")))
{
if (animationTree.Visible)
{
ImGui.TextUnformatted("Animation Options");
var modes = new[]
{
AnimationValidationMode.Unsafe,
AnimationValidationMode.Safe,
AnimationValidationMode.Safest,
};
var labels = new[]
{
"Unsafe",
"Safe (Race)",
"Safest (Race + Bones)",
};
var tooltips = new[]
{
"No validation. Fastest, but may allow incompatible animations (riskier).",
"Validates skeleton race + modded skeleton check (recommended).",
"Requires matching skeleton race + bone compatibility (strictest).",
};
var currentMode = _configService.Current.AnimationValidationMode;
int selectedIndex = Array.IndexOf(modes, currentMode);
if (selectedIndex < 0) selectedIndex = 1;
ImGui.SetNextItemWidth(140 * ImGuiHelpers.GlobalScale);
bool open = ImGui.BeginCombo("Animation validation", labels[selectedIndex]);
if (ImGui.IsItemHovered())
ImGui.SetTooltip(tooltips[selectedIndex]);
if (open)
{
for (int i = 0; i < modes.Length; i++)
{
bool isSelected = (i == selectedIndex);
if (ImGui.Selectable(labels[i], isSelected))
{
selectedIndex = i;
_configService.Current.AnimationValidationMode = modes[i];
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(tooltips[i]);
if (isSelected)
ImGui.SetItemDefaultFocus();
}
ImGui.EndCombo();
}
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
var cfg = _configService.Current;
bool oneBased = cfg.AnimationAllowOneBasedShift;
if (ImGui.Checkbox("Treat 1-based PAP indices as compatible", ref oneBased))
{
cfg.AnimationAllowOneBasedShift = oneBased;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Fixes off-by-one PAPs (one bone differance in bones and PAP). Can also increase crashing, toggle off if alot of crashing is happening");
bool neighbor = cfg.AnimationAllowNeighborIndexTolerance;
if (ImGui.Checkbox("Allow 1+- bone index tolerance", ref neighbor))
{
cfg.AnimationAllowNeighborIndexTolerance = neighbor;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Looser matching on bone matching. Can reduce false blocks happening but also reduces safety and more prone to crashing.");
ImGui.TreePop();
animationTree.MarkContentEnd();
}
}
ImGui.EndChild(); ImGui.EndChild();
ImGui.EndGroup(); ImGui.EndGroup();
ImGui.Separator();
generalSelune.DrawHighlightOnly(ImGui.GetIO().DeltaTime); generalSelune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
} }
} }
@@ -3273,7 +3130,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
return 1f - (elapsed / GeneralTreeHighlightDuration); return 1f - (elapsed / GeneralTreeHighlightDuration);
} }
[StructLayout(LayoutKind.Auto)]
private struct GeneralTreeScope : IDisposable private struct GeneralTreeScope : IDisposable
{ {
private readonly bool _visible; private readonly bool _visible;
@@ -3581,7 +3437,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.DrawHelpText("Controls whether Lightless reduces index textures that exceed the size limit."); _uiShared.DrawHelpText("Controls whether Lightless reduces index textures that exceed the size limit.");
var dimensionOptions = new[] { 512, 1024, 2048, 4096 }; var dimensionOptions = new[] { 512, 1024, 2048, 4096 };
var optionLabels = dimensionOptions.Select(selector: static value => value.ToString()).ToArray(); var optionLabels = dimensionOptions.Select(static value => value.ToString()).ToArray();
var currentDimension = textureConfig.TextureDownscaleMaxDimension; var currentDimension = textureConfig.TextureDownscaleMaxDimension;
var selectedIndex = Array.IndexOf(dimensionOptions, currentDimension); var selectedIndex = Array.IndexOf(dimensionOptions, currentDimension);
if (selectedIndex < 0) if (selectedIndex < 0)

View File

@@ -11,7 +11,6 @@ using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Services; using LightlessSync.UI.Services;
using LightlessSync.UI.Tags; using LightlessSync.UI.Tags;
using LightlessSync.Utils; using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Numerics; using System.Numerics;
@@ -23,7 +22,6 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
private readonly PairUiService _pairUiService; private readonly PairUiService _pairUiService;
private readonly ServerConfigurationManager _serverManager; private readonly ServerConfigurationManager _serverManager;
private readonly ProfileTagService _profileTagService; private readonly ProfileTagService _profileTagService;
private readonly ApiController _apiController;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly UserData? _userData; private readonly UserData? _userData;
private readonly GroupData? _groupData; private readonly GroupData? _groupData;
@@ -62,8 +60,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
GroupData? groupData, GroupData? groupData,
bool isLightfinderContext, bool isLightfinderContext,
string? lightfinderCid, string? lightfinderCid,
PerformanceCollectorService performanceCollector, PerformanceCollectorService performanceCollector)
ApiController apiController)
: base(logger, mediator, BuildWindowTitle( : base(logger, mediator, BuildWindowTitle(
userData, userData,
groupData, groupData,
@@ -97,7 +94,6 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
.Apply(); .Apply();
IsOpen = true; IsOpen = true;
_apiController = apiController;
} }
public Pair? Pair { get; } public Pair? Pair { get; }
@@ -252,33 +248,19 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
ResetBannerTexture(); ResetBannerTexture();
_lastBannerPicture = bannerBytes; _lastBannerPicture = bannerBytes;
} }
string? noteText = null; string? noteText = null;
string statusLabel = _isLightfinderContext ? "Exploring" : "Offline";
var isSelfProfile = !_isLightfinderContext
&& _userData is not null
&& !string.IsNullOrEmpty(_apiController.UID)
&& string.Equals(_userData.UID, _apiController.UID, StringComparison.Ordinal);
string statusLabel = _isLightfinderContext
? "Exploring"
: isSelfProfile ? "Online" : "Offline";
string? visiblePlayerName = null; string? visiblePlayerName = null;
bool directPair = false; bool directPair = false;
bool youPaused = false; bool youPaused = false;
bool theyPaused = false; bool theyPaused = false;
List<string> syncshellLines = []; List<string> syncshellLines = [];
if (!_isLightfinderContext)
{
noteText = _serverManager.GetNoteForUid(_userData!.UID);
}
if (!_isLightfinderContext && Pair != null) if (!_isLightfinderContext && Pair != null)
{ {
var snapshot = _pairUiService.GetSnapshot(); var snapshot = _pairUiService.GetSnapshot();
noteText = _serverManager.GetNoteForUid(Pair.UserData.UID); noteText = _serverManager.GetNoteForUid(Pair.UserData.UID);
statusLabel = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline"); statusLabel = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline");
visiblePlayerName = Pair.IsVisible ? Pair.PlayerName : null; visiblePlayerName = Pair.IsVisible ? Pair.PlayerName : null;
@@ -300,15 +282,11 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
var groupLabel = snapshot.GroupsByGid.TryGetValue(gid, out var groupInfo) var groupLabel = snapshot.GroupsByGid.TryGetValue(gid, out var groupInfo)
? groupInfo.GroupAliasOrGID ? groupInfo.GroupAliasOrGID
: gid; : gid;
var groupNote = _serverManager.GetNoteForGid(gid); var groupNote = _serverManager.GetNoteForGid(gid);
syncshellLines.Add(string.IsNullOrEmpty(groupNote) ? groupLabel : $"{groupNote} ({groupLabel})"); syncshellLines.Add(string.IsNullOrEmpty(groupNote) ? groupLabel : $"{groupNote} ({groupLabel})");
} }
} }
} }
if (isSelfProfile)
statusLabel = "Online";
} }
var presenceTokens = new List<PresenceToken> var presenceTokens = new List<PresenceToken>

View File

@@ -392,27 +392,25 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
} }
UiSharedService.AttachToolTip("When enabled, inactive non-pinned, non-moderator users will be pruned automatically on the server."); UiSharedService.AttachToolTip("When enabled, inactive non-pinned, non-moderator users will be pruned automatically on the server.");
if (!_autoPruneEnabled)
{
ImGui.BeginDisabled();
}
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetNextItemWidth(150); ImGui.SetNextItemWidth(150);
_uiSharedService.DrawCombo(
"Day(s) of inactivity (gets checked hourly)", using (ImRaii.Disabled(!_autoPruneEnabled))
[0, 1, 3, 7, 14, 30, 90], {
(count) => count == 0 ? "2 hours(s)" : count + " day(s)", _uiSharedService.DrawCombo(
selected => "Day(s) of inactivity",
{ [1, 3, 7, 14, 30, 90],
_autoPruneDays = selected; days => $"{days} day(s)",
SavePruneSettings(); selected =>
}, {
_autoPruneDays); _autoPruneDays = selected;
SavePruneSettings();
},
_autoPruneDays);
}
if (!_autoPruneEnabled) if (!_autoPruneEnabled)
{ {
ImGui.EndDisabled();
UiSharedService.ColorTextWrapped( UiSharedService.ColorTextWrapped(
"Automatic prune is currently disabled. Enable it and choose an inactivity threshold to let the server clean up inactive users automatically.", "Automatic prune is currently disabled. Enable it and choose an inactivity threshold to let the server clean up inactive users automatically.",
ImGuiColors.DalamudGrey); ImGuiColors.DalamudGrey);
@@ -595,7 +593,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
_uiSharedService.DrawCombo( _uiSharedService.DrawCombo(
"Day(s) of inactivity", "Day(s) of inactivity",
[0, 1, 3, 7, 14, 30, 90], [0, 1, 3, 7, 14, 30, 90],
(count) => count == 0 ? "2 hours(s)" : count + " day(s)", (count) => count == 0 ? "15 minute(s)" : count + " day(s)",
(selected) => (selected) =>
{ {
_pruneDays = selected; _pruneDays = selected;
@@ -1095,7 +1093,6 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
} }
private void SavePruneSettings() private void SavePruneSettings()
{ {
if (_autoPruneDays <= 0) if (_autoPruneDays <= 0)
@@ -1103,7 +1100,8 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
_autoPruneEnabled = false; _autoPruneEnabled = false;
} }
var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: _autoPruneEnabled, AutoPruneDays: _autoPruneDays); var enabled = _autoPruneEnabled && _autoPruneDays > 0;
var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: enabled, AutoPruneDays: enabled ? _autoPruneDays : 0);
try try
{ {

View File

@@ -28,10 +28,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
private readonly TextureMetadataHelper _textureMetadataHelper; private readonly TextureMetadataHelper _textureMetadataHelper;
private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams; private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams;
private readonly SemaphoreSlim _decompressGate =
new(Math.Max(1, Environment.ProcessorCount / 2), Math.Max(1, Environment.ProcessorCount / 2));
private readonly ConcurrentQueue<string> _deferredCompressionQueue = new();
private volatile bool _disableDirectDownloads; private volatile bool _disableDirectDownloads;
private int _consecutiveDirectDownloadFailures; private int _consecutiveDirectDownloadFailures;
@@ -406,32 +402,76 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
private async Task WaitForDownloadReady(List<DownloadFileTransfer> downloadFileTransfer, Guid requestId, CancellationToken downloadCt) private async Task WaitForDownloadReady(List<DownloadFileTransfer> downloadFileTransfer, Guid requestId, CancellationToken downloadCt)
{ {
while (true) bool alreadyCancelled = false;
try
{ {
downloadCt.ThrowIfCancellationRequested(); CancellationTokenSource localTimeoutCts = new();
localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5));
CancellationTokenSource composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token);
if (_orchestrator.IsDownloadReady(requestId)) while (!_orchestrator.IsDownloadReady(requestId))
break;
using var resp = await _orchestrator.SendRequestAsync(
HttpMethod.Get,
LightlessFiles.RequestCheckQueueFullPath(downloadFileTransfer[0].DownloadUri, requestId),
downloadFileTransfer.Select(t => t.Hash).ToList(),
downloadCt).ConfigureAwait(false);
resp.EnsureSuccessStatusCode();
var body = (await resp.Content.ReadAsStringAsync(downloadCt).ConfigureAwait(false)).Trim();
if (string.Equals(body, "true", StringComparison.OrdinalIgnoreCase) ||
body.Contains("\"ready\":true", StringComparison.OrdinalIgnoreCase))
{ {
break; try
{
await Task.Delay(250, composite.Token).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
if (downloadCt.IsCancellationRequested) throw;
var req = await _orchestrator.SendRequestAsync(
HttpMethod.Get,
LightlessFiles.RequestCheckQueueFullPath(downloadFileTransfer[0].DownloadUri, requestId),
downloadFileTransfer.Select(c => c.Hash).ToList(),
downloadCt).ConfigureAwait(false);
req.EnsureSuccessStatusCode();
localTimeoutCts.Dispose();
composite.Dispose();
localTimeoutCts = new();
localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5));
composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token);
}
} }
await Task.Delay(250, downloadCt).ConfigureAwait(false); localTimeoutCts.Dispose();
} composite.Dispose();
_orchestrator.ClearDownloadRequest(requestId); Logger.LogDebug("Download {requestId} ready", requestId);
}
catch (TaskCanceledException)
{
try
{
await _orchestrator.SendRequestAsync(HttpMethod.Get, LightlessFiles.RequestCancelFullPath(downloadFileTransfer[0].DownloadUri, requestId))
.ConfigureAwait(false);
alreadyCancelled = true;
}
catch
{
// ignore
}
throw;
}
finally
{
if (downloadCt.IsCancellationRequested && !alreadyCancelled)
{
try
{
await _orchestrator.SendRequestAsync(HttpMethod.Get, LightlessFiles.RequestCancelFullPath(downloadFileTransfer[0].DownloadUri, requestId))
.ConfigureAwait(false);
}
catch
{
// ignore
}
}
_orchestrator.ClearDownloadRequest(requestId);
}
} }
private async Task DownloadQueuedBlockFileAsync( private async Task DownloadQueuedBlockFileAsync(
@@ -460,14 +500,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
} }
} }
private void RemoveStatus(string key)
{
lock (_downloadStatusLock)
{
_downloadStatus.Remove(key);
}
}
private async Task DecompressBlockFileAsync( private async Task DecompressBlockFileAsync(
string downloadStatusKey, string downloadStatusKey,
string blockFilePath, string blockFilePath,
@@ -493,53 +525,29 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
if (fileLengthBytes < 0 || fileLengthBytes > int.MaxValue) if (fileLengthBytes < 0 || fileLengthBytes > int.MaxValue)
throw new InvalidDataException($"Invalid block entry length: {fileLengthBytes}"); throw new InvalidDataException($"Invalid block entry length: {fileLengthBytes}");
var len = checked((int)fileLengthBytes);
if (!replacementLookup.TryGetValue(fileHash, out var repl)) if (!replacementLookup.TryGetValue(fileHash, out var repl))
{ {
Logger.LogWarning("{dlName}: No replacement mapping for {fileHash}", downloadLabel, fileHash); Logger.LogWarning("{dlName}: No replacement mapping for {fileHash}", downloadLabel, fileHash);
fileBlockStream.Seek(len, SeekOrigin.Current); // still need to skip bytes:
var skip = checked((int)fileLengthBytes);
fileBlockStream.Position += skip;
continue; continue;
} }
var filePath = _fileDbManager.GetCacheFilePath(fileHash, repl.Extension); var filePath = _fileDbManager.GetCacheFilePath(fileHash, repl.Extension);
Logger.LogTrace("{dlName}: Decompressing {file}:{len} => {dest}", downloadLabel, fileHash, fileLengthBytes, filePath);
Logger.LogDebug("{dlName}: Decompressing {file}:{len} => {dest}", downloadLabel, fileHash, fileLengthBytes, filePath);
var len = checked((int)fileLengthBytes);
var compressed = new byte[len]; var compressed = new byte[len];
await ReadExactlyAsync(fileBlockStream, compressed.AsMemory(0, len), ct).ConfigureAwait(false); await ReadExactlyAsync(fileBlockStream, compressed.AsMemory(0, len), ct).ConfigureAwait(false);
if (len == 0)
{
await File.WriteAllBytesAsync(filePath, Array.Empty<byte>(), ct).ConfigureAwait(false);
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
continue;
}
MungeBuffer(compressed); MungeBuffer(compressed);
var decompressed = LZ4Wrapper.Unwrap(compressed);
await _decompressGate.WaitAsync(ct).ConfigureAwait(false); await _fileCompactor.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false);
try PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
{
// offload CPU-intensive decompression to threadpool to free up worker
await Task.Run(async () =>
{
var sw = System.Diagnostics.Stopwatch.StartNew();
// decompress
var decompressed = LZ4Wrapper.Unwrap(compressed);
Logger.LogTrace("{dlName}: Unwrap {fileHash} took {ms}ms (compressed {c} bytes, decompressed {d} bytes)",
downloadLabel, fileHash, sw.ElapsedMilliseconds, compressed.Length, decompressed?.Length ?? -1);
// write to file without compacting during download
await File.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false);
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
}, ct).ConfigureAwait(false);
}
finally
{
_decompressGate.Release();
}
} }
catch (EndOfStreamException) catch (EndOfStreamException)
{ {
@@ -560,10 +568,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
{ {
Logger.LogError(ex, "{dlName}: Error during block file read", downloadLabel); Logger.LogError(ex, "{dlName}: Error during block file read", downloadLabel);
} }
finally
{
RemoveStatus(downloadStatusKey);
}
} }
public async Task<List<DownloadFileTransfer>> InitiateDownloadList( public async Task<List<DownloadFileTransfer>> InitiateDownloadList(
@@ -601,16 +605,20 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
.. await FilesGetSizes(hashes, ct).ConfigureAwait(false), .. await FilesGetSizes(hashes, ct).ConfigureAwait(false),
]; ];
Logger.LogDebug("Files with size 0 or less: {files}",
string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash)));
foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden)) foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden))
{ {
if (!_orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal))) if (!_orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal)))
_orchestrator.ForbiddenTransfers.Add(new DownloadFileTransfer(dto)); _orchestrator.ForbiddenTransfers.Add(new DownloadFileTransfer(dto));
} }
CurrentDownloads = [.. downloadFileInfoFromService CurrentDownloads = downloadFileInfoFromService
.Distinct() .Distinct()
.Select(d => new DownloadFileTransfer(d)) .Select(d => new DownloadFileTransfer(d))
.Where(d => d.CanBeTransferred)]; .Where(d => d.CanBeTransferred)
.ToList();
return CurrentDownloads; return CurrentDownloads;
} }
@@ -709,16 +717,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
if (gameObjectHandler is not null) if (gameObjectHandler is not null)
Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus)); Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus));
// work based on cpu count and slots
var coreCount = Environment.ProcessorCount;
var baseWorkers = Math.Min(slots, coreCount);
// only add buffer if decompression has capacity AND we have cores to spare
var availableDecompressSlots = _decompressGate.CurrentCount;
var extraWorkers = (availableDecompressSlots > 0 && coreCount >= 6) ? 2 : 0;
// allow some extra workers so downloads can continue while earlier items decompress. // allow some extra workers so downloads can continue while earlier items decompress.
var workerDop = Math.Clamp(baseWorkers + extraWorkers, 2, coreCount); var workerDop = Math.Clamp(slots * 2, 2, 16);
// batch downloads // batch downloads
Task batchTask = batchChunks.Length == 0 Task batchTask = batchChunks.Length == 0
@@ -734,9 +734,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
await Task.WhenAll(batchTask, directTask).ConfigureAwait(false); await Task.WhenAll(batchTask, directTask).ConfigureAwait(false);
// process deferred compressions after all downloads complete
await ProcessDeferredCompressionsAsync(ct).ConfigureAwait(false);
Logger.LogDebug("Download end: {id}", objectName); Logger.LogDebug("Download end: {id}", objectName);
ClearDownload(); ClearDownload();
} }
@@ -761,6 +758,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
try try
{ {
// download (with slot)
var progress = CreateInlineProgress(bytes => AddTransferredBytes(statusKey, bytes)); var progress = CreateInlineProgress(bytes => AddTransferredBytes(statusKey, bytes));
// Download slot held on get // Download slot held on get
@@ -840,13 +838,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
byte[] compressedBytes = await File.ReadAllBytesAsync(tempFilename, ct).ConfigureAwait(false); byte[] compressedBytes = await File.ReadAllBytesAsync(tempFilename, ct).ConfigureAwait(false);
var decompressedBytes = LZ4Wrapper.Unwrap(compressedBytes); var decompressedBytes = LZ4Wrapper.Unwrap(compressedBytes);
await File.WriteAllBytesAsync(finalFilename, decompressedBytes, ct).ConfigureAwait(false); await _fileCompactor.WriteAllBytesAsync(finalFilename, decompressedBytes, ct).ConfigureAwait(false);
PersistFileToStorage(directDownload.Hash, finalFilename, repl.GamePath, skipDownscale); PersistFileToStorage(directDownload.Hash, finalFilename, repl.GamePath, skipDownscale);
MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1); MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1);
Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash); Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash);
RemoveStatus(directDownload.DirectDownloadUrl!);
} }
catch (OperationCanceledException ex) catch (OperationCanceledException ex)
{ {
@@ -941,12 +937,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
if (!_orchestrator.IsInitialized) if (!_orchestrator.IsInitialized)
throw new InvalidOperationException("FileTransferManager is not initialized"); throw new InvalidOperationException("FileTransferManager is not initialized");
// batch request
var response = await _orchestrator.SendRequestAsync( var response = await _orchestrator.SendRequestAsync(
HttpMethod.Get, HttpMethod.Get,
LightlessFiles.ServerFilesGetSizesFullPath(_orchestrator.FilesCdnUri!), LightlessFiles.ServerFilesGetSizesFullPath(_orchestrator.FilesCdnUri!),
hashes, hashes,
ct).ConfigureAwait(false); ct).ConfigureAwait(false);
// ensure success
return await response.Content.ReadFromJsonAsync<List<DownloadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? []; return await response.Content.ReadFromJsonAsync<List<DownloadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? [];
} }
@@ -966,10 +964,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
fi.LastAccessTime = DateTime.Today; fi.LastAccessTime = DateTime.Today;
fi.LastWriteTime = RandomDayInThePast().Invoke(); fi.LastWriteTime = RandomDayInThePast().Invoke();
// queue file for deferred compression instead of compressing immediately
if (_configService.Current.UseCompactor)
_deferredCompressionQueue.Enqueue(filePath);
try try
{ {
var entry = _fileDbManager.CreateCacheEntry(filePath); var entry = _fileDbManager.CreateCacheEntry(filePath);
@@ -995,52 +989,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
private static IProgress<long> CreateInlineProgress(Action<long> callback) => new InlineProgress(callback); private static IProgress<long> CreateInlineProgress(Action<long> callback) => new InlineProgress(callback);
private async Task ProcessDeferredCompressionsAsync(CancellationToken ct)
{
if (_deferredCompressionQueue.IsEmpty)
return;
var filesToCompress = new List<string>();
while (_deferredCompressionQueue.TryDequeue(out var filePath))
{
if (File.Exists(filePath))
filesToCompress.Add(filePath);
}
if (filesToCompress.Count == 0)
return;
Logger.LogDebug("Starting deferred compression of {count} files", filesToCompress.Count);
var compressionWorkers = Math.Clamp(Environment.ProcessorCount / 4, 2, 4);
await Parallel.ForEachAsync(filesToCompress,
new ParallelOptions
{
MaxDegreeOfParallelism = compressionWorkers,
CancellationToken = ct
},
async (filePath, token) =>
{
try
{
await Task.Yield();
if (_configService.Current.UseCompactor && File.Exists(filePath))
{
var bytes = await File.ReadAllBytesAsync(filePath, token).ConfigureAwait(false);
await _fileCompactor.WriteAllBytesAsync(filePath, bytes, token).ConfigureAwait(false);
Logger.LogTrace("Compressed file: {filePath}", filePath);
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to compress file: {filePath}", filePath);
}
}).ConfigureAwait(false);
Logger.LogDebug("Completed deferred compression of {count} files", filesToCompress.Count);
}
private sealed class InlineProgress : IProgress<long> private sealed class InlineProgress : IProgress<long>
{ {
private readonly Action<long> _callback; private readonly Action<long> _callback;

View File

@@ -200,21 +200,5 @@ public partial class ApiController
await UserPushData(new(visibleCharacters, character, censusDto)).ConfigureAwait(false); await UserPushData(new(visibleCharacters, character, censusDto)).ConfigureAwait(false);
} }
public async Task UpdateLocation(LocationDto locationDto, bool offline = false)
{
if (!IsConnected) return;
await _lightlessHub!.SendAsync(nameof(UpdateLocation), locationDto, offline).ConfigureAwait(false);
}
public async Task<(List<LocationWithTimeDto>, List<SharingStatusDto>)> RequestAllLocationInfo()
{
if (!IsConnected) return ([],[]);
return await _lightlessHub!.InvokeAsync<(List<LocationWithTimeDto>, List<SharingStatusDto>)>(nameof(RequestAllLocationInfo)).ConfigureAwait(false);
}
public async Task<bool> ToggleLocationSharing(LocationSharingToggleDto dto)
{
if (!IsConnected) return false;
return await _lightlessHub!.InvokeAsync<bool>(nameof(ToggleLocationSharing), dto).ConfigureAwait(false);
}
} }
#pragma warning restore MA0040 #pragma warning restore MA0040

View File

@@ -260,13 +260,6 @@ public partial class ApiController
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task Client_SendLocationToClient(LocationDto locationDto, DateTimeOffset expireAt)
{
Logger.LogDebug($"{nameof(Client_SendLocationToClient)}: {locationDto.User} {expireAt}");
ExecuteSafely(() => Mediator.Publish(new LocationSharingMessage(locationDto.User, locationDto.Location, expireAt)));
return Task.CompletedTask;
}
public void OnDownloadReady(Action<Guid> act) public void OnDownloadReady(Action<Guid> act)
{ {
if (_initialized) return; if (_initialized) return;
@@ -448,12 +441,6 @@ public partial class ApiController
_lightlessHub!.On(nameof(Client_GposeLobbyPushWorldData), act); _lightlessHub!.On(nameof(Client_GposeLobbyPushWorldData), act);
} }
public void OnReceiveLocation(Action<LocationDto, DateTimeOffset> act)
{
if (_initialized) return;
_lightlessHub!.On(nameof(Client_SendLocationToClient), act);
}
private void ExecuteSafely(Action act) private void ExecuteSafely(Action act)
{ {
try try

View File

@@ -606,7 +606,6 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
OnGposeLobbyPushCharacterData((dto) => _ = Client_GposeLobbyPushCharacterData(dto)); OnGposeLobbyPushCharacterData((dto) => _ = Client_GposeLobbyPushCharacterData(dto));
OnGposeLobbyPushPoseData((dto, data) => _ = Client_GposeLobbyPushPoseData(dto, data)); OnGposeLobbyPushPoseData((dto, data) => _ = Client_GposeLobbyPushPoseData(dto, data));
OnGposeLobbyPushWorldData((dto, data) => _ = Client_GposeLobbyPushWorldData(dto, data)); OnGposeLobbyPushWorldData((dto, data) => _ = Client_GposeLobbyPushWorldData(dto, data));
OnReceiveLocation((dto, time) => _ = Client_SendLocationToClient(dto, time));
_healthCheckTokenSource?.Cancel(); _healthCheckTokenSource?.Cancel();
_healthCheckTokenSource?.Dispose(); _healthCheckTokenSource?.Dispose();
@@ -775,6 +774,5 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
ServerState = state; ServerState = state;
} }
} }
#pragma warning restore MA0040 #pragma warning restore MA0040

View File

@@ -76,19 +76,6 @@
"Microsoft.AspNetCore.SignalR.Common": "10.0.1" "Microsoft.AspNetCore.SignalR.Common": "10.0.1"
} }
}, },
"Microsoft.Extensions.Caching.Memory": {
"type": "Direct",
"requested": "[10.0.1, )",
"resolved": "10.0.1",
"contentHash": "NxqSP0Ky4dZ5ybszdZCqs1X2C70s+dXflqhYBUh/vhcQVTIooNCXIYnLVbafoAFGZMs51d9+rHxveXs0ZC3SQQ==",
"dependencies": {
"Microsoft.Extensions.Caching.Abstractions": "10.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1",
"Microsoft.Extensions.Logging.Abstractions": "10.0.1",
"Microsoft.Extensions.Options": "10.0.1",
"Microsoft.Extensions.Primitives": "10.0.1"
}
},
"Microsoft.Extensions.Hosting": { "Microsoft.Extensions.Hosting": {
"type": "Direct", "type": "Direct",
"requested": "[10.0.1, )", "requested": "[10.0.1, )",
@@ -246,14 +233,6 @@
"Microsoft.AspNetCore.SignalR.Common": "10.0.1" "Microsoft.AspNetCore.SignalR.Common": "10.0.1"
} }
}, },
"Microsoft.Extensions.Caching.Abstractions": {
"type": "Transitive",
"resolved": "10.0.1",
"contentHash": "Vb1vVAQDxHpXVdL9fpOX2BzeV7bbhzG4pAcIKRauRl0/VfkE8mq0f+fYC+gWICh3dlzTZInJ/cTeBS2MgU/XvQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.1"
}
},
"Microsoft.Extensions.Configuration": { "Microsoft.Extensions.Configuration": {
"type": "Transitive", "type": "Transitive",
"resolved": "10.0.1", "resolved": "10.0.1",
@@ -639,7 +618,7 @@
"FlatSharp.Compiler": "[7.9.0, )", "FlatSharp.Compiler": "[7.9.0, )",
"FlatSharp.Runtime": "[7.9.0, )", "FlatSharp.Runtime": "[7.9.0, )",
"OtterGui": "[1.0.0, )", "OtterGui": "[1.0.0, )",
"Penumbra.Api": "[5.13.1, )", "Penumbra.Api": "[5.13.0, )",
"Penumbra.String": "[1.0.7, )" "Penumbra.String": "[1.0.7, )"
} }
}, },