Compare commits
6 Commits
2.0.2.82-D
...
collection
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
699535b68b | ||
|
|
828705cbfb | ||
|
|
d8b4122ec3 | ||
|
|
f6a5c85c2d | ||
|
|
9fcbd68ca2 | ||
|
|
73dee6d9a5 |
@@ -21,6 +21,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
private long _currentFileProgress = 0;
|
private long _currentFileProgress = 0;
|
||||||
private CancellationTokenSource _scanCancellationTokenSource = new();
|
private CancellationTokenSource _scanCancellationTokenSource = new();
|
||||||
private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
|
private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
|
||||||
|
private readonly SemaphoreSlim _dbGate = new(1, 1);
|
||||||
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
|
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
|
||||||
private static readonly HashSet<string> AllowedFileExtensionSet = new(AllowedFileExtensions, StringComparer.OrdinalIgnoreCase);
|
private static readonly HashSet<string> AllowedFileExtensionSet = new(AllowedFileExtensions, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
@@ -68,6 +69,9 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
Logger.LogInformation("Starting Periodic Storage Directory Calculation Task");
|
Logger.LogInformation("Starting Periodic Storage Directory Calculation Task");
|
||||||
var token = _periodicCalculationTokenSource.Token;
|
var token = _periodicCalculationTokenSource.Token;
|
||||||
|
while (IsHalted() && !token.IsCancellationRequested)
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5), token).ConfigureAwait(false);
|
||||||
|
|
||||||
while (!token.IsCancellationRequested)
|
while (!token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -91,6 +95,9 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
public long CurrentFileProgress => _currentFileProgress;
|
public long CurrentFileProgress => _currentFileProgress;
|
||||||
public long FileCacheSize { get; set; }
|
public long FileCacheSize { get; set; }
|
||||||
public long FileCacheDriveFree { get; set; }
|
public long FileCacheDriveFree { get; set; }
|
||||||
|
|
||||||
|
private int _haltCount;
|
||||||
|
private bool IsHalted() => Volatile.Read(ref _haltCount) > 0;
|
||||||
public ConcurrentDictionary<string, int> HaltScanLocks { get; set; } = new(StringComparer.Ordinal);
|
public ConcurrentDictionary<string, int> HaltScanLocks { get; set; } = new(StringComparer.Ordinal);
|
||||||
public bool IsScanRunning => CurrentFileProgress > 0 || TotalFiles > 0;
|
public bool IsScanRunning => CurrentFileProgress > 0 || TotalFiles > 0;
|
||||||
public long TotalFiles { get; private set; }
|
public long TotalFiles { get; private set; }
|
||||||
@@ -98,14 +105,36 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void HaltScan(string source)
|
public void HaltScan(string source)
|
||||||
{
|
{
|
||||||
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
|
HaltScanLocks.AddOrUpdate(source, 1, (_, v) => v + 1);
|
||||||
HaltScanLocks[source]++;
|
Interlocked.Increment(ref _haltCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null);
|
record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null);
|
||||||
private readonly record struct CacheEvictionCandidate(string FullPath, long Size, DateTime LastAccessTime);
|
private readonly record struct CacheEvictionCandidate(string FullPath, long Size, DateTime LastAccessTime);
|
||||||
private readonly Dictionary<string, WatcherChange> _watcherChanges = new(StringComparer.OrdinalIgnoreCase);
|
private readonly object _penumbraGate = new();
|
||||||
private readonly Dictionary<string, WatcherChange> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
|
private Dictionary<string, WatcherChange> _watcherChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private readonly object _lightlessGate = new();
|
||||||
|
private Dictionary<string, WatcherChange> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private Dictionary<string, WatcherChange> DrainPenumbraChanges()
|
||||||
|
{
|
||||||
|
lock (_penumbraGate)
|
||||||
|
{
|
||||||
|
var snapshot = _watcherChanges;
|
||||||
|
_watcherChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, WatcherChange> DrainLightlessChanges()
|
||||||
|
{
|
||||||
|
lock (_lightlessGate)
|
||||||
|
{
|
||||||
|
var snapshot = _lightlessChanges;
|
||||||
|
_lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void StopMonitoring()
|
public void StopMonitoring()
|
||||||
{
|
{
|
||||||
@@ -168,7 +197,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!HasAllowedExtension(e.FullPath)) return;
|
if (!HasAllowedExtension(e.FullPath)) return;
|
||||||
|
|
||||||
lock (_watcherChanges)
|
lock (_lightlessChanges)
|
||||||
{
|
{
|
||||||
_lightlessChanges[e.FullPath] = new(e.ChangeType);
|
_lightlessChanges[e.FullPath] = new(e.ChangeType);
|
||||||
}
|
}
|
||||||
@@ -279,67 +308,58 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private async Task LightlessWatcherExecution()
|
private async Task LightlessWatcherExecution()
|
||||||
{
|
{
|
||||||
_lightlessFswCts = _lightlessFswCts.CancelRecreate();
|
_lightlessFswCts = _lightlessFswCts.CancelRecreate();
|
||||||
var token = _lightlessFswCts.Token;
|
var token = _lightlessFswCts.Token;
|
||||||
var delay = TimeSpan.FromSeconds(5);
|
|
||||||
Dictionary<string, WatcherChange> changes;
|
|
||||||
lock (_lightlessChanges)
|
|
||||||
changes = _lightlessChanges.ToDictionary(t => t.Key, t => t.Value, StringComparer.Ordinal);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
do
|
await Task.Delay(TimeSpan.FromSeconds(10), token).ConfigureAwait(false);
|
||||||
{
|
while (IsHalted() && !token.IsCancellationRequested)
|
||||||
await Task.Delay(delay, token).ConfigureAwait(false);
|
await Task.Delay(250, token).ConfigureAwait(false);
|
||||||
} while (HaltScanLocks.Any(f => f.Value > 0));
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
catch (TaskCanceledException) { return; }
|
||||||
|
|
||||||
lock (_lightlessChanges)
|
var changes = DrainLightlessChanges();
|
||||||
{
|
if (changes.Count > 0)
|
||||||
foreach (var key in changes.Keys)
|
_ = HandleChangesAsync(changes, token);
|
||||||
{
|
|
||||||
_lightlessChanges.Remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HandleChanges(changes);
|
|
||||||
}
|
}
|
||||||
|
private async Task HandleChangesAsync(Dictionary<string, WatcherChange> changes, CancellationToken token)
|
||||||
private void HandleChanges(Dictionary<string, WatcherChange> changes)
|
|
||||||
{
|
{
|
||||||
lock (_fileDbManager)
|
await _dbGate.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var deletedEntries = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Deleted).Select(c => c.Key);
|
var deleted = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Deleted).Select(c => c.Key);
|
||||||
var renamedEntries = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Renamed);
|
var renamed = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Renamed);
|
||||||
var remainingEntries = changes.Where(c => c.Value.ChangeType != WatcherChangeTypes.Deleted).Select(c => c.Key);
|
var remaining = changes.Where(c => c.Value.ChangeType != WatcherChangeTypes.Deleted).Select(c => c.Key);
|
||||||
|
|
||||||
foreach (var entry in deletedEntries)
|
foreach (var entry in deleted)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("FSW Change: Deletion - {val}", entry);
|
Logger.LogDebug("FSW Change: Deletion - {val}", entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var entry in renamedEntries)
|
foreach (var entry in renamed)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("FSW Change: Renamed - {oldVal} => {val}", entry.Value.OldPath, entry.Key);
|
Logger.LogDebug("FSW Change: Renamed - {oldVal} => {val}", entry.Value.OldPath, entry.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var entry in remainingEntries)
|
foreach (var entry in remaining)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("FSW Change: Creation or Change - {val}", entry);
|
Logger.LogDebug("FSW Change: Creation or Change - {val}", entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
var allChanges = deletedEntries
|
var allChanges = deleted
|
||||||
.Concat(renamedEntries.Select(c => c.Value.OldPath!))
|
.Concat(renamed.Select(c => c.Value.OldPath!))
|
||||||
.Concat(renamedEntries.Select(c => c.Key))
|
.Concat(renamed.Select(c => c.Key))
|
||||||
.Concat(remainingEntries)
|
.Concat(remaining)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
_ = _fileDbManager.GetFileCachesByPaths(allChanges);
|
_ = _fileDbManager.GetFileCachesByPaths(allChanges);
|
||||||
|
await _fileDbManager.WriteOutFullCsvAsync(token).ConfigureAwait(false);
|
||||||
_fileDbManager.WriteOutFullCsv();
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dbGate.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,77 +367,97 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_penumbraFswCts = _penumbraFswCts.CancelRecreate();
|
_penumbraFswCts = _penumbraFswCts.CancelRecreate();
|
||||||
var token = _penumbraFswCts.Token;
|
var token = _penumbraFswCts.Token;
|
||||||
Dictionary<string, WatcherChange> changes;
|
|
||||||
lock (_watcherChanges)
|
|
||||||
changes = _watcherChanges.ToDictionary(t => t.Key, t => t.Value, StringComparer.Ordinal);
|
|
||||||
var delay = TimeSpan.FromSeconds(10);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
do
|
await Task.Delay(TimeSpan.FromSeconds(10), token).ConfigureAwait(false);
|
||||||
{
|
while (IsHalted() && !token.IsCancellationRequested)
|
||||||
await Task.Delay(delay, token).ConfigureAwait(false);
|
await Task.Delay(250, token).ConfigureAwait(false);
|
||||||
} while (HaltScanLocks.Any(f => f.Value > 0));
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
catch (TaskCanceledException) { return; }
|
||||||
|
|
||||||
lock (_watcherChanges)
|
var changes = DrainPenumbraChanges();
|
||||||
{
|
if (changes.Count > 0)
|
||||||
foreach (var key in changes.Keys)
|
_ = HandleChangesAsync(changes, token);
|
||||||
{
|
|
||||||
_watcherChanges.Remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HandleChanges(changes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void InvokeScan()
|
public void InvokeScan()
|
||||||
{
|
{
|
||||||
TotalFiles = 0;
|
TotalFiles = 0;
|
||||||
_currentFileProgress = 0;
|
TotalFilesStorage = 0;
|
||||||
|
Interlocked.Exchange(ref _currentFileProgress, 0);
|
||||||
|
|
||||||
_scanCancellationTokenSource = _scanCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
|
_scanCancellationTokenSource = _scanCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
|
||||||
var token = _scanCancellationTokenSource.Token;
|
var token = _scanCancellationTokenSource.Token;
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Starting Full File Scan");
|
TaskCompletionSource scanTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
TotalFiles = 0;
|
|
||||||
_currentFileProgress = 0;
|
|
||||||
while (_dalamudUtil.IsOnFrameworkThread)
|
|
||||||
{
|
|
||||||
Logger.LogWarning("Scanner is on framework, waiting for leaving thread before continuing");
|
|
||||||
await Task.Delay(250, token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread scanThread = new(() =>
|
try
|
||||||
{
|
{
|
||||||
try
|
Logger.LogDebug("Starting Full File Scan");
|
||||||
|
|
||||||
|
while (IsHalted() && !token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_performanceCollector.LogPerformance(this, $"FullFileScan", () => FullFileScan(token));
|
Logger.LogDebug("Scan is halted, waiting...");
|
||||||
|
await Task.Delay(250, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
|
var scanThread = new Thread(() =>
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error during Full File Scan");
|
try
|
||||||
}
|
{
|
||||||
})
|
token.ThrowIfCancellationRequested();
|
||||||
{
|
|
||||||
Priority = ThreadPriority.Lowest,
|
_performanceCollector.LogPerformance(this, $"FullFileScan",
|
||||||
IsBackground = true
|
() => FullFileScan(token));
|
||||||
};
|
|
||||||
scanThread.Start();
|
scanTcs.TrySetResult();
|
||||||
while (scanThread.IsAlive)
|
}
|
||||||
{
|
catch (OperationCanceledException)
|
||||||
await Task.Delay(250, token).ConfigureAwait(false);
|
{
|
||||||
|
scanTcs.TrySetCanceled(token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error during Full File Scan");
|
||||||
|
scanTcs.TrySetException(ex);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Priority = ThreadPriority.Lowest,
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "LightlessSync.FullFileScan"
|
||||||
|
};
|
||||||
|
|
||||||
|
scanThread.Start();
|
||||||
|
|
||||||
|
using var _ = token.Register(() => scanTcs.TrySetCanceled(token));
|
||||||
|
|
||||||
|
await scanTcs.Task.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Full File Scan was canceled.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Unexpected error in InvokeScan task");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
TotalFiles = 0;
|
||||||
|
TotalFilesStorage = 0;
|
||||||
|
Interlocked.Exchange(ref _currentFileProgress, 0);
|
||||||
}
|
}
|
||||||
TotalFiles = 0;
|
|
||||||
_currentFileProgress = 0;
|
|
||||||
}, token);
|
}, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RecalculateFileCacheSize(CancellationToken token)
|
public void RecalculateFileCacheSize(CancellationToken token)
|
||||||
{
|
{
|
||||||
|
if (IsHalted()) return;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) ||
|
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) ||
|
||||||
!Directory.Exists(_configService.Current.CacheFolder))
|
!Directory.Exists(_configService.Current.CacheFolder))
|
||||||
{
|
{
|
||||||
@@ -594,10 +634,20 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void ResumeScan(string source)
|
public void ResumeScan(string source)
|
||||||
{
|
{
|
||||||
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
|
int delta = 0;
|
||||||
|
|
||||||
HaltScanLocks[source]--;
|
HaltScanLocks.AddOrUpdate(source,
|
||||||
if (HaltScanLocks[source] < 0) HaltScanLocks[source] = 0;
|
addValueFactory: _ => 0,
|
||||||
|
updateValueFactory: (_, v) =>
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrEmpty(_);
|
||||||
|
if (v <= 0) return 0;
|
||||||
|
delta = 1;
|
||||||
|
return v - 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (delta == 1)
|
||||||
|
Interlocked.Decrement(ref _haltCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
@@ -621,201 +671,81 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
private void FullFileScan(CancellationToken ct)
|
private void FullFileScan(CancellationToken ct)
|
||||||
{
|
{
|
||||||
TotalFiles = 1;
|
TotalFiles = 1;
|
||||||
|
_currentFileProgress = 0;
|
||||||
|
|
||||||
var penumbraDir = _ipcManager.Penumbra.ModDirectory;
|
var penumbraDir = _ipcManager.Penumbra.ModDirectory;
|
||||||
bool penDirExists = true;
|
var cacheFolder = _configService.Current.CacheFolder;
|
||||||
bool cacheDirExists = true;
|
|
||||||
if (string.IsNullOrEmpty(penumbraDir) || !Directory.Exists(penumbraDir))
|
if (string.IsNullOrEmpty(penumbraDir) || !Directory.Exists(penumbraDir))
|
||||||
{
|
{
|
||||||
penDirExists = false;
|
|
||||||
Logger.LogWarning("Penumbra directory is not set or does not exist.");
|
Logger.LogWarning("Penumbra directory is not set or does not exist.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder))
|
|
||||||
|
if (string.IsNullOrEmpty(cacheFolder) || !Directory.Exists(cacheFolder))
|
||||||
{
|
{
|
||||||
cacheDirExists = false;
|
|
||||||
Logger.LogWarning("Lightless Cache directory is not set or does not exist.");
|
Logger.LogWarning("Lightless Cache directory is not set or does not exist.");
|
||||||
}
|
|
||||||
if (!penDirExists || !cacheDirExists)
|
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var previousThreadPriority = Thread.CurrentThread.Priority;
|
var prevPriority = Thread.CurrentThread.Priority;
|
||||||
Thread.CurrentThread.Priority = ThreadPriority.Lowest;
|
Thread.CurrentThread.Priority = ThreadPriority.Lowest;
|
||||||
Logger.LogDebug("Getting files from {penumbra} and {storage}", penumbraDir, _configService.Current.CacheFolder);
|
|
||||||
|
|
||||||
Dictionary<string, string[]> penumbraFiles = new(StringComparer.Ordinal);
|
try
|
||||||
foreach (var folder in Directory.EnumerateDirectories(penumbraDir!))
|
|
||||||
{
|
{
|
||||||
try
|
Logger.LogDebug("Getting files from {penumbra} and {storage}", penumbraDir, cacheFolder);
|
||||||
|
|
||||||
|
var onDiskPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
static bool IsExcludedPenumbraPath(string path)
|
||||||
|
=> path.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| path.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| path.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var folder in Directory.EnumerateDirectories(penumbraDir))
|
||||||
{
|
{
|
||||||
penumbraFiles[folder] =
|
ct.ThrowIfCancellationRequested();
|
||||||
[
|
|
||||||
.. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
|
|
||||||
.AsParallel()
|
|
||||||
.Where(f => HasAllowedExtension(f)
|
|
||||||
&& !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogWarning(ex, "Could not enumerate path {path}", folder);
|
|
||||||
}
|
|
||||||
Thread.Sleep(50);
|
|
||||||
if (ct.IsCancellationRequested) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var allCacheFiles = Directory.GetFiles(_configService.Current.CacheFolder, "*.*", SearchOption.TopDirectoryOnly)
|
try
|
||||||
.AsParallel()
|
|
||||||
.Where(f =>
|
|
||||||
{
|
|
||||||
var val = f.Split('\\')[^1];
|
|
||||||
return val.Length == 40 || (val.Split('.').FirstOrDefault()?.Length ?? 0) == 40;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ct.IsCancellationRequested) return;
|
|
||||||
|
|
||||||
var allScannedFiles = (penumbraFiles.SelectMany(k => k.Value))
|
|
||||||
.Concat(allCacheFiles)
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToDictionary(t => t.ToLowerInvariant(), t => false, StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
TotalFiles = allScannedFiles.Count;
|
|
||||||
Thread.CurrentThread.Priority = previousThreadPriority;
|
|
||||||
|
|
||||||
Thread.Sleep(TimeSpan.FromSeconds(2));
|
|
||||||
|
|
||||||
if (ct.IsCancellationRequested) return;
|
|
||||||
|
|
||||||
// scan files from database
|
|
||||||
var threadCount = Math.Clamp((int)(Environment.ProcessorCount / 2.0f), 2, 8);
|
|
||||||
|
|
||||||
List<FileCacheEntity> entitiesToRemove = [];
|
|
||||||
List<FileCacheEntity> entitiesToUpdate = [];
|
|
||||||
Lock sync = new();
|
|
||||||
Thread[] workerThreads = new Thread[threadCount];
|
|
||||||
|
|
||||||
ConcurrentQueue<FileCacheEntity> fileCaches = new(_fileDbManager.GetAllFileCaches());
|
|
||||||
|
|
||||||
TotalFilesStorage = fileCaches.Count;
|
|
||||||
|
|
||||||
for (int i = 0; i < threadCount; i++)
|
|
||||||
{
|
|
||||||
Logger.LogTrace("Creating Thread {i}", i);
|
|
||||||
workerThreads[i] = new((tcounter) =>
|
|
||||||
{
|
|
||||||
var threadNr = (int)tcounter!;
|
|
||||||
Logger.LogTrace("Spawning Worker Thread {i}", threadNr);
|
|
||||||
while (!ct.IsCancellationRequested && fileCaches.TryDequeue(out var workload))
|
|
||||||
{
|
{
|
||||||
try
|
foreach (var file in Directory.EnumerateFiles(folder, "*.*", SearchOption.AllDirectories))
|
||||||
{
|
{
|
||||||
if (ct.IsCancellationRequested) return;
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (!_ipcManager.Penumbra.APIAvailable)
|
if (!HasAllowedExtension(file)) continue;
|
||||||
{
|
if (IsExcludedPenumbraPath(file)) continue;
|
||||||
Logger.LogWarning("Penumbra not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var validatedCacheResult = _fileDbManager.ValidateFileCacheEntity(workload);
|
onDiskPaths.Add(file);
|
||||||
if (validatedCacheResult.State != FileState.RequireDeletion)
|
|
||||||
{
|
|
||||||
lock (sync) { allScannedFiles[validatedCacheResult.FileCache.ResolvedFilepath] = true; }
|
|
||||||
}
|
|
||||||
if (validatedCacheResult.State == FileState.RequireUpdate)
|
|
||||||
{
|
|
||||||
Logger.LogTrace("To update: {path}", validatedCacheResult.FileCache.ResolvedFilepath);
|
|
||||||
lock (sync) { entitiesToUpdate.Add(validatedCacheResult.FileCache); }
|
|
||||||
}
|
|
||||||
else if (validatedCacheResult.State == FileState.RequireDeletion)
|
|
||||||
{
|
|
||||||
Logger.LogTrace("To delete: {path}", validatedCacheResult.FileCache.ResolvedFilepath);
|
|
||||||
lock (sync) { entitiesToRemove.Add(validatedCacheResult.FileCache); }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
if (workload != null)
|
|
||||||
{
|
|
||||||
Logger.LogWarning(ex, "Failed validating {path}", workload.ResolvedFilepath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Logger.LogWarning(ex, "Failed validating unknown workload");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Interlocked.Increment(ref _currentFileProgress);
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
Logger.LogTrace("Ending Worker Thread {i}", threadNr);
|
{
|
||||||
})
|
Logger.LogWarning(ex, "Could not enumerate path {path}", folder);
|
||||||
{
|
}
|
||||||
Priority = ThreadPriority.Lowest,
|
|
||||||
IsBackground = true
|
|
||||||
};
|
|
||||||
workerThreads[i].Start(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (!ct.IsCancellationRequested && workerThreads.Any(u => u.IsAlive))
|
|
||||||
{
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ct.IsCancellationRequested) return;
|
|
||||||
|
|
||||||
Logger.LogTrace("Threads exited");
|
|
||||||
|
|
||||||
if (!_ipcManager.Penumbra.APIAvailable)
|
|
||||||
{
|
|
||||||
Logger.LogWarning("Penumbra not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entitiesToUpdate.Count != 0 || entitiesToRemove.Count != 0)
|
|
||||||
{
|
|
||||||
foreach (var entity in entitiesToUpdate)
|
|
||||||
{
|
|
||||||
_fileDbManager.UpdateHashedFile(entity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var entity in entitiesToRemove)
|
foreach (var file in Directory.EnumerateFiles(cacheFolder, "*.*", SearchOption.TopDirectoryOnly))
|
||||||
{
|
{
|
||||||
_fileDbManager.RemoveHashedFile(entity.Hash, entity.PrefixedFilePath);
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var name = Path.GetFileName(file);
|
||||||
|
var stem = Path.GetFileNameWithoutExtension(file);
|
||||||
|
|
||||||
|
if (name.Length == 40 || stem.Length == 40)
|
||||||
|
onDiskPaths.Add(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
_fileDbManager.WriteOutFullCsv();
|
var threadCount = Math.Clamp((int)(Environment.ProcessorCount / 2.0f), 2, 8);
|
||||||
}
|
|
||||||
|
|
||||||
Logger.LogTrace("Scanner validated existing db files");
|
var fileCacheList = _fileDbManager.GetAllFileCaches();
|
||||||
|
var fileCaches = new ConcurrentQueue<FileCacheEntity>(fileCacheList);
|
||||||
|
|
||||||
if (!_ipcManager.Penumbra.APIAvailable)
|
TotalFilesStorage = fileCaches.Count;
|
||||||
{
|
TotalFiles = onDiskPaths.Count + TotalFilesStorage;
|
||||||
Logger.LogWarning("Penumbra not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ct.IsCancellationRequested) return;
|
var validOrPresentInDb = new ConcurrentDictionary<string, byte>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var entitiesToUpdate = new ConcurrentBag<FileCacheEntity>();
|
||||||
var newFiles = allScannedFiles.Where(c => !c.Value).Select(c => c.Key).ToList();
|
var entitiesToRemove = new ConcurrentBag<FileCacheEntity>();
|
||||||
foreach (var cachePath in newFiles)
|
|
||||||
{
|
|
||||||
if (ct.IsCancellationRequested) break;
|
|
||||||
ProcessOne(cachePath);
|
|
||||||
Interlocked.Increment(ref _currentFileProgress);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.LogTrace("Scanner added {count} new files to db", newFiles.Count);
|
|
||||||
|
|
||||||
void ProcessOne(string? cachePath)
|
|
||||||
{
|
|
||||||
if (_fileDbManager == null || _ipcManager?.Penumbra == null || cachePath == null)
|
|
||||||
{
|
|
||||||
Logger.LogTrace("Potential null in db: {isDbNull} penumbra: {isPenumbraNull} cachepath: {isPathNull}",
|
|
||||||
_fileDbManager == null, _ipcManager?.Penumbra == null, cachePath == null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_ipcManager.Penumbra.APIAvailable)
|
if (!_ipcManager.Penumbra.APIAvailable)
|
||||||
{
|
{
|
||||||
@@ -823,33 +753,161 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
Thread[] workerThreads = new Thread[threadCount];
|
||||||
|
for (int i = 0; i < threadCount; i++)
|
||||||
{
|
{
|
||||||
var entry = _fileDbManager.CreateFileEntry(cachePath);
|
workerThreads[i] = new Thread(tcounter =>
|
||||||
if (entry == null) _ = _fileDbManager.CreateCacheEntry(cachePath);
|
{
|
||||||
|
var threadNr = (int)tcounter!;
|
||||||
|
Logger.LogTrace("Spawning Worker Thread {i}", threadNr);
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested && fileCaches.TryDequeue(out var workload))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
|
||||||
|
if (!_ipcManager.Penumbra.APIAvailable)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var validated = _fileDbManager.ValidateFileCacheEntity(workload);
|
||||||
|
|
||||||
|
if (validated.State != FileState.RequireDeletion)
|
||||||
|
{
|
||||||
|
validOrPresentInDb.TryAdd(validated.FileCache.ResolvedFilepath, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validated.State == FileState.RequireUpdate)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("To update: {path}", validated.FileCache.ResolvedFilepath);
|
||||||
|
entitiesToUpdate.Add(validated.FileCache);
|
||||||
|
}
|
||||||
|
else if (validated.State == FileState.RequireDeletion)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("To delete: {path}", validated.FileCache.ResolvedFilepath);
|
||||||
|
entitiesToRemove.Add(validated.FileCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (workload != null)
|
||||||
|
Logger.LogWarning(ex, "Failed validating {path}", workload.ResolvedFilepath);
|
||||||
|
else
|
||||||
|
Logger.LogWarning(ex, "Failed validating unknown workload");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _currentFileProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogTrace("Ending Worker Thread {i}", threadNr);
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Priority = ThreadPriority.Lowest,
|
||||||
|
IsBackground = true
|
||||||
|
};
|
||||||
|
|
||||||
|
workerThreads[i].Start(i);
|
||||||
}
|
}
|
||||||
catch (IOException ioex)
|
|
||||||
|
while (!ct.IsCancellationRequested && workerThreads.Any(t => t.IsAlive))
|
||||||
{
|
{
|
||||||
Logger.LogDebug(ioex, "File busy or locked: {file}", cachePath);
|
ct.WaitHandle.WaitOne(250);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
Logger.LogTrace("Scanner validated existing db files");
|
||||||
|
|
||||||
|
if (!_ipcManager.Penumbra.APIAvailable)
|
||||||
{
|
{
|
||||||
Logger.LogWarning(ex, "Failed adding {file}", cachePath);
|
Logger.LogWarning("Penumbra not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var didMutateDb = false;
|
||||||
|
|
||||||
|
foreach (var entity in entitiesToUpdate)
|
||||||
|
{
|
||||||
|
didMutateDb = true;
|
||||||
|
_fileDbManager.UpdateHashedFile(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entity in entitiesToRemove)
|
||||||
|
{
|
||||||
|
didMutateDb = true;
|
||||||
|
_fileDbManager.RemoveHashedFile(entity.Hash, entity.PrefixedFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (didMutateDb)
|
||||||
|
_fileDbManager.WriteOutFullCsv();
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
var newFiles = onDiskPaths.Where(p => !validOrPresentInDb.ContainsKey(p)).ToList();
|
||||||
|
|
||||||
|
foreach (var path in newFiles)
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
ProcessOne(path);
|
||||||
|
Interlocked.Increment(ref _currentFileProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogTrace("Scanner added {count} new files to db", newFiles.Count);
|
||||||
|
|
||||||
|
void ProcessOne(string? filePath)
|
||||||
|
{
|
||||||
|
if (filePath == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_ipcManager.Penumbra.APIAvailable)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Penumbra not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entry = _fileDbManager.CreateFileEntry(filePath);
|
||||||
|
if (entry == null)
|
||||||
|
_ = _fileDbManager.CreateCacheEntry(filePath);
|
||||||
|
}
|
||||||
|
catch (IOException ioex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(ioex, "File busy or locked: {file}", filePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed adding {file}", filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug("Scan complete");
|
||||||
|
|
||||||
|
TotalFiles = 0;
|
||||||
|
_currentFileProgress = 0;
|
||||||
|
|
||||||
|
if (!_configService.Current.InitialScanComplete)
|
||||||
|
{
|
||||||
|
_configService.Current.InitialScanComplete = true;
|
||||||
|
_configService.Save();
|
||||||
|
|
||||||
|
StartLightlessWatcher(cacheFolder);
|
||||||
|
StartPenumbraWatcher(penumbraDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
Logger.LogDebug("Scan complete");
|
|
||||||
TotalFiles = 0;
|
|
||||||
_currentFileProgress = 0;
|
|
||||||
entitiesToRemove.Clear();
|
|
||||||
allScannedFiles.Clear();
|
|
||||||
|
|
||||||
if (!_configService.Current.InitialScanComplete)
|
|
||||||
{
|
{
|
||||||
_configService.Current.InitialScanComplete = true;
|
// normal cancellation
|
||||||
_configService.Save();
|
}
|
||||||
StartLightlessWatcher(_configService.Current.CacheFolder);
|
catch (Exception ex)
|
||||||
StartPenumbraWatcher(penumbraDir);
|
{
|
||||||
|
Logger.LogError(ex, "Error during Full File Scan");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Thread.CurrentThread.Priority = prevPriority;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +213,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
/// <param name="bytes">Bytes that have to be written</param>
|
/// <param name="bytes">Bytes that have to be written</param>
|
||||||
/// <param name="token">Cancellation Token for interupts</param>
|
/// <param name="token">Cancellation Token for interupts</param>
|
||||||
/// <returns>Writing Task</returns>
|
/// <returns>Writing Task</returns>
|
||||||
public async Task WriteAllBytesAsync(string filePath, byte[] bytes, CancellationToken token)
|
public async Task WriteAllBytesAsync(string filePath, byte[] bytes, CancellationToken token, bool enqueueCompaction = true)
|
||||||
{
|
{
|
||||||
var dir = Path.GetDirectoryName(filePath);
|
var dir = Path.GetDirectoryName(filePath);
|
||||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||||
@@ -221,6 +221,12 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
|
|
||||||
await File.WriteAllBytesAsync(filePath, bytes, token).ConfigureAwait(false);
|
await File.WriteAllBytesAsync(filePath, bytes, token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (enqueueCompaction && _lightlessConfigService.Current.UseCompactor)
|
||||||
|
EnqueueCompaction(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestCompaction(string filePath)
|
||||||
|
{
|
||||||
if (_lightlessConfigService.Current.UseCompactor)
|
if (_lightlessConfigService.Current.UseCompactor)
|
||||||
EnqueueCompaction(filePath);
|
EnqueueCompaction(filePath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
foreach (var handler in _playerRelatedPointers)
|
foreach (var handler in _playerRelatedPointers)
|
||||||
{
|
{
|
||||||
var address = (nint)handler.Address;
|
var address = handler.Address;
|
||||||
if (address != nint.Zero)
|
if (address != nint.Zero)
|
||||||
{
|
{
|
||||||
tempMap[address] = handler;
|
tempMap[address] = handler;
|
||||||
|
|||||||
@@ -81,7 +81,10 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
|||||||
=> _collections.RemoveTemporaryCollectionAsync(logger, applicationId, collectionId);
|
=> _collections.RemoveTemporaryCollectionAsync(logger, applicationId, collectionId);
|
||||||
|
|
||||||
public Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths)
|
public Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths)
|
||||||
=> _collections.SetTemporaryModsAsync(logger, applicationId, collectionId, modPaths);
|
=> _collections.SetTemporaryModsAsync(logger, applicationId, collectionId, modPaths, "Player");
|
||||||
|
|
||||||
|
public Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths, string scope)
|
||||||
|
=> _collections.SetTemporaryModsAsync(logger, applicationId, collectionId, modPaths, scope);
|
||||||
|
|
||||||
public Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collectionId, string manipulationData)
|
public Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collectionId, string manipulationData)
|
||||||
=> _collections.SetManipulationDataAsync(logger, applicationId, collectionId, manipulationData);
|
=> _collections.SetManipulationDataAsync(logger, applicationId, collectionId, manipulationData);
|
||||||
|
|||||||
@@ -92,25 +92,43 @@ public sealed class PenumbraCollections : PenumbraBase
|
|||||||
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths)
|
public async Task SetTemporaryModsAsync(
|
||||||
|
ILogger logger,
|
||||||
|
Guid applicationId,
|
||||||
|
Guid collectionId,
|
||||||
|
Dictionary<string, string> modPaths,
|
||||||
|
string scope)
|
||||||
{
|
{
|
||||||
if (!IsAvailable || collectionId == Guid.Empty)
|
if (!IsAvailable || collectionId == Guid.Empty)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var modName = $"LightlessChara_Files_{applicationId:N}_{scope}";
|
||||||
|
|
||||||
|
var normalized = new Dictionary<string, string>(modPaths.Count, StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var kvp in modPaths)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(kvp.Key) || string.IsNullOrWhiteSpace(kvp.Value))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var gamePath = kvp.Key.Replace('\\', '/').ToLowerInvariant();
|
||||||
|
normalized[gamePath] = kvp.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
await DalamudUtil.RunOnFrameworkThread(() =>
|
await DalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
foreach (var mod in modPaths)
|
foreach (var mod in normalized)
|
||||||
{
|
logger.LogTrace("[{ApplicationId}] {ModName}: {From} => {To}", applicationId, modName, mod.Key, mod.Value);
|
||||||
logger.LogTrace("[{ApplicationId}] Change: {From} => {To}", applicationId, mod.Key, mod.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var removeResult = _removeTemporaryMod.Invoke("LightlessChara_Files", collectionId, 0);
|
var removeResult = _removeTemporaryMod.Invoke(modName, collectionId, 0);
|
||||||
logger.LogTrace("[{ApplicationId}] Removing temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, removeResult);
|
logger.LogTrace("[{ApplicationId}] Removing temp mod {ModName} for {CollectionId}, Success: {Result}",
|
||||||
|
applicationId, modName, collectionId, removeResult);
|
||||||
|
|
||||||
var addResult = _addTemporaryMod.Invoke("LightlessChara_Files", collectionId, modPaths, string.Empty, 0);
|
if (normalized.Count == 0)
|
||||||
logger.LogTrace("[{ApplicationId}] Setting temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, addResult);
|
return;
|
||||||
|
|
||||||
|
var addResult = _addTemporaryMod.Invoke(modName, collectionId, normalized, string.Empty, 0);
|
||||||
|
logger.LogTrace("[{ApplicationId}] Setting temp mod {ModName} for {CollectionId}, Success: {Result}",
|
||||||
|
applicationId, modName, collectionId, addResult);
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +189,7 @@ public sealed class PenumbraCollections : PenumbraBase
|
|||||||
Logger.LogDebug("Cleaning up stale temporary collection {CollectionName} ({CollectionId})", name, collectionId);
|
Logger.LogDebug("Cleaning up stale temporary collection {CollectionName} ({CollectionId})", name, collectionId);
|
||||||
var deleteResult = await DalamudUtil.RunOnFrameworkThread(() =>
|
var deleteResult = await DalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
var result = (PenumbraApiEc)_removeTemporaryCollection.Invoke(collectionId);
|
var result = _removeTemporaryCollection.Invoke(collectionId);
|
||||||
Logger.LogTrace("Cleanup RemoveTemporaryCollection result for {CollectionName} ({CollectionId}): {Result}", name, collectionId, result);
|
Logger.LogTrace("Cleanup RemoveTemporaryCollection result for {CollectionName} ({CollectionId}): {Result}", name, collectionId, result);
|
||||||
return result;
|
return result;
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ 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 bool EnableParticleEffects { get; set; } = true;
|
public bool EnableParticleEffects { get; set; } = true;
|
||||||
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
|
|
||||||
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Unsafe;
|
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Unsafe;
|
||||||
public bool AnimationAllowOneBasedShift { get; set; } = false;
|
public bool AnimationAllowOneBasedShift { get; set; } = false;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
public class PenumbraJanitorConfig : ILightlessConfiguration
|
||||||
|
{
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
|
||||||
|
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace LightlessSync.LightlessConfiguration;
|
||||||
|
|
||||||
|
public class PenumbraJanitorConfigService : ConfigurationServiceBase<PenumbraJanitorConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "penumbra-collections.json";
|
||||||
|
|
||||||
|
public PenumbraJanitorConfigService(string configDir) : base(configDir)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using LightlessSync.API.Data.Enum;
|
using Dalamud.Plugin.Services;
|
||||||
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
@@ -11,6 +12,7 @@ public class GameObjectHandlerFactory
|
|||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
private readonly IObjectTable _objectTable;
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||||
|
|
||||||
@@ -18,12 +20,14 @@ public class GameObjectHandlerFactory
|
|||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
PerformanceCollectorService performanceCollectorService,
|
PerformanceCollectorService performanceCollectorService,
|
||||||
LightlessMediator lightlessMediator,
|
LightlessMediator lightlessMediator,
|
||||||
IServiceProvider serviceProvider)
|
IServiceProvider serviceProvider,
|
||||||
|
IObjectTable objectTable)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_performanceCollectorService = performanceCollectorService;
|
_performanceCollectorService = performanceCollectorService;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
|
_objectTable = objectTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GameObjectHandler> Create(ObjectKind objectKind, Func<nint> getAddressFunc, bool isWatched = false)
|
public async Task<GameObjectHandler> Create(ObjectKind objectKind, Func<nint> getAddressFunc, bool isWatched = false)
|
||||||
@@ -36,6 +40,7 @@ public class GameObjectHandlerFactory
|
|||||||
dalamudUtilService,
|
dalamudUtilService,
|
||||||
objectKind,
|
objectKind,
|
||||||
getAddressFunc,
|
getAddressFunc,
|
||||||
|
_objectTable,
|
||||||
isWatched)).ConfigureAwait(false);
|
isWatched)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using LightlessSync.API.Data.Enum;
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
@@ -9,11 +9,12 @@ 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 LightlessSync.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.ExceptionServices;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Factories;
|
namespace LightlessSync.PlayerData.Factories;
|
||||||
|
|
||||||
@@ -119,45 +120,28 @@ public class PlayerDataFactory
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly int _characterGameObjectOffset =
|
||||||
|
(int)Marshal.OffsetOf<Character>(nameof(Character.GameObject));
|
||||||
|
|
||||||
|
private static readonly int _gameObjectDrawObjectOffset =
|
||||||
|
(int)Marshal.OffsetOf<GameObject>(nameof(GameObject.DrawObject));
|
||||||
|
|
||||||
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
|
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
|
||||||
=> await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false);
|
=> await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectSafe(playerPointer))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
private unsafe static bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
|
private static bool CheckForNullDrawObjectSafe(nint playerPointer)
|
||||||
{
|
{
|
||||||
if (playerPointer == IntPtr.Zero)
|
if (playerPointer == nint.Zero)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (!IsPointerValid(playerPointer))
|
var drawObjPtrAddress = playerPointer + _characterGameObjectOffset + _gameObjectDrawObjectOffset;
|
||||||
|
|
||||||
|
// Read the DrawObject pointer from memory
|
||||||
|
if (!MemoryProcessProbe.TryReadIntPtr(drawObjPtrAddress, out var drawObj))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
var character = (Character*)playerPointer;
|
return drawObj == nint.Zero;
|
||||||
if (character == null)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
var gameObject = &character->GameObject;
|
|
||||||
if (gameObject == null)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (!IsPointerValid((IntPtr)gameObject))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return gameObject->DrawObject == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsPointerValid(IntPtr ptr)
|
|
||||||
{
|
|
||||||
if (ptr == IntPtr.Zero)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_ = Marshal.ReadByte(ptr);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsCacheFresh(CacheEntry entry)
|
private static bool IsCacheFresh(CacheEntry entry)
|
||||||
@@ -173,7 +157,7 @@ public class PlayerDataFactory
|
|||||||
if (_characterBuildCache.TryGetValue(key, out var cached) && IsCacheFresh(cached) && !_characterBuildInflight.ContainsKey(key))
|
if (_characterBuildCache.TryGetValue(key, out var cached) && IsCacheFresh(cached) && !_characterBuildInflight.ContainsKey(key))
|
||||||
return cached.Fragment;
|
return cached.Fragment;
|
||||||
|
|
||||||
var buildTask = _characterBuildInflight.GetOrAdd(key, _ => BuildAndCacheAsync(obj, key));
|
var buildTask = _characterBuildInflight.GetOrAdd(key, valueFactory: k => BuildAndCacheAsync(obj, k));
|
||||||
|
|
||||||
if (_characterBuildCache.TryGetValue(key, out cached))
|
if (_characterBuildCache.TryGetValue(key, out cached))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,45 +1,68 @@
|
|||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using Dalamud.Game.ClientState.Objects.Enums;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
|
|
||||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
|
||||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Handlers;
|
namespace LightlessSync.PlayerData.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Game object handler for managing game object state and updates
|
||||||
|
/// </summary>
|
||||||
public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighPriorityMediatorSubscriber
|
public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighPriorityMediatorSubscriber
|
||||||
{
|
{
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly IObjectTable _objectTable;
|
||||||
private readonly Func<IntPtr> _getAddress;
|
private readonly Func<IntPtr> _getAddress;
|
||||||
private readonly bool _isOwnedObject;
|
private readonly bool _isOwnedObject;
|
||||||
private readonly PerformanceCollectorService _performanceCollector;
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
private readonly object _frameworkUpdateGate = new();
|
private readonly Lock _frameworkUpdateGate = new();
|
||||||
private bool _frameworkUpdateSubscribed;
|
private bool _frameworkUpdateSubscribed;
|
||||||
private byte _classJob = 0;
|
private byte _classJob = 0;
|
||||||
private Task? _delayedZoningTask;
|
private Task? _delayedZoningTask;
|
||||||
private bool _haltProcessing = false;
|
private bool _haltProcessing = false;
|
||||||
private CancellationTokenSource _zoningCts = new();
|
private CancellationTokenSource _zoningCts = new();
|
||||||
|
|
||||||
public GameObjectHandler(ILogger<GameObjectHandler> logger, PerformanceCollectorService performanceCollector,
|
/// <summary>
|
||||||
LightlessMediator mediator, DalamudUtilService dalamudUtil, ObjectKind objectKind, Func<IntPtr> getAddress, bool ownedObject = true) : base(logger, mediator)
|
/// Constructor for GameObjectHandler
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger</param>
|
||||||
|
/// <param name="performanceCollector">Performance Collector</param>
|
||||||
|
/// <param name="mediator">Lightless Mediator</param>
|
||||||
|
/// <param name="dalamudUtil">Dalamud Utilties Service</param>
|
||||||
|
/// <param name="objectKind">Object kind of Object</param>
|
||||||
|
/// <param name="getAddress">Get Adress</param>
|
||||||
|
/// <param name="objectTable">Object table of Dalamud</param>
|
||||||
|
/// <param name="ownedObject">Object is owned by user</param>
|
||||||
|
public GameObjectHandler(
|
||||||
|
ILogger<GameObjectHandler> logger,
|
||||||
|
PerformanceCollectorService performanceCollector,
|
||||||
|
LightlessMediator mediator,
|
||||||
|
DalamudUtilService dalamudUtil,
|
||||||
|
ObjectKind objectKind,
|
||||||
|
Func<IntPtr> getAddress,
|
||||||
|
IObjectTable objectTable,
|
||||||
|
bool ownedObject = true) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_performanceCollector = performanceCollector;
|
_performanceCollector = performanceCollector;
|
||||||
ObjectKind = objectKind;
|
ObjectKind = objectKind;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_objectTable = objectTable;
|
||||||
|
|
||||||
_getAddress = () =>
|
_getAddress = () =>
|
||||||
{
|
{
|
||||||
_dalamudUtil.EnsureIsOnFramework();
|
_dalamudUtil.EnsureIsOnFramework();
|
||||||
return getAddress.Invoke();
|
return getAddress.Invoke();
|
||||||
};
|
};
|
||||||
|
|
||||||
_isOwnedObject = ownedObject;
|
_isOwnedObject = ownedObject;
|
||||||
Name = string.Empty;
|
Name = string.Empty;
|
||||||
|
|
||||||
if (ownedObject)
|
if (ownedObject)
|
||||||
{
|
{
|
||||||
Mediator.Subscribe<TransientResourceChangedMessage>(this, (msg) =>
|
Mediator.Subscribe<TransientResourceChangedMessage>(this, msg =>
|
||||||
{
|
{
|
||||||
if (_delayedZoningTask?.IsCompleted ?? true)
|
if (_delayedZoningTask?.IsCompleted ?? true)
|
||||||
{
|
{
|
||||||
@@ -49,43 +72,36 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isOwnedObject)
|
EnableFrameworkUpdates();
|
||||||
{
|
|
||||||
EnableFrameworkUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
|
Mediator.Subscribe<ZoneSwitchEndMessage>(this, _ => ZoneSwitchEnd());
|
||||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
|
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ => ZoneSwitchStart());
|
||||||
|
|
||||||
Mediator.Subscribe<CutsceneStartMessage>(this, (_) =>
|
Mediator.Subscribe<CutsceneStartMessage>(this, _ => _haltProcessing = true);
|
||||||
{
|
Mediator.Subscribe<CutsceneEndMessage>(this, _ =>
|
||||||
_haltProcessing = true;
|
|
||||||
});
|
|
||||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) =>
|
|
||||||
{
|
{
|
||||||
_haltProcessing = false;
|
_haltProcessing = false;
|
||||||
ZoneSwitchEnd();
|
ZoneSwitchEnd();
|
||||||
});
|
});
|
||||||
Mediator.Subscribe<PenumbraStartRedrawMessage>(this, (msg) =>
|
|
||||||
|
Mediator.Subscribe<PenumbraStartRedrawMessage>(this, msg =>
|
||||||
{
|
{
|
||||||
if (msg.Address == Address)
|
if (msg.Address == Address) _haltProcessing = true;
|
||||||
{
|
|
||||||
_haltProcessing = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
Mediator.Subscribe<PenumbraEndRedrawMessage>(this, (msg) =>
|
Mediator.Subscribe<PenumbraEndRedrawMessage>(this, msg =>
|
||||||
{
|
{
|
||||||
if (msg.Address == Address)
|
if (msg.Address == Address) _haltProcessing = false;
|
||||||
{
|
|
||||||
_haltProcessing = false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Mediator.Publish(new GameObjectHandlerCreatedMessage(this, _isOwnedObject));
|
Mediator.Publish(new GameObjectHandlerCreatedMessage(this, _isOwnedObject));
|
||||||
|
|
||||||
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
_dalamudUtil.EnsureIsOnFramework();
|
||||||
|
CheckAndUpdateObject(allowPublish: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draw Condition Enum
|
||||||
|
/// </summary>
|
||||||
public enum DrawCondition
|
public enum DrawCondition
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
@@ -96,6 +112,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
ModelFilesInSlotLoaded
|
ModelFilesInSlotLoaded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Properties
|
||||||
public IntPtr Address { get; private set; }
|
public IntPtr Address { get; private set; }
|
||||||
public DrawCondition CurrentDrawCondition { get; set; } = DrawCondition.None;
|
public DrawCondition CurrentDrawCondition { get; set; } = DrawCondition.None;
|
||||||
public byte Gender { get; private set; }
|
public byte Gender { get; private set; }
|
||||||
@@ -106,18 +123,21 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
public byte TribeId { get; private set; }
|
public byte TribeId { get; private set; }
|
||||||
private byte[] CustomizeData { get; set; } = new byte[26];
|
private byte[] CustomizeData { get; set; } = new byte[26];
|
||||||
private IntPtr DrawObjectAddress { get; set; }
|
private IntPtr DrawObjectAddress { get; set; }
|
||||||
private byte[] EquipSlotData { get; set; } = new byte[40];
|
|
||||||
private ushort[] MainHandData { get; set; } = new ushort[3];
|
|
||||||
private ushort[] OffHandData { get; set; } = new ushort[3];
|
|
||||||
|
|
||||||
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<Dalamud.Game.ClientState.Objects.Types.ICharacter> act, CancellationToken token)
|
/// <summary>
|
||||||
|
/// Act on framework thread after ensuring no draw condition
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="act">Action of Character</param>
|
||||||
|
/// <param name="token">Cancellation Token</param>
|
||||||
|
/// <returns>Task Completion</returns>
|
||||||
|
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<ICharacter> act, CancellationToken token)
|
||||||
{
|
{
|
||||||
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
EnsureLatestObjectState();
|
EnsureLatestObjectState();
|
||||||
if (CurrentDrawCondition != DrawCondition.None) return true;
|
if (CurrentDrawCondition != DrawCondition.None) return true;
|
||||||
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
||||||
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
if (gameObj is ICharacter chara)
|
||||||
{
|
{
|
||||||
act.Invoke(chara);
|
act.Invoke(chara);
|
||||||
}
|
}
|
||||||
@@ -128,6 +148,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compare Name And Throw if not equal
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">Name that will be compared to Object Handler.</param>
|
||||||
|
/// <exception cref="InvalidOperationException">Not equal if thrown</exception>
|
||||||
public void CompareNameAndThrow(string name)
|
public void CompareNameAndThrow(string name)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Name, name, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(Name, name, StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -140,11 +165,18 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dalamud.Game.ClientState.Objects.Types.IGameObject? GetGameObject()
|
/// <summary>
|
||||||
|
/// Gets the game object from the address
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Gane object</returns>
|
||||||
|
public IGameObject? GetGameObject()
|
||||||
{
|
{
|
||||||
return _dalamudUtil.CreateGameObject(Address);
|
return _dalamudUtil.CreateGameObject(Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalidate the object handler
|
||||||
|
/// </summary>
|
||||||
public void Invalidate()
|
public void Invalidate()
|
||||||
{
|
{
|
||||||
Address = IntPtr.Zero;
|
Address = IntPtr.Zero;
|
||||||
@@ -153,25 +185,62 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
_haltProcessing = false;
|
_haltProcessing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refresh the object handler state
|
||||||
|
/// </summary>
|
||||||
public void Refresh()
|
public void Refresh()
|
||||||
{
|
{
|
||||||
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is Being Drawn Run On Framework Asyncronously
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Object is being run in framework</returns>
|
||||||
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
||||||
{
|
{
|
||||||
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override ToString method for GameObjectHandler
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>String</returns>
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
var owned = _isOwnedObject ? "Self" : "Other";
|
var owned = _isOwnedObject ? "Self" : "Other";
|
||||||
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
|
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try Get Object By Address from Object Table
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">Object address</param>
|
||||||
|
/// <returns>Game Object of adress</returns>
|
||||||
|
private IGameObject? TryGetObjectByAddress(nint address)
|
||||||
|
{
|
||||||
|
if (address == nint.Zero) return null;
|
||||||
|
|
||||||
|
// Search object table
|
||||||
|
foreach (var obj in _objectTable)
|
||||||
|
{
|
||||||
|
if (obj is null) continue;
|
||||||
|
if (obj.Address == address)
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks and updates the object state
|
||||||
|
/// </summary>
|
||||||
private void CheckAndUpdateObject() => CheckAndUpdateObject(allowPublish: true);
|
private void CheckAndUpdateObject() => CheckAndUpdateObject(allowPublish: true);
|
||||||
|
|
||||||
private unsafe void CheckAndUpdateObject(bool allowPublish)
|
/// <summary>
|
||||||
|
/// Checks and updates the object state with option to allow publish
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="allowPublish">Allows to publish the object</param>
|
||||||
|
private void CheckAndUpdateObject(bool allowPublish)
|
||||||
{
|
{
|
||||||
var prevAddr = Address;
|
var prevAddr = Address;
|
||||||
var prevDrawObj = DrawObjectAddress;
|
var prevDrawObj = DrawObjectAddress;
|
||||||
@@ -179,127 +248,140 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
|
|
||||||
Address = _getAddress();
|
Address = _getAddress();
|
||||||
|
|
||||||
if (Address != IntPtr.Zero)
|
IGameObject? obj = null;
|
||||||
|
ICharacter? chara = null;
|
||||||
|
|
||||||
|
if (Address != nint.Zero)
|
||||||
{
|
{
|
||||||
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
|
// Try get object
|
||||||
DrawObjectAddress = (IntPtr)gameObject->DrawObject;
|
obj = TryGetObjectByAddress(Address);
|
||||||
EntityId = gameObject->EntityId;
|
|
||||||
|
|
||||||
var chara = (Character*)Address;
|
if (obj is not null)
|
||||||
nameString = chara->GameObject.NameString;
|
|
||||||
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
|
|
||||||
Name = nameString;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DrawObjectAddress = IntPtr.Zero;
|
|
||||||
EntityId = uint.MaxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
CurrentDrawCondition = IsBeingDrawnUnsafe();
|
|
||||||
|
|
||||||
if (_haltProcessing || !allowPublish) return;
|
|
||||||
|
|
||||||
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
|
||||||
bool addrDiff = Address != prevAddr;
|
|
||||||
|
|
||||||
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
var chara = (Character*)Address;
|
|
||||||
var drawObj = (DrawObject*)DrawObjectAddress;
|
|
||||||
var objType = drawObj->Object.GetObjectType();
|
|
||||||
var isHuman = objType == ObjectType.CharacterBase
|
|
||||||
&& ((CharacterBase*)drawObj)->GetModelType() == CharacterBase.ModelType.Human;
|
|
||||||
|
|
||||||
nameString ??= ((Character*)Address)->GameObject.NameString;
|
|
||||||
var nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
|
|
||||||
if (nameChange) Name = nameString;
|
|
||||||
|
|
||||||
bool equipDiff = false;
|
|
||||||
|
|
||||||
if (isHuman)
|
|
||||||
{
|
{
|
||||||
var classJob = chara->CharacterData.ClassJob;
|
EntityId = obj.EntityId;
|
||||||
if (classJob != _classJob)
|
|
||||||
{
|
|
||||||
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
|
|
||||||
_classJob = classJob;
|
|
||||||
Mediator.Publish(new ClassJobChangedMessage(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
|
DrawObjectAddress = Address;
|
||||||
|
|
||||||
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
// Name update
|
||||||
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
nameString = obj.Name.TextValue ?? string.Empty;
|
||||||
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
|
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
|
||||||
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
|
Name = nameString;
|
||||||
|
|
||||||
if (equipDiff)
|
chara = obj as ICharacter;
|
||||||
Logger.LogTrace("Checking [{this}] equip data as human from draw obj, result: {diff}", this, equipDiff);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
equipDiff = CompareAndUpdateEquipByteData((byte*)Unsafe.AsPointer(ref chara->DrawData.EquipmentModelIds[0]));
|
DrawObjectAddress = nint.Zero;
|
||||||
if (equipDiff)
|
EntityId = uint.MaxValue;
|
||||||
Logger.LogTrace("Checking [{this}] equip data from game obj, result: {diff}", this, equipDiff);
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DrawObjectAddress = nint.Zero;
|
||||||
|
EntityId = uint.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update draw condition
|
||||||
|
CurrentDrawCondition = IsBeingDrawnSafe(obj, chara);
|
||||||
|
|
||||||
|
if (_haltProcessing || !allowPublish) return;
|
||||||
|
|
||||||
|
// Determine differences
|
||||||
|
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
||||||
|
bool addrDiff = Address != prevAddr;
|
||||||
|
|
||||||
|
// Name change check
|
||||||
|
bool nameChange = false;
|
||||||
|
if (nameString is not null)
|
||||||
|
{
|
||||||
|
nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
|
||||||
|
if (nameChange) Name = nameString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customize data change check
|
||||||
|
bool customizeDiff = false;
|
||||||
|
if (chara is not null)
|
||||||
|
{
|
||||||
|
// Class job change check
|
||||||
|
var classJob = chara.ClassJob.RowId;
|
||||||
|
if (classJob != _classJob)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
|
||||||
|
_classJob = (byte)classJob;
|
||||||
|
Mediator.Publish(new ClassJobChangedMessage(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (equipDiff && !_isOwnedObject) // send the message out immediately and cancel out, no reason to continue if not self
|
// Customize data comparison
|
||||||
|
customizeDiff = CompareAndUpdateCustomizeData(chara.Customize);
|
||||||
|
|
||||||
|
// Census update publish
|
||||||
|
if (_isOwnedObject && ObjectKind == ObjectKind.Player && chara.Customize.Length > (int)CustomizeIndex.Tribe)
|
||||||
{
|
{
|
||||||
Logger.LogTrace("[{this}] Changed", this);
|
var gender = chara.Customize[(int)CustomizeIndex.Gender];
|
||||||
return;
|
var raceId = chara.Customize[(int)CustomizeIndex.Race];
|
||||||
}
|
var tribeId = chara.Customize[(int)CustomizeIndex.Tribe];
|
||||||
|
|
||||||
bool customizeDiff = false;
|
if (gender != Gender || raceId != RaceId || tribeId != TribeId)
|
||||||
|
|
||||||
if (isHuman)
|
|
||||||
{
|
|
||||||
var gender = ((Human*)drawObj)->Customize.Sex;
|
|
||||||
var raceId = ((Human*)drawObj)->Customize.Race;
|
|
||||||
var tribeId = ((Human*)drawObj)->Customize.Tribe;
|
|
||||||
|
|
||||||
if (_isOwnedObject && ObjectKind == ObjectKind.Player
|
|
||||||
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
|
|
||||||
{
|
{
|
||||||
Mediator.Publish(new CensusUpdateMessage(gender, raceId, tribeId));
|
Mediator.Publish(new CensusUpdateMessage(gender, raceId, tribeId));
|
||||||
Gender = gender;
|
Gender = gender;
|
||||||
RaceId = raceId;
|
RaceId = raceId;
|
||||||
TribeId = tribeId;
|
TribeId = tribeId;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
customizeDiff = CompareAndUpdateCustomizeData(((Human*)drawObj)->Customize.Data);
|
|
||||||
if (customizeDiff)
|
|
||||||
Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
customizeDiff = CompareAndUpdateCustomizeData(chara->DrawData.CustomizeData.Data);
|
|
||||||
if (customizeDiff)
|
|
||||||
Logger.LogTrace("Checking [{this}] customize data from game obj, result: {diff}", this, equipDiff);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((addrDiff || drawObjDiff || equipDiff || customizeDiff || nameChange) && _isOwnedObject)
|
if ((addrDiff || drawObjDiff || customizeDiff || nameChange) && _isOwnedObject)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("[{this}] Changed, Sending CreateCacheObjectMessage", this);
|
Logger.LogDebug("[{this}] Changed, Sending CreateCacheObjectMessage", this);
|
||||||
Mediator.Publish(new CreateCacheForObjectMessage(this));
|
Mediator.Publish(new CreateCacheForObjectMessage(this));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (addrDiff || drawObjDiff)
|
else if (addrDiff || drawObjDiff)
|
||||||
{
|
{
|
||||||
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
if (Address == nint.Zero)
|
||||||
|
CurrentDrawCondition = DrawCondition.ObjectZero;
|
||||||
|
else if (DrawObjectAddress == nint.Zero)
|
||||||
|
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
||||||
|
|
||||||
Logger.LogTrace("[{this}] Changed", this);
|
Logger.LogTrace("[{this}] Changed", this);
|
||||||
|
|
||||||
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
|
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
|
||||||
{
|
|
||||||
Mediator.Publish(new ClearCacheForObjectMessage(this));
|
Mediator.Publish(new ClearCacheForObjectMessage(this));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe bool CompareAndUpdateCustomizeData(Span<byte> customizeData)
|
/// <summary>
|
||||||
|
/// Is object being drawn safe check
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">Object thats being checked</param>
|
||||||
|
/// <param name="chara">Character of the object</param>
|
||||||
|
/// <returns>Draw Condition of character</returns>
|
||||||
|
private DrawCondition IsBeingDrawnSafe(IGameObject? obj, ICharacter? chara)
|
||||||
|
{
|
||||||
|
// Object zero check
|
||||||
|
if (Address == nint.Zero) return DrawCondition.ObjectZero;
|
||||||
|
if (obj is null) return DrawCondition.DrawObjectZero;
|
||||||
|
|
||||||
|
// Draw Object check
|
||||||
|
if (chara is not null && (chara.Customize is null || chara.Customize.Length == 0))
|
||||||
|
return DrawCondition.DrawObjectZero;
|
||||||
|
|
||||||
|
return DrawCondition.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compare and update customize data of character
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customizeData">Customize+ data of object</param>
|
||||||
|
/// <returns>Successfully applied or not</returns>
|
||||||
|
private bool CompareAndUpdateCustomizeData(ReadOnlySpan<byte> customizeData)
|
||||||
{
|
{
|
||||||
bool hasChanges = false;
|
bool hasChanges = false;
|
||||||
|
|
||||||
for (int i = 0; i < customizeData.Length; i++)
|
// Resize if needed
|
||||||
|
var len = Math.Min(customizeData.Length, CustomizeData.Length);
|
||||||
|
for (int i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
var data = customizeData[i];
|
var data = customizeData[i];
|
||||||
if (CustomizeData[i] != data)
|
if (CustomizeData[i] != data)
|
||||||
@@ -312,48 +394,9 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
return hasChanges;
|
return hasChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe bool CompareAndUpdateEquipByteData(byte* equipSlotData)
|
/// <summary>
|
||||||
{
|
/// Framework update method
|
||||||
bool hasChanges = false;
|
/// </summary>
|
||||||
for (int i = 0; i < EquipSlotData.Length; i++)
|
|
||||||
{
|
|
||||||
var data = equipSlotData[i];
|
|
||||||
if (EquipSlotData[i] != data)
|
|
||||||
{
|
|
||||||
EquipSlotData[i] = data;
|
|
||||||
hasChanges = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasChanges;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe bool CompareAndUpdateMainHand(Weapon* weapon)
|
|
||||||
{
|
|
||||||
if ((nint)weapon == nint.Zero) return false;
|
|
||||||
bool hasChanges = false;
|
|
||||||
hasChanges |= weapon->ModelSetId != MainHandData[0];
|
|
||||||
MainHandData[0] = weapon->ModelSetId;
|
|
||||||
hasChanges |= weapon->Variant != MainHandData[1];
|
|
||||||
MainHandData[1] = weapon->Variant;
|
|
||||||
hasChanges |= weapon->SecondaryId != MainHandData[2];
|
|
||||||
MainHandData[2] = weapon->SecondaryId;
|
|
||||||
return hasChanges;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe bool CompareAndUpdateOffHand(Weapon* weapon)
|
|
||||||
{
|
|
||||||
if ((nint)weapon == nint.Zero) return false;
|
|
||||||
bool hasChanges = false;
|
|
||||||
hasChanges |= weapon->ModelSetId != OffHandData[0];
|
|
||||||
OffHandData[0] = weapon->ModelSetId;
|
|
||||||
hasChanges |= weapon->Variant != OffHandData[1];
|
|
||||||
OffHandData[1] = weapon->Variant;
|
|
||||||
hasChanges |= weapon->SecondaryId != OffHandData[2];
|
|
||||||
OffHandData[2] = weapon->SecondaryId;
|
|
||||||
return hasChanges;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FrameworkUpdate()
|
private void FrameworkUpdate()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -367,6 +410,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is object being drawn check
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Is being drawn</returns>
|
||||||
private bool IsBeingDrawn()
|
private bool IsBeingDrawn()
|
||||||
{
|
{
|
||||||
EnsureLatestObjectState();
|
EnsureLatestObjectState();
|
||||||
@@ -381,6 +428,9 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
return CurrentDrawCondition != DrawCondition.None;
|
return CurrentDrawCondition != DrawCondition.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures the latest object state
|
||||||
|
/// </summary>
|
||||||
private void EnsureLatestObjectState()
|
private void EnsureLatestObjectState()
|
||||||
{
|
{
|
||||||
if (_haltProcessing || !_frameworkUpdateSubscribed)
|
if (_haltProcessing || !_frameworkUpdateSubscribed)
|
||||||
@@ -389,6 +439,9 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables framework updates for the object handler
|
||||||
|
/// </summary>
|
||||||
private void EnableFrameworkUpdates()
|
private void EnableFrameworkUpdates()
|
||||||
{
|
{
|
||||||
lock (_frameworkUpdateGate)
|
lock (_frameworkUpdateGate)
|
||||||
@@ -403,24 +456,9 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe DrawCondition IsBeingDrawnUnsafe()
|
/// <summary>
|
||||||
{
|
/// Zone switch end handling
|
||||||
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
/// </summary>
|
||||||
if (DrawObjectAddress == IntPtr.Zero) return DrawCondition.DrawObjectZero;
|
|
||||||
var visibilityFlags = ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags;
|
|
||||||
if (visibilityFlags != VisibilityFlags.None) return DrawCondition.RenderFlags;
|
|
||||||
|
|
||||||
if (ObjectKind == ObjectKind.Player)
|
|
||||||
{
|
|
||||||
var modelInSlotLoaded = (((CharacterBase*)DrawObjectAddress)->HasModelInSlotLoaded != 0);
|
|
||||||
if (modelInSlotLoaded) return DrawCondition.ModelInSlotLoaded;
|
|
||||||
var modelFilesInSlotLoaded = (((CharacterBase*)DrawObjectAddress)->HasModelFilesInSlotLoaded != 0);
|
|
||||||
if (modelFilesInSlotLoaded) return DrawCondition.ModelFilesInSlotLoaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DrawCondition.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ZoneSwitchEnd()
|
private void ZoneSwitchEnd()
|
||||||
{
|
{
|
||||||
if (!_isOwnedObject) return;
|
if (!_isOwnedObject) return;
|
||||||
@@ -431,7 +469,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
catch (ObjectDisposedException)
|
catch (ObjectDisposedException)
|
||||||
{
|
{
|
||||||
// ignore
|
// ignore canelled after disposed
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -439,6 +477,9 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zone switch start handling
|
||||||
|
/// </summary>
|
||||||
private void ZoneSwitchStart()
|
private void ZoneSwitchStart()
|
||||||
{
|
{
|
||||||
if (!_isOwnedObject) return;
|
if (!_isOwnedObject) return;
|
||||||
|
|||||||
461
LightlessSync/PlayerData/Handlers/OwnedObjectHandler.cs
Normal file
461
LightlessSync/PlayerData/Handlers/OwnedObjectHandler.cs
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.Enums;
|
||||||
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using LightlessSync.API.Data;
|
||||||
|
using LightlessSync.Interop.Ipc;
|
||||||
|
using LightlessSync.PlayerData.Factories;
|
||||||
|
using LightlessSync.PlayerData.Pairs;
|
||||||
|
using LightlessSync.Services;
|
||||||
|
using LightlessSync.Services.ActorTracking;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||||
|
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||||
|
|
||||||
|
namespace LightlessSync.PlayerData.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owned object handler for applying changes to owned objects.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class OwnedObjectHandler
|
||||||
|
{
|
||||||
|
// Debug information for owned object resolution
|
||||||
|
internal readonly record struct OwnedResolveDebug(
|
||||||
|
DateTime? ResolvedAtUtc,
|
||||||
|
nint Address,
|
||||||
|
ushort? ObjectIndex,
|
||||||
|
string Stage,
|
||||||
|
string? FailureReason)
|
||||||
|
{
|
||||||
|
public string? AddressHex => Address == nint.Zero ? null : $"0x{Address:X}";
|
||||||
|
public static OwnedResolveDebug Empty => new(null, nint.Zero, null, string.Empty, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OwnedResolveDebug _minionResolveDebug = OwnedResolveDebug.Empty;
|
||||||
|
public OwnedResolveDebug MinionResolveDebug => _minionResolveDebug;
|
||||||
|
|
||||||
|
// Dependencies
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly GameObjectHandlerFactory _handlerFactory;
|
||||||
|
private readonly IpcManager _ipc;
|
||||||
|
private readonly ActorObjectService _actorObjectService;
|
||||||
|
private readonly IObjectTable _objectTable;
|
||||||
|
|
||||||
|
// Timeouts for fully loaded checks
|
||||||
|
private const int _fullyLoadedTimeoutMsPlayer = 30000;
|
||||||
|
private const int _fullyLoadedTimeoutMsOther = 5000;
|
||||||
|
|
||||||
|
public OwnedObjectHandler(
|
||||||
|
ILogger logger,
|
||||||
|
DalamudUtilService dalamudUtil,
|
||||||
|
GameObjectHandlerFactory handlerFactory,
|
||||||
|
IpcManager ipc,
|
||||||
|
ActorObjectService actorObjectService,
|
||||||
|
IObjectTable objectTable)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_handlerFactory = handlerFactory;
|
||||||
|
_ipc = ipc;
|
||||||
|
_actorObjectService = actorObjectService;
|
||||||
|
_objectTable = objectTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the specified changes to the owned object of the given kind.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="applicationId">Application ID of the Character Object</param>
|
||||||
|
/// <param name="kind">Object Kind of the given object</param>
|
||||||
|
/// <param name="changes">Changes of the object</param>
|
||||||
|
/// <param name="data">Data of the object</param>
|
||||||
|
/// <param name="playerHandler">Owner of the object</param>
|
||||||
|
/// <param name="penumbraCollection">Collection if needed</param>
|
||||||
|
/// <param name="customizeIds">Customizing identications for the object</param>
|
||||||
|
/// <param name="token">Cancellation Token</param>
|
||||||
|
/// <returns>Successfully applied or not</returns>
|
||||||
|
public async Task<bool> ApplyAsync(
|
||||||
|
Guid applicationId,
|
||||||
|
ObjectKind kind,
|
||||||
|
HashSet<PlayerChanges> changes,
|
||||||
|
CharacterData data,
|
||||||
|
GameObjectHandler playerHandler,
|
||||||
|
Guid penumbraCollection,
|
||||||
|
Dictionary<ObjectKind, Guid?> customizeIds,
|
||||||
|
CancellationToken token)
|
||||||
|
{
|
||||||
|
// Validate player handler
|
||||||
|
if (playerHandler.Address == nint.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Create handler for owned object
|
||||||
|
var handler = await CreateHandlerAsync(kind, playerHandler, token).ConfigureAwait(false);
|
||||||
|
if (handler is null || handler.Address == nint.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Determine if we have file replacements for this kind
|
||||||
|
bool hasFileReplacements =
|
||||||
|
kind != ObjectKind.Player
|
||||||
|
&& data.FileReplacements.TryGetValue(kind, out var repls)
|
||||||
|
&& repls is { Count: > 0 };
|
||||||
|
|
||||||
|
// Determine if we should assign a Penumbra collection
|
||||||
|
bool shouldAssignCollection =
|
||||||
|
kind != ObjectKind.Player
|
||||||
|
&& hasFileReplacements
|
||||||
|
&& penumbraCollection != Guid.Empty
|
||||||
|
&& _ipc.Penumbra.APIAvailable;
|
||||||
|
|
||||||
|
// Determine if only IPC-only changes are being made for player
|
||||||
|
bool isPlayerIpcOnly =
|
||||||
|
kind == ObjectKind.Player
|
||||||
|
&& changes.Count > 0
|
||||||
|
&& changes.All(c => c is PlayerChanges.Honorific
|
||||||
|
or PlayerChanges.Moodles
|
||||||
|
or PlayerChanges.PetNames
|
||||||
|
or PlayerChanges.Heels);
|
||||||
|
|
||||||
|
// Wait for drawing to complete
|
||||||
|
await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Determine timeouts
|
||||||
|
var drawTimeoutMs = handler.ObjectKind == ObjectKind.Player ? 30000 : 5000;
|
||||||
|
var fullyLoadedTimeoutMs = handler.ObjectKind == ObjectKind.Player ? _fullyLoadedTimeoutMsPlayer : _fullyLoadedTimeoutMsOther;
|
||||||
|
|
||||||
|
// Wait for drawing to complete
|
||||||
|
await _dalamudUtil
|
||||||
|
.WaitWhileCharacterIsDrawing(_logger, handler, applicationId, drawTimeoutMs, token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (handler.Address != nint.Zero)
|
||||||
|
{
|
||||||
|
// Wait for fully loaded
|
||||||
|
var loaded = await _actorObjectService
|
||||||
|
.WaitForFullyLoadedAsync(handler.Address, token, fullyLoadedTimeoutMs)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!loaded)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("[{appId}] {kind}: not fully loaded in time, skipping for now", applicationId, kind);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Assign Penumbra collection if needed
|
||||||
|
if (shouldAssignCollection)
|
||||||
|
{
|
||||||
|
// Get object index
|
||||||
|
var objIndex = await _dalamudUtil
|
||||||
|
.RunOnFrameworkThread(() => handler.GetGameObject()?.ObjectIndex)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!objIndex.HasValue)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("[{appId}] {kind}: ObjectIndex not available yet, cannot assign collection", applicationId, kind);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign collection
|
||||||
|
await _ipc.Penumbra
|
||||||
|
.AssignTemporaryCollectionAsync(_logger, penumbraCollection, objIndex.Value)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
|
||||||
|
// Apply each change
|
||||||
|
foreach (var change in changes.OrderBy(c => (int)c))
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Handle each change type
|
||||||
|
switch (change)
|
||||||
|
{
|
||||||
|
case PlayerChanges.Customize:
|
||||||
|
if (data.CustomizePlusData.TryGetValue(kind, out var customizeData) && !string.IsNullOrEmpty(customizeData))
|
||||||
|
tasks.Add(ApplyCustomizeAsync(handler.Address, customizeData, kind, customizeIds));
|
||||||
|
else if (customizeIds.TryGetValue(kind, out var existingId))
|
||||||
|
tasks.Add(RevertCustomizeAsync(existingId, kind, customizeIds));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.Glamourer:
|
||||||
|
if (data.GlamourerData.TryGetValue(kind, out var glamourerData) && !string.IsNullOrEmpty(glamourerData))
|
||||||
|
tasks.Add(_ipc.Glamourer.ApplyAllAsync(_logger, handler, glamourerData, applicationId, token));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.Heels:
|
||||||
|
if (kind == ObjectKind.Player && !string.IsNullOrEmpty(data.HeelsData))
|
||||||
|
tasks.Add(_ipc.Heels.SetOffsetForPlayerAsync(handler.Address, data.HeelsData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.Honorific:
|
||||||
|
if (kind == ObjectKind.Player && !string.IsNullOrEmpty(data.HonorificData))
|
||||||
|
tasks.Add(_ipc.Honorific.SetTitleAsync(handler.Address, data.HonorificData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.Moodles:
|
||||||
|
if (kind == ObjectKind.Player && !string.IsNullOrEmpty(data.MoodlesData))
|
||||||
|
tasks.Add(_ipc.Moodles.SetStatusAsync(handler.Address, data.MoodlesData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.PetNames:
|
||||||
|
if (kind == ObjectKind.Player && !string.IsNullOrEmpty(data.PetNamesData))
|
||||||
|
tasks.Add(_ipc.PetNames.SetPlayerData(handler.Address, data.PetNamesData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.ModFiles:
|
||||||
|
case PlayerChanges.ModManip:
|
||||||
|
case PlayerChanges.ForcedRedraw:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Await all tasks for change applications
|
||||||
|
if (tasks.Count > 0)
|
||||||
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Determine if redraw is needed
|
||||||
|
bool needsRedraw =
|
||||||
|
_ipc.Penumbra.APIAvailable
|
||||||
|
&& (
|
||||||
|
shouldAssignCollection
|
||||||
|
|| changes.Contains(PlayerChanges.ForcedRedraw)
|
||||||
|
|| changes.Contains(PlayerChanges.ModFiles)
|
||||||
|
|| changes.Contains(PlayerChanges.ModManip)
|
||||||
|
|| changes.Contains(PlayerChanges.Glamourer)
|
||||||
|
|| changes.Contains(PlayerChanges.Customize)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skip redraw for player if only IPC-only changes were made
|
||||||
|
if (isPlayerIpcOnly)
|
||||||
|
needsRedraw = false;
|
||||||
|
|
||||||
|
// Perform redraw if needed
|
||||||
|
if (needsRedraw && _ipc.Penumbra.APIAvailable)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"[{appId}] {kind}: Redrawing ownedTarget={isOwned} (needsRedraw={needsRedraw})",
|
||||||
|
applicationId, kind, kind != ObjectKind.Player, needsRedraw);
|
||||||
|
|
||||||
|
await _ipc.Penumbra
|
||||||
|
.RedrawAsync(_logger, handler, applicationId, token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(handler, playerHandler))
|
||||||
|
handler.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a GameObjectHandler for the owned object of the specified kind.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="kind">Object kind of the handler</param>
|
||||||
|
/// <param name="playerHandler">Owner of the given object</param>
|
||||||
|
/// <param name="token">Cancellation Token</param>
|
||||||
|
/// <returns>Handler for the GameObject with the handler</returns>
|
||||||
|
private async Task<GameObjectHandler?> CreateHandlerAsync(ObjectKind kind, GameObjectHandler playerHandler, CancellationToken token)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Debug info setter
|
||||||
|
void SetMinionDebug(string stage, string? failure, nint addr = default, ushort? objIndex = null)
|
||||||
|
{
|
||||||
|
if (kind != ObjectKind.MinionOrMount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_minionResolveDebug = new OwnedResolveDebug(
|
||||||
|
DateTime.UtcNow,
|
||||||
|
addr,
|
||||||
|
objIndex,
|
||||||
|
stage,
|
||||||
|
failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct return for player
|
||||||
|
if (kind == ObjectKind.Player)
|
||||||
|
return playerHandler;
|
||||||
|
|
||||||
|
// First, try direct retrieval via Dalamud API
|
||||||
|
var playerPtr = playerHandler.Address;
|
||||||
|
if (playerPtr == nint.Zero)
|
||||||
|
{
|
||||||
|
SetMinionDebug("player_ptr_zero", "playerHandler.Address == 0");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try direct retrieval
|
||||||
|
nint ownedPtr = kind switch
|
||||||
|
{
|
||||||
|
ObjectKind.Companion => await _dalamudUtil.GetCompanionAsync(playerPtr).ConfigureAwait(false),
|
||||||
|
ObjectKind.MinionOrMount => await _dalamudUtil.GetMinionOrMountAsync(playerPtr).ConfigureAwait(false),
|
||||||
|
ObjectKind.Pet => await _dalamudUtil.GetPetAsync(playerPtr).ConfigureAwait(false),
|
||||||
|
_ => nint.Zero
|
||||||
|
};
|
||||||
|
|
||||||
|
// If that fails, scan the object table for owned objects
|
||||||
|
var stage = ownedPtr != nint.Zero ? "direct" : "direct_miss";
|
||||||
|
|
||||||
|
// Owner ID based scan
|
||||||
|
if (ownedPtr == nint.Zero)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Get owner entity ID
|
||||||
|
var ownerEntityId = playerHandler.EntityId;
|
||||||
|
if (ownerEntityId == 0 || ownerEntityId == uint.MaxValue)
|
||||||
|
{
|
||||||
|
// Read unsafe
|
||||||
|
ownerEntityId = await _dalamudUtil
|
||||||
|
.RunOnFrameworkThread(() => ReadEntityIdSafe(playerHandler))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ownerEntityId != 0 && ownerEntityId != uint.MaxValue)
|
||||||
|
{
|
||||||
|
// Scan for owned object
|
||||||
|
ownedPtr = await _dalamudUtil
|
||||||
|
.RunOnFrameworkThread(() => FindOwnedByOwnerIdSafe(kind, ownerEntityId))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
stage = ownedPtr != nint.Zero ? "owner_scan" : "owner_scan_miss";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stage = "owner_id_unavailable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ownedPtr == nint.Zero)
|
||||||
|
{
|
||||||
|
SetMinionDebug(stage, "ownedPtr == 0");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Create handler
|
||||||
|
var handler = await _handlerFactory.Create(kind, () => ownedPtr, isWatched: false).ConfigureAwait(false);
|
||||||
|
if (handler is null || handler.Address == nint.Zero)
|
||||||
|
{
|
||||||
|
SetMinionDebug(stage, "handlerFactory returned null/zero", ownedPtr);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get object index for debug
|
||||||
|
ushort? objIndex = await _dalamudUtil.RunOnFrameworkThread(() => handler.GetGameObject()?.ObjectIndex)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
SetMinionDebug(stage, failure: null, handler.Address, objIndex);
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entity ID reader with safety checks.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="playerHandler">Handler of the Object</param>
|
||||||
|
/// <returns>Entity Id</returns>
|
||||||
|
private static uint ReadEntityIdSafe(GameObjectHandler playerHandler) => playerHandler.GetGameObject()?.EntityId ?? 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds an owned object by scanning the object table for the specified owner entity ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="kind">Object kind to find of owned object</param>
|
||||||
|
/// <param name="ownerEntityId">Owner Id</param>
|
||||||
|
/// <returns>Object Id</returns>
|
||||||
|
private nint FindOwnedByOwnerIdSafe(ObjectKind kind, uint ownerEntityId)
|
||||||
|
{
|
||||||
|
// Validate owner ID
|
||||||
|
if (ownerEntityId == 0 || ownerEntityId == uint.MaxValue)
|
||||||
|
return nint.Zero;
|
||||||
|
|
||||||
|
// Scan object table
|
||||||
|
foreach (var obj in _objectTable)
|
||||||
|
{
|
||||||
|
// Validate object
|
||||||
|
if (obj is null || obj.Address == nint.Zero)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check owner ID match
|
||||||
|
if (obj.OwnerId != ownerEntityId)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check kind match
|
||||||
|
if (!IsOwnedKindMatch(obj, kind))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return obj.Address;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nint.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if the given object matches the specified owned kind.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">Game Object</param>
|
||||||
|
/// <param name="kind">Object Kind</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static bool IsOwnedKindMatch(IGameObject obj, ObjectKind kind) => kind switch
|
||||||
|
{
|
||||||
|
// Match minion or mount
|
||||||
|
ObjectKind.MinionOrMount =>
|
||||||
|
obj.ObjectKind is DalamudObjectKind.MountType
|
||||||
|
or DalamudObjectKind.Companion,
|
||||||
|
|
||||||
|
// Match pet
|
||||||
|
ObjectKind.Pet =>
|
||||||
|
obj.ObjectKind == DalamudObjectKind.BattleNpc
|
||||||
|
&& obj is IBattleNpc bnPet
|
||||||
|
&& bnPet.BattleNpcKind == BattleNpcSubKind.Pet,
|
||||||
|
|
||||||
|
// Match companion
|
||||||
|
ObjectKind.Companion =>
|
||||||
|
obj.ObjectKind == DalamudObjectKind.BattleNpc
|
||||||
|
&& obj is IBattleNpc bnBuddy
|
||||||
|
&& bnBuddy.BattleNpcKind == BattleNpcSubKind.Chocobo,
|
||||||
|
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies Customize Plus data to the specified object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">Object Address</param>
|
||||||
|
/// <param name="customizeData">Data of the Customize+ that has to be applied</param>
|
||||||
|
/// <param name="kind">Object Kind</param>
|
||||||
|
/// <param name="customizeIds">Customize+ Ids</param>
|
||||||
|
/// <returns>Task</returns>
|
||||||
|
private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind, Dictionary<ObjectKind, Guid?> customizeIds)
|
||||||
|
{
|
||||||
|
customizeIds[kind] = await _ipc.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reverts Customize Plus changes for the specified object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customizeId">Customize+ Id</param>
|
||||||
|
/// <param name="kind">Object Id</param>
|
||||||
|
/// <param name="customizeIds">List of Customize+ ids</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private async Task RevertCustomizeAsync(Guid? customizeId, ObjectKind kind, Dictionary<ObjectKind, Guid?> customizeIds)
|
||||||
|
{
|
||||||
|
if (!customizeId.HasValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _ipc.CustomizePlus.RevertByIdAsync(customizeId.Value).ConfigureAwait(false);
|
||||||
|
customizeIds.Remove(kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +1,69 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
|
using LightlessSync.API.Data.Enum;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Pairs;
|
namespace LightlessSync.PlayerData.Pairs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// orchestrates the lifecycle of a paired character
|
/// orchestrates the lifecycle of a paired character
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||||
{
|
{
|
||||||
new string Ident { get; }
|
new string Ident { get; }
|
||||||
bool Initialized { get; }
|
bool Initialized { get; }
|
||||||
bool IsVisible { get; }
|
bool IsVisible { get; }
|
||||||
bool ScheduledForDeletion { get; set; }
|
bool ScheduledForDeletion { get; set; }
|
||||||
CharacterData? LastReceivedCharacterData { get; }
|
CharacterData? LastReceivedCharacterData { get; }
|
||||||
long LastAppliedDataBytes { get; }
|
long LastAppliedDataBytes { get; }
|
||||||
new string? PlayerName { get; }
|
new string? PlayerName { get; }
|
||||||
string PlayerNameHash { get; }
|
string PlayerNameHash { get; }
|
||||||
uint PlayerCharacterId { get; }
|
uint PlayerCharacterId { get; }
|
||||||
DateTime? LastDataReceivedAt { get; }
|
|
||||||
DateTime? LastApplyAttemptAt { get; }
|
DateTime? LastDataReceivedAt { get; }
|
||||||
DateTime? LastSuccessfulApplyAt { get; }
|
DateTime? LastApplyAttemptAt { get; }
|
||||||
string? LastFailureReason { get; }
|
DateTime? LastSuccessfulApplyAt { get; }
|
||||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
|
||||||
bool IsApplying { get; }
|
string? LastFailureReason { get; }
|
||||||
bool IsDownloading { get; }
|
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||||
int PendingDownloadCount { get; }
|
|
||||||
int ForbiddenDownloadCount { get; }
|
bool IsApplying { get; }
|
||||||
bool PendingModReapply { get; }
|
bool IsDownloading { get; }
|
||||||
bool ModApplyDeferred { get; }
|
int PendingDownloadCount { get; }
|
||||||
int MissingCriticalMods { get; }
|
int ForbiddenDownloadCount { get; }
|
||||||
int MissingNonCriticalMods { get; }
|
|
||||||
int MissingForbiddenMods { get; }
|
bool PendingModReapply { get; }
|
||||||
DateTime? InvisibleSinceUtc { get; }
|
bool ModApplyDeferred { get; }
|
||||||
DateTime? VisibilityEvictionDueAtUtc { get; }
|
int MissingCriticalMods { get; }
|
||||||
|
int MissingNonCriticalMods { get; }
|
||||||
|
int MissingForbiddenMods { get; }
|
||||||
|
|
||||||
|
DateTime? InvisibleSinceUtc { get; }
|
||||||
|
DateTime? VisibilityEvictionDueAtUtc { get; }
|
||||||
|
|
||||||
|
string? MinionAddressHex { get; }
|
||||||
|
|
||||||
|
ushort? MinionObjectIndex { get; }
|
||||||
|
|
||||||
|
DateTime? MinionResolvedAtUtc { get; }
|
||||||
|
string? MinionResolveStage { get; }
|
||||||
|
string? MinionResolveFailureReason { get; }
|
||||||
|
|
||||||
|
bool MinionPendingRetry { get; }
|
||||||
|
IReadOnlyList<string> MinionPendingRetryChanges { get; }
|
||||||
|
bool MinionHasAppearanceData { get; }
|
||||||
|
|
||||||
|
Guid OwnedPenumbraCollectionId { get; }
|
||||||
|
bool NeedsCollectionRebuildDebug { get; }
|
||||||
|
|
||||||
|
uint MinionOrMountCharacterId { get; }
|
||||||
|
uint PetCharacterId { get; }
|
||||||
|
uint CompanionCharacterId { get; }
|
||||||
|
|
||||||
void Initialize();
|
void Initialize();
|
||||||
void ApplyData(CharacterData data);
|
void ApplyData(CharacterData data);
|
||||||
void ApplyLastReceivedData(bool forced = false);
|
void ApplyLastReceivedData(bool forced = false);
|
||||||
bool FetchPerformanceMetricsFromCache();
|
void HardReapplyLastData();
|
||||||
void LoadCachedCharacterData(CharacterData data);
|
bool FetchPerformanceMetricsFromCache();
|
||||||
void SetUploading(bool uploading);
|
void LoadCachedCharacterData(CharacterData data);
|
||||||
void SetPaused(bool paused);
|
void SetUploading(bool uploading);
|
||||||
}
|
void SetPaused(bool paused);
|
||||||
|
}
|
||||||
|
|||||||
@@ -82,60 +82,114 @@ public class Pair
|
|||||||
|
|
||||||
public void AddContextMenu(IMenuOpenedArgs args)
|
public void AddContextMenu(IMenuOpenedArgs args)
|
||||||
{
|
{
|
||||||
|
|
||||||
var handler = TryGetHandler();
|
var handler = TryGetHandler();
|
||||||
if (handler is null)
|
if (handler is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.Target is not MenuTargetDefault target)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var obj = target.TargetObject;
|
||||||
|
if (obj is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var eid = obj.EntityId;
|
||||||
|
|
||||||
|
var isPlayerTarget = eid != 0 && eid != uint.MaxValue && eid == handler.PlayerCharacterId;
|
||||||
|
|
||||||
|
if (!(isPlayerTarget))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (isPlayerTarget)
|
||||||
{
|
{
|
||||||
|
if (!IsPaused)
|
||||||
|
{
|
||||||
|
UiSharedService.AddContextMenuItem(
|
||||||
|
args,
|
||||||
|
name: "Open Profile",
|
||||||
|
prefixChar: 'L',
|
||||||
|
colorMenuItem: _lightlessPrefixColor,
|
||||||
|
onClick: () =>
|
||||||
|
{
|
||||||
|
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
UiSharedService.AddContextMenuItem(
|
||||||
|
args,
|
||||||
|
name: "(Soft) - Reapply last data",
|
||||||
|
prefixChar: 'L',
|
||||||
|
colorMenuItem: _lightlessPrefixColor,
|
||||||
|
onClick: () =>
|
||||||
|
{
|
||||||
|
ApplyLastReceivedData(forced: true);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
UiSharedService.AddContextMenuItem(
|
||||||
|
args,
|
||||||
|
name: "(Hard) - Reapply last data",
|
||||||
|
prefixChar: 'L',
|
||||||
|
colorMenuItem: _lightlessPrefixColor,
|
||||||
|
onClick: () =>
|
||||||
|
{
|
||||||
|
HardApplyLastReceivedData();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
UiSharedService.AddContextMenuItem(
|
||||||
|
args,
|
||||||
|
name: "Change Permissions",
|
||||||
|
prefixChar: 'L',
|
||||||
|
colorMenuItem: _lightlessPrefixColor,
|
||||||
|
onClick: () =>
|
||||||
|
{
|
||||||
|
_mediator.Publish(new OpenPermissionWindow(this));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (IsPaused)
|
||||||
|
{
|
||||||
|
UiSharedService.AddContextMenuItem(
|
||||||
|
args,
|
||||||
|
name: "Toggle Unpause State",
|
||||||
|
prefixChar: 'L',
|
||||||
|
colorMenuItem: _lightlessPrefixColor,
|
||||||
|
onClick: () =>
|
||||||
|
{
|
||||||
|
_ = _apiController.Value.UnpauseAsync(UserData);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UiSharedService.AddContextMenuItem(
|
||||||
|
args,
|
||||||
|
name: "Toggle Pause State",
|
||||||
|
prefixChar: 'L',
|
||||||
|
colorMenuItem: _lightlessPrefixColor,
|
||||||
|
onClick: () =>
|
||||||
|
{
|
||||||
|
_ = _apiController.Value.PauseAsync(UserData);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
UiSharedService.AddContextMenuItem(
|
||||||
|
args,
|
||||||
|
name: "Cycle Pause State",
|
||||||
|
prefixChar: 'L',
|
||||||
|
colorMenuItem: _lightlessPrefixColor,
|
||||||
|
onClick: () =>
|
||||||
|
{
|
||||||
|
TriggerCyclePause();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsPaused)
|
|
||||||
{
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
|
||||||
{
|
|
||||||
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
|
||||||
{
|
|
||||||
ApplyLastReceivedData(forced: true);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
|
||||||
{
|
|
||||||
_mediator.Publish(new OpenPermissionWindow(this));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (IsPaused)
|
|
||||||
{
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Toggle Unpause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
|
||||||
{
|
|
||||||
_ = _apiController.Value.UnpauseAsync(UserData);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Toggle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
|
||||||
{
|
|
||||||
_ = _apiController.Value.PauseAsync(UserData);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Cycle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
|
||||||
{
|
|
||||||
TriggerCyclePause();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ApplyData(OnlineUserCharaDataDto data)
|
public void ApplyData(OnlineUserCharaDataDto data)
|
||||||
@@ -160,6 +214,18 @@ public class Pair
|
|||||||
handler.ApplyLastReceivedData(forced);
|
handler.ApplyLastReceivedData(forced);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void HardApplyLastReceivedData()
|
||||||
|
{
|
||||||
|
var handler = TryGetHandler();
|
||||||
|
if (handler is null)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("ApplyLastReceivedData skipped for {Uid}: handler missing.", UserData.UID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.HardReapplyLastData();
|
||||||
|
}
|
||||||
|
|
||||||
public void CreateCachedPlayer(OnlineUserIdentDto? dto = null)
|
public void CreateCachedPlayer(OnlineUserIdentDto? dto = null)
|
||||||
{
|
{
|
||||||
var handler = TryGetHandler();
|
var handler = TryGetHandler();
|
||||||
@@ -244,6 +310,17 @@ public class Pair
|
|||||||
handler.ModApplyDeferred,
|
handler.ModApplyDeferred,
|
||||||
handler.MissingCriticalMods,
|
handler.MissingCriticalMods,
|
||||||
handler.MissingNonCriticalMods,
|
handler.MissingNonCriticalMods,
|
||||||
handler.MissingForbiddenMods);
|
handler.MissingForbiddenMods,
|
||||||
|
|
||||||
|
handler.MinionAddressHex,
|
||||||
|
handler.MinionObjectIndex,
|
||||||
|
handler.MinionResolvedAtUtc,
|
||||||
|
handler.MinionResolveStage,
|
||||||
|
handler.MinionResolveFailureReason,
|
||||||
|
handler.MinionPendingRetry,
|
||||||
|
handler.MinionPendingRetryChanges,
|
||||||
|
handler.MinionHasAppearanceData,
|
||||||
|
handler.OwnedPenumbraCollectionId,
|
||||||
|
handler.NeedsCollectionRebuildDebug);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,28 +21,50 @@ public sealed record PairDebugInfo(
|
|||||||
bool ModApplyDeferred,
|
bool ModApplyDeferred,
|
||||||
int MissingCriticalMods,
|
int MissingCriticalMods,
|
||||||
int MissingNonCriticalMods,
|
int MissingNonCriticalMods,
|
||||||
int MissingForbiddenMods)
|
int MissingForbiddenMods,
|
||||||
|
|
||||||
|
string? MinionAddressHex,
|
||||||
|
ushort? MinionObjectIndex,
|
||||||
|
DateTime? MinionResolvedAtUtc,
|
||||||
|
string? MinionResolveStage,
|
||||||
|
string? MinionResolveFailureReason,
|
||||||
|
bool MinionPendingRetry,
|
||||||
|
IReadOnlyList<string> MinionPendingRetryChanges,
|
||||||
|
bool MinionHasAppearanceData,
|
||||||
|
Guid OwnedPenumbraCollectionId,
|
||||||
|
bool NeedsCollectionRebuild)
|
||||||
{
|
{
|
||||||
public static PairDebugInfo Empty { get; } = new(
|
public static PairDebugInfo Empty { get; } = new(
|
||||||
false,
|
HasHandler: false,
|
||||||
false,
|
HandlerInitialized: false,
|
||||||
false,
|
HandlerVisible: false,
|
||||||
false,
|
HandlerScheduledForDeletion: false,
|
||||||
null,
|
LastDataReceivedAt: null,
|
||||||
null,
|
LastApplyAttemptAt: null,
|
||||||
null,
|
LastSuccessfulApplyAt: null,
|
||||||
null,
|
InvisibleSinceUtc: null,
|
||||||
null,
|
VisibilityEvictionDueAtUtc: null,
|
||||||
null,
|
VisibilityEvictionRemainingSeconds: null,
|
||||||
null,
|
LastFailureReason: null,
|
||||||
Array.Empty<string>(),
|
BlockingConditions: [],
|
||||||
false,
|
IsApplying: false,
|
||||||
false,
|
IsDownloading: false,
|
||||||
0,
|
PendingDownloadCount: 0,
|
||||||
0,
|
ForbiddenDownloadCount: 0,
|
||||||
false,
|
PendingModReapply: false,
|
||||||
false,
|
ModApplyDeferred: false,
|
||||||
0,
|
MissingCriticalMods: 0,
|
||||||
0,
|
MissingNonCriticalMods: 0,
|
||||||
0);
|
MissingForbiddenMods: 0,
|
||||||
|
|
||||||
|
MinionAddressHex: null,
|
||||||
|
MinionObjectIndex: null,
|
||||||
|
MinionResolvedAtUtc: null,
|
||||||
|
MinionResolveStage: null,
|
||||||
|
MinionResolveFailureReason: null,
|
||||||
|
MinionPendingRetry: false,
|
||||||
|
MinionPendingRetryChanges: [],
|
||||||
|
MinionHasAppearanceData: false,
|
||||||
|
OwnedPenumbraCollectionId: Guid.Empty,
|
||||||
|
NeedsCollectionRebuild: false);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly XivDataAnalyzer _modelAnalyzer;
|
private readonly XivDataAnalyzer _modelAnalyzer;
|
||||||
private readonly IFramework _framework;
|
private readonly IFramework _framework;
|
||||||
|
private readonly IObjectTable _objectTable;
|
||||||
|
|
||||||
public PairHandlerAdapterFactory(
|
public PairHandlerAdapterFactory(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
@@ -63,7 +64,8 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
PairPerformanceMetricsCache pairPerformanceMetricsCache,
|
PairPerformanceMetricsCache pairPerformanceMetricsCache,
|
||||||
PenumbraTempCollectionJanitor tempCollectionJanitor,
|
PenumbraTempCollectionJanitor tempCollectionJanitor,
|
||||||
XivDataAnalyzer modelAnalyzer,
|
XivDataAnalyzer modelAnalyzer,
|
||||||
LightlessConfigService configService)
|
LightlessConfigService configService,
|
||||||
|
IObjectTable objectTable)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
@@ -87,6 +89,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
_tempCollectionJanitor = tempCollectionJanitor;
|
_tempCollectionJanitor = tempCollectionJanitor;
|
||||||
_modelAnalyzer = modelAnalyzer;
|
_modelAnalyzer = modelAnalyzer;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
|
_objectTable = objectTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPairHandlerAdapter Create(string ident)
|
public IPairHandlerAdapter Create(string ident)
|
||||||
@@ -105,6 +108,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
_pluginWarningNotificationManager,
|
_pluginWarningNotificationManager,
|
||||||
dalamudUtilService,
|
dalamudUtilService,
|
||||||
_framework,
|
_framework,
|
||||||
|
_objectTable,
|
||||||
actorObjectService,
|
actorObjectService,
|
||||||
_lifetime,
|
_lifetime,
|
||||||
_fileCacheManager,
|
_fileCacheManager,
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton(gameGui);
|
services.AddSingleton(gameGui);
|
||||||
services.AddSingleton(gameInteropProvider);
|
services.AddSingleton(gameInteropProvider);
|
||||||
services.AddSingleton(addonLifecycle);
|
services.AddSingleton(addonLifecycle);
|
||||||
|
services.AddSingleton(objectTable);
|
||||||
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
|
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
|
||||||
|
|
||||||
// Core singletons
|
// Core singletons
|
||||||
@@ -428,6 +429,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
return cfg;
|
return cfg;
|
||||||
});
|
});
|
||||||
services.AddSingleton(sp => new ServerConfigService(configDir));
|
services.AddSingleton(sp => new ServerConfigService(configDir));
|
||||||
|
services.AddSingleton(sp => new PenumbraJanitorConfigService(configDir));
|
||||||
services.AddSingleton(sp => new NotesConfigService(configDir));
|
services.AddSingleton(sp => new NotesConfigService(configDir));
|
||||||
services.AddSingleton(sp => new PairTagConfigService(configDir));
|
services.AddSingleton(sp => new PairTagConfigService(configDir));
|
||||||
services.AddSingleton(sp => new SyncshellTagConfigService(configDir));
|
services.AddSingleton(sp => new SyncshellTagConfigService(configDir));
|
||||||
@@ -441,6 +443,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ChatConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ChatConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ServerConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ServerConfigService>());
|
||||||
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PenumbraJanitorConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<NotesConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<NotesConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<SyncshellTagConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<SyncshellTagConfigService>());
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSu
|
|||||||
using IPlayerCharacter = Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter;
|
using IPlayerCharacter = Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter;
|
||||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||||
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace LightlessSync.Services.ActorTracking;
|
namespace LightlessSync.Services.ActorTracking;
|
||||||
|
|
||||||
@@ -57,6 +58,8 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
private bool _hooksActive;
|
private bool _hooksActive;
|
||||||
private static readonly TimeSpan SnapshotRefreshInterval = TimeSpan.FromSeconds(1);
|
private static readonly TimeSpan SnapshotRefreshInterval = TimeSpan.FromSeconds(1);
|
||||||
private DateTime _nextRefreshAllowed = DateTime.MinValue;
|
private DateTime _nextRefreshAllowed = DateTime.MinValue;
|
||||||
|
private int _warmStartQueued;
|
||||||
|
private int _warmStartRan;
|
||||||
|
|
||||||
public ActorObjectService(
|
public ActorObjectService(
|
||||||
ILogger<ActorObjectService> logger,
|
ILogger<ActorObjectService> logger,
|
||||||
@@ -74,7 +77,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
_clientState = clientState;
|
_clientState = clientState;
|
||||||
_condition = condition;
|
_condition = condition;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
|
|
||||||
_mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
_mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
if (!msg.OwnedObject) return;
|
if (!msg.OwnedObject) return;
|
||||||
@@ -96,7 +98,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
||||||
|
|
||||||
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
||||||
private GposeSnapshot CurrentGposeSnapshot => Volatile.Read(ref _gposeSnapshot);
|
private GposeSnapshot CurrentGposeSnapshot => Volatile.Read(ref _gposeSnapshot);
|
||||||
|
|
||||||
@@ -341,6 +342,8 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
_warmStartRan = 0;
|
||||||
|
|
||||||
DisposeHooks();
|
DisposeHooks();
|
||||||
_activePlayers.Clear();
|
_activePlayers.Clear();
|
||||||
_gposePlayers.Clear();
|
_gposePlayers.Clear();
|
||||||
@@ -505,7 +508,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
return new ActorDescriptor(name, hashedCid, address, objectIndex, isLocal, isInGpose, objectKind, ownedKind, ownerEntityId);
|
return new ActorDescriptor(name, hashedCid, address, objectIndex, isLocal, isInGpose, objectKind, ownedKind, ownerEntityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(GameObject* gameObject, DalamudObjectKind objectKind, bool isLocalPlayer)
|
private unsafe (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(
|
||||||
|
GameObject* gameObject,
|
||||||
|
DalamudObjectKind objectKind,
|
||||||
|
bool isLocalPlayer)
|
||||||
{
|
{
|
||||||
if (gameObject == null)
|
if (gameObject == null)
|
||||||
return (null, 0);
|
return (null, 0);
|
||||||
@@ -517,6 +523,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ownerId = ResolveOwnerId(gameObject);
|
var ownerId = ResolveOwnerId(gameObject);
|
||||||
|
|
||||||
var localPlayerAddress = _objectTable.LocalPlayer?.Address ?? nint.Zero;
|
var localPlayerAddress = _objectTable.LocalPlayer?.Address ?? nint.Zero;
|
||||||
if (localPlayerAddress == nint.Zero)
|
if (localPlayerAddress == nint.Zero)
|
||||||
return (null, ownerId);
|
return (null, ownerId);
|
||||||
@@ -528,9 +535,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
||||||
{
|
{
|
||||||
var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId);
|
var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId);
|
||||||
if (expectedMinionOrMount != nint.Zero
|
if (expectedMinionOrMount != nint.Zero && (nint)gameObject == expectedMinionOrMount)
|
||||||
&& (nint)gameObject == expectedMinionOrMount
|
|
||||||
&& IsPlayerRelatedOwnedAddress(expectedMinionOrMount, LightlessObjectKind.MinionOrMount))
|
|
||||||
{
|
{
|
||||||
var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
|
var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
|
||||||
return (LightlessObjectKind.MinionOrMount, resolvedOwner);
|
return (LightlessObjectKind.MinionOrMount, resolvedOwner);
|
||||||
@@ -540,20 +545,16 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
if (objectKind != DalamudObjectKind.BattleNpc)
|
if (objectKind != DalamudObjectKind.BattleNpc)
|
||||||
return (null, ownerId);
|
return (null, ownerId);
|
||||||
|
|
||||||
if (ownerId != localEntityId)
|
if (ownerId != 0 && ownerId != localEntityId)
|
||||||
return (null, ownerId);
|
return (null, ownerId);
|
||||||
|
|
||||||
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
|
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
|
||||||
if (expectedPet != nint.Zero
|
if (expectedPet != nint.Zero && (nint)gameObject == expectedPet)
|
||||||
&& (nint)gameObject == expectedPet
|
return (LightlessObjectKind.Pet, ownerId != 0 ? ownerId : localEntityId);
|
||||||
&& IsPlayerRelatedOwnedAddress(expectedPet, LightlessObjectKind.Pet))
|
|
||||||
return (LightlessObjectKind.Pet, ownerId);
|
|
||||||
|
|
||||||
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
|
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
|
||||||
if (expectedCompanion != nint.Zero
|
if (expectedCompanion != nint.Zero && (nint)gameObject == expectedCompanion)
|
||||||
&& (nint)gameObject == expectedCompanion
|
return (LightlessObjectKind.Companion, ownerId != 0 ? ownerId : localEntityId);
|
||||||
&& IsPlayerRelatedOwnedAddress(expectedCompanion, LightlessObjectKind.Companion))
|
|
||||||
return (LightlessObjectKind.Companion, ownerId);
|
|
||||||
|
|
||||||
return (null, ownerId);
|
return (null, ownerId);
|
||||||
}
|
}
|
||||||
@@ -581,21 +582,25 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
return nint.Zero;
|
return nint.Zero;
|
||||||
|
|
||||||
var playerObject = (GameObject*)localPlayerAddress;
|
var playerObject = (GameObject*)localPlayerAddress;
|
||||||
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
|
|
||||||
if (ownerEntityId == 0)
|
|
||||||
return nint.Zero;
|
|
||||||
|
|
||||||
|
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
|
||||||
if (candidateAddress != nint.Zero)
|
if (candidateAddress != nint.Zero)
|
||||||
{
|
{
|
||||||
var candidate = (GameObject*)candidateAddress;
|
var candidate = (GameObject*)candidateAddress;
|
||||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
||||||
|
|
||||||
if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
||||||
{
|
{
|
||||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
var resolvedOwner = ResolveOwnerId(candidate);
|
||||||
|
|
||||||
|
if (resolvedOwner == ownerEntityId || resolvedOwner == 0)
|
||||||
return candidateAddress;
|
return candidateAddress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ownerEntityId == 0)
|
||||||
|
return nint.Zero;
|
||||||
|
|
||||||
foreach (var obj in _objectTable)
|
foreach (var obj in _objectTable)
|
||||||
{
|
{
|
||||||
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
||||||
@@ -612,6 +617,90 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
return nint.Zero;
|
return nint.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public unsafe bool TryFindOwnedObject(uint ownerEntityId, LightlessObjectKind kind, out nint address)
|
||||||
|
{
|
||||||
|
address = nint.Zero;
|
||||||
|
if (ownerEntityId == 0) return false;
|
||||||
|
|
||||||
|
foreach (var addr in EnumerateActiveCharacterAddresses())
|
||||||
|
{
|
||||||
|
if (addr == nint.Zero) continue;
|
||||||
|
|
||||||
|
var go = (GameObject*)addr;
|
||||||
|
var ok = (DalamudObjectKind)go->ObjectKind;
|
||||||
|
|
||||||
|
switch (kind)
|
||||||
|
{
|
||||||
|
case LightlessObjectKind.MinionOrMount:
|
||||||
|
if (ok is DalamudObjectKind.MountType or DalamudObjectKind.Companion
|
||||||
|
&& ResolveOwnerId(go) == ownerEntityId)
|
||||||
|
{
|
||||||
|
address = addr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LightlessObjectKind.Pet:
|
||||||
|
if (ok == DalamudObjectKind.BattleNpc
|
||||||
|
&& go->BattleNpcSubKind == BattleNpcSubKind.Pet
|
||||||
|
&& ResolveOwnerId(go) == ownerEntityId)
|
||||||
|
{
|
||||||
|
address = addr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LightlessObjectKind.Companion:
|
||||||
|
if (ok == DalamudObjectKind.BattleNpc
|
||||||
|
&& go->BattleNpcSubKind == BattleNpcSubKind.Buddy
|
||||||
|
&& ResolveOwnerId(go) == ownerEntityId)
|
||||||
|
{
|
||||||
|
address = addr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe IReadOnlyList<nint> GetMinionOrMountCandidates(uint ownerEntityId, ushort preferredPlayerIndex)
|
||||||
|
{
|
||||||
|
var results = new List<(nint Ptr, int Score)>(4);
|
||||||
|
|
||||||
|
var manager = GameObjectManager.Instance();
|
||||||
|
if (manager == null || ownerEntityId == 0)
|
||||||
|
return Array.Empty<nint>();
|
||||||
|
|
||||||
|
const int objectLimit = 200;
|
||||||
|
for (var i = 0; i < objectLimit; i++)
|
||||||
|
{
|
||||||
|
var obj = manager->Objects.IndexSorted[i].Value;
|
||||||
|
if (obj == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var kind = (DalamudObjectKind)obj->ObjectKind;
|
||||||
|
if (kind is not (DalamudObjectKind.MountType or DalamudObjectKind.Companion))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var owner = ResolveOwnerId(obj);
|
||||||
|
if (owner != ownerEntityId)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var idx = obj->ObjectIndex;
|
||||||
|
var score = Math.Abs(idx - (preferredPlayerIndex + 1));
|
||||||
|
if (obj->DrawObject == null) score += 50;
|
||||||
|
|
||||||
|
results.Add(((nint)obj, score));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
.OrderBy(r => r.Score)
|
||||||
|
.Select(r => r.Ptr)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId)
|
private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId)
|
||||||
{
|
{
|
||||||
if (localPlayerAddress == nint.Zero || ownerEntityId == 0)
|
if (localPlayerAddress == nint.Zero || ownerEntityId == 0)
|
||||||
@@ -1216,21 +1305,19 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
|
|
||||||
private static unsafe bool IsObjectFullyLoaded(nint address)
|
private static unsafe bool IsObjectFullyLoaded(nint address)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero)
|
if (address == nint.Zero) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
var gameObject = (GameObject*)address;
|
var gameObject = (GameObject*)address;
|
||||||
if (gameObject == null)
|
if (gameObject == null) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
var drawObject = gameObject->DrawObject;
|
var drawObject = gameObject->DrawObject;
|
||||||
if (drawObject == null)
|
if (drawObject == null) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
if ((ulong)gameObject->RenderFlags == 2048)
|
if ((ulong)gameObject->RenderFlags == 2048)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var characterBase = (CharacterBase*)drawObject;
|
var characterBase = (CharacterBase*)drawObject;
|
||||||
|
|
||||||
if (characterBase->HasModelInSlotLoaded != 0)
|
if (characterBase->HasModelInSlotLoaded != 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -1240,6 +1327,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Auto)]
|
||||||
private readonly record struct LoadState(bool IsValid, bool IsLoaded)
|
private readonly record struct LoadState(bool IsValid, bool IsLoaded)
|
||||||
{
|
{
|
||||||
public static LoadState Invalid => new(false, false);
|
public static LoadState Invalid => new(false, false);
|
||||||
|
|||||||
@@ -816,9 +816,12 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Starting DalamudUtilService");
|
_logger.LogInformation("Starting DalamudUtilService");
|
||||||
_framework.Update += FrameworkOnUpdate;
|
_framework.Update += FrameworkOnUpdate;
|
||||||
if (IsLoggedIn)
|
_clientState.Login += OnClientLogin;
|
||||||
|
_clientState.Logout += OnClientLogout;
|
||||||
|
|
||||||
|
if (_clientState.IsLoggedIn)
|
||||||
{
|
{
|
||||||
_classJobId = _objectTable.LocalPlayer!.ClassJob.RowId;
|
OnClientLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Started DalamudUtilService");
|
_logger.LogInformation("Started DalamudUtilService");
|
||||||
@@ -831,6 +834,9 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
Mediator.UnsubscribeAll(this);
|
Mediator.UnsubscribeAll(this);
|
||||||
_framework.Update -= FrameworkOnUpdate;
|
_framework.Update -= FrameworkOnUpdate;
|
||||||
|
_clientState.Login -= OnClientLogin;
|
||||||
|
_clientState.Logout -= OnClientLogout;
|
||||||
|
|
||||||
if (_FocusPairIdent.HasValue)
|
if (_FocusPairIdent.HasValue)
|
||||||
{
|
{
|
||||||
if (_framework.IsInFrameworkUpdateThread)
|
if (_framework.IsInFrameworkUpdateThread)
|
||||||
@@ -845,6 +851,41 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnClientLogin()
|
||||||
|
{
|
||||||
|
if (IsLoggedIn)
|
||||||
|
return;
|
||||||
|
_ = RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
if (IsLoggedIn)
|
||||||
|
return;
|
||||||
|
var localPlayer = _objectTable.LocalPlayer;
|
||||||
|
IsLoggedIn = true;
|
||||||
|
_lastZone = _clientState.TerritoryType;
|
||||||
|
if (localPlayer != null)
|
||||||
|
{
|
||||||
|
_lastWorldId = (ushort)localPlayer.CurrentWorld.RowId;
|
||||||
|
_classJobId = localPlayer.ClassJob.RowId;
|
||||||
|
}
|
||||||
|
_cid = RebuildCID();
|
||||||
|
Mediator.Publish(new DalamudLoginMessage());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClientLogout(int type, int code)
|
||||||
|
{
|
||||||
|
if (!IsLoggedIn)
|
||||||
|
return;
|
||||||
|
_ = RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
if (!IsLoggedIn)
|
||||||
|
return;
|
||||||
|
IsLoggedIn = false;
|
||||||
|
_lastWorldId = 0;
|
||||||
|
Mediator.Publish(new DalamudLogoutMessage());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async Task WaitWhileCharacterIsDrawing(
|
public async Task WaitWhileCharacterIsDrawing(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
GameObjectHandler handler,
|
GameObjectHandler handler,
|
||||||
@@ -1272,23 +1313,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
if (isNormalFrameworkUpdate)
|
if (isNormalFrameworkUpdate)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (localPlayer != null && !IsLoggedIn)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Logged in");
|
|
||||||
IsLoggedIn = true;
|
|
||||||
_lastZone = _clientState.TerritoryType;
|
|
||||||
_lastWorldId = (ushort)localPlayer.CurrentWorld.RowId;
|
|
||||||
_cid = RebuildCID();
|
|
||||||
Mediator.Publish(new DalamudLoginMessage());
|
|
||||||
}
|
|
||||||
else if (localPlayer == null && IsLoggedIn)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Logged out");
|
|
||||||
IsLoggedIn = false;
|
|
||||||
_lastWorldId = 0;
|
|
||||||
Mediator.Publish(new DalamudLogoutMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_gameConfig != null
|
if (_gameConfig != null
|
||||||
&& _gameConfig.TryGet(Dalamud.Game.Config.SystemConfigOption.LodType_DX11, out bool lodEnabled))
|
&& _gameConfig.TryGet(Dalamud.Game.Config.SystemConfigOption.LodType_DX11, out bool lodEnabled))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
ImGuiWindowFlags.NoMove |
|
ImGuiWindowFlags.NoMove |
|
||||||
ImGuiWindowFlags.NoSavedSettings |
|
ImGuiWindowFlags.NoSavedSettings |
|
||||||
ImGuiWindowFlags.NoNav |
|
ImGuiWindowFlags.NoNav |
|
||||||
|
ImGuiWindowFlags.NoFocusOnAppearing |
|
||||||
ImGuiWindowFlags.NoInputs;
|
ImGuiWindowFlags.NoInputs;
|
||||||
|
|
||||||
private readonly List<RectF> _uiRects = new(128);
|
private readonly List<RectF> _uiRects = new(128);
|
||||||
|
|||||||
@@ -138,5 +138,6 @@ 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 LocationSharingMessage(UserData User, LocationInfo LocationInfo, DateTimeOffset ExpireAt) : MessageBase;
|
||||||
public record MapChangedMessage(uint MapId) : MessageBase;
|
public record MapChangedMessage(uint MapId) : MessageBase;
|
||||||
|
public record PenumbraTempCollectionsCleanedMessage : 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
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ namespace LightlessSync.Services;
|
|||||||
public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriberBase
|
public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private readonly IpcManager _ipc;
|
private readonly IpcManager _ipc;
|
||||||
private readonly LightlessConfigService _config;
|
private readonly PenumbraJanitorConfigService _config;
|
||||||
private int _ran;
|
private int _ran;
|
||||||
|
|
||||||
public PenumbraTempCollectionJanitor(
|
public PenumbraTempCollectionJanitor(
|
||||||
ILogger<PenumbraTempCollectionJanitor> logger,
|
ILogger<PenumbraTempCollectionJanitor> logger,
|
||||||
LightlessMediator mediator,
|
LightlessMediator mediator,
|
||||||
IpcManager ipc,
|
IpcManager ipc,
|
||||||
LightlessConfigService config) : base(logger, mediator)
|
PenumbraJanitorConfigService config) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_ipc = ipc;
|
_ipc = ipc;
|
||||||
_config = config;
|
_config = config;
|
||||||
@@ -67,5 +67,8 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
|||||||
|
|
||||||
_config.Current.OrphanableTempCollections.Clear();
|
_config.Current.OrphanableTempCollections.Clear();
|
||||||
_config.Save();
|
_config.Save();
|
||||||
|
|
||||||
|
// Notify cleanup complete
|
||||||
|
Mediator.Publish(new PenumbraTempCollectionsCleanedMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
|
|
||||||
public async Task StopAsync(CancellationToken cancellationToken)
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_cancellationTokenSource.Cancel();
|
await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
if (_dalamudUtilService.IsOnFrameworkThread)
|
if (_dalamudUtilService.IsOnFrameworkThread)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
|
|||||||
ImGuiWindowFlags.NoCollapse |
|
ImGuiWindowFlags.NoCollapse |
|
||||||
ImGuiWindowFlags.NoTitleBar |
|
ImGuiWindowFlags.NoTitleBar |
|
||||||
ImGuiWindowFlags.NoScrollbar |
|
ImGuiWindowFlags.NoScrollbar |
|
||||||
|
ImGuiWindowFlags.NoFocusOnAppearing |
|
||||||
ImGuiWindowFlags.AlwaysAutoResize;
|
ImGuiWindowFlags.AlwaysAutoResize;
|
||||||
|
|
||||||
PositionCondition = ImGuiCond.Always;
|
PositionCondition = ImGuiCond.Always;
|
||||||
|
|||||||
@@ -1688,6 +1688,46 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.EndTable();
|
ImGui.EndTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.TextUnformatted("Owned / Minion-Mount");
|
||||||
|
|
||||||
|
if (ImGui.BeginTable("##pairDebugOwnedMinion", 2, ImGuiTableFlags.SizingStretchProp))
|
||||||
|
{
|
||||||
|
DrawPairPropertyRow("Owned Temp Collection", debugInfo.OwnedPenumbraCollectionId == Guid.Empty
|
||||||
|
? "n/a"
|
||||||
|
: debugInfo.OwnedPenumbraCollectionId.ToString());
|
||||||
|
|
||||||
|
DrawPairPropertyRow("Needs Collection Rebuild", FormatBool(debugInfo.NeedsCollectionRebuild));
|
||||||
|
|
||||||
|
DrawPairPropertyRow("Minion Ptr", string.IsNullOrEmpty(debugInfo.MinionAddressHex)
|
||||||
|
? "n/a"
|
||||||
|
: debugInfo.MinionAddressHex);
|
||||||
|
|
||||||
|
DrawPairPropertyRow("Minion ObjectIndex", debugInfo.MinionObjectIndex.HasValue
|
||||||
|
? debugInfo.MinionObjectIndex.Value.ToString(CultureInfo.InvariantCulture)
|
||||||
|
: "n/a");
|
||||||
|
|
||||||
|
DrawPairPropertyRow("Minion Resolved At", FormatTimestamp(debugInfo.MinionResolvedAtUtc));
|
||||||
|
|
||||||
|
DrawPairPropertyRow("Minion Resolve Stage", string.IsNullOrEmpty(debugInfo.MinionResolveStage)
|
||||||
|
? "n/a"
|
||||||
|
: debugInfo.MinionResolveStage);
|
||||||
|
|
||||||
|
DrawPairPropertyRow("Minion Resolve Failure", string.IsNullOrEmpty(debugInfo.MinionResolveFailureReason)
|
||||||
|
? "n/a"
|
||||||
|
: debugInfo.MinionResolveFailureReason);
|
||||||
|
|
||||||
|
DrawPairPropertyRow("Minion Pending Retry", FormatBool(debugInfo.MinionPendingRetry));
|
||||||
|
|
||||||
|
var retryChanges = debugInfo.MinionPendingRetryChanges is { Count: > 0 }
|
||||||
|
? string.Join(", ", debugInfo.MinionPendingRetryChanges)
|
||||||
|
: "n/a";
|
||||||
|
DrawPairPropertyRow("Minion Pending Changes", retryChanges);
|
||||||
|
|
||||||
|
DrawPairPropertyRow("Minion Has Appearance Data", FormatBool(debugInfo.MinionHasAppearanceData));
|
||||||
|
|
||||||
|
ImGui.EndTable();
|
||||||
|
}
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.TextUnformatted("Syncshell Memberships");
|
ImGui.TextUnformatted("Syncshell Memberships");
|
||||||
if (snapshot.PairsWithGroups.TryGetValue(pair, out var groups) && groups.Count > 0)
|
if (snapshot.PairsWithGroups.TryGetValue(pair, out var groups) && groups.Count > 0)
|
||||||
|
|||||||
41
LightlessSync/Utils/MemoryProcessProbe.cs
Normal file
41
LightlessSync/Utils/MemoryProcessProbe.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace LightlessSync.Utils;
|
||||||
|
|
||||||
|
internal static class MemoryProcessProbe
|
||||||
|
{
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern nint GetCurrentProcess();
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern bool ReadProcessMemory(
|
||||||
|
nint hProcess,
|
||||||
|
nint lpBaseAddress,
|
||||||
|
byte[] lpBuffer,
|
||||||
|
int dwSize,
|
||||||
|
out nint lpNumberOfBytesRead);
|
||||||
|
|
||||||
|
private static readonly nint _proc = GetCurrentProcess();
|
||||||
|
|
||||||
|
public static bool TryReadIntPtr(nint address, out nint value)
|
||||||
|
{
|
||||||
|
value = nint.Zero;
|
||||||
|
|
||||||
|
if (address == nint.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if ((ulong)address < 0x10000UL)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var buf = new byte[IntPtr.Size];
|
||||||
|
if (!ReadProcessMemory(_proc, address, buf, buf.Length, out var read) || read != (nint)buf.Length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
value = IntPtr.Size == 8
|
||||||
|
? (nint)BitConverter.ToInt64(buf, 0)
|
||||||
|
: (nint)BitConverter.ToInt32(buf, 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,6 @@ using LightlessSync.API.Data;
|
|||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace LightlessSync.Utils;
|
namespace LightlessSync.Utils;
|
||||||
@@ -56,164 +54,168 @@ public static class VariousExtensions
|
|||||||
return new CancellationTokenSource();
|
return new CancellationTokenSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Dictionary<ObjectKind, HashSet<PlayerChanges>> CheckUpdatedData(this CharacterData newData, Guid applicationBase,
|
public static Dictionary<ObjectKind, HashSet<PlayerChanges>> CheckUpdatedData(
|
||||||
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods,
|
this CharacterData newData,
|
||||||
|
Guid applicationBase,
|
||||||
|
CharacterData? oldData,
|
||||||
|
ILogger logger,
|
||||||
|
IPairPerformanceSubject cachedPlayer,
|
||||||
|
bool forceApplyCustomization,
|
||||||
|
bool forceApplyMods,
|
||||||
bool suppressForcedRedrawOnForcedModApply = false)
|
bool suppressForcedRedrawOnForcedModApply = false)
|
||||||
{
|
{
|
||||||
oldData ??= new();
|
oldData ??= new();
|
||||||
|
|
||||||
|
static bool HasFiles(List<FileReplacementData>? list) => list is { Count: > 0 };
|
||||||
|
static bool HasText(string? s) => !string.IsNullOrEmpty(s);
|
||||||
|
static string Norm(string? s) => s ?? string.Empty;
|
||||||
|
|
||||||
|
var forceRedrawOnForcedApply = forceApplyMods && !suppressForcedRedrawOnForcedModApply;
|
||||||
|
|
||||||
var charaDataToUpdate = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
|
var charaDataToUpdate = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
|
||||||
|
|
||||||
foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>())
|
foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>())
|
||||||
{
|
{
|
||||||
charaDataToUpdate[objectKind] = [];
|
var set = new HashSet<PlayerChanges>();
|
||||||
oldData.FileReplacements.TryGetValue(objectKind, out var existingFileReplacements);
|
|
||||||
newData.FileReplacements.TryGetValue(objectKind, out var newFileReplacements);
|
|
||||||
oldData.GlamourerData.TryGetValue(objectKind, out var existingGlamourerData);
|
|
||||||
newData.GlamourerData.TryGetValue(objectKind, out var newGlamourerData);
|
|
||||||
|
|
||||||
bool hasNewButNotOldFileReplacements = newFileReplacements != null && existingFileReplacements == null;
|
oldData.FileReplacements.TryGetValue(objectKind, out var oldFileRepls);
|
||||||
bool hasOldButNotNewFileReplacements = existingFileReplacements != null && newFileReplacements == null;
|
newData.FileReplacements.TryGetValue(objectKind, out var newFileRepls);
|
||||||
|
|
||||||
bool hasNewButNotOldGlamourerData = newGlamourerData != null && existingGlamourerData == null;
|
oldData.GlamourerData.TryGetValue(objectKind, out var oldGlam);
|
||||||
bool hasOldButNotNewGlamourerData = existingGlamourerData != null && newGlamourerData == null;
|
newData.GlamourerData.TryGetValue(objectKind, out var newGlam);
|
||||||
|
|
||||||
bool hasNewAndOldFileReplacements = newFileReplacements != null && existingFileReplacements != null;
|
var oldHasFiles = HasFiles(oldFileRepls);
|
||||||
bool hasNewAndOldGlamourerData = newGlamourerData != null && existingGlamourerData != null;
|
var newHasFiles = HasFiles(newFileRepls);
|
||||||
var forceRedrawOnForcedApply = forceApplyMods && !suppressForcedRedrawOnForcedModApply;
|
|
||||||
|
|
||||||
if (hasNewButNotOldFileReplacements || hasOldButNotNewFileReplacements || hasNewButNotOldGlamourerData || hasOldButNotNewGlamourerData)
|
if (oldHasFiles != newHasFiles)
|
||||||
{
|
{
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Some new data arrived: NewButNotOldFiles:{hasNewButNotOldFileReplacements}," +
|
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (File presence changed old={old} new={new}) => {change}",
|
||||||
" OldButNotNewFiles:{hasOldButNotNewFileReplacements}, NewButNotOldGlam:{hasNewButNotOldGlamourerData}, OldButNotNewGlam:{hasOldButNotNewGlamourerData}) => {change}, {change2}",
|
applicationBase, cachedPlayer, objectKind, oldHasFiles, newHasFiles, PlayerChanges.ModFiles);
|
||||||
applicationBase,
|
|
||||||
cachedPlayer, objectKind, hasNewButNotOldFileReplacements, hasOldButNotNewFileReplacements, hasNewButNotOldGlamourerData, hasOldButNotNewGlamourerData, PlayerChanges.ModFiles, PlayerChanges.Glamourer);
|
set.Add(PlayerChanges.ModFiles);
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ModFiles);
|
if (objectKind != ObjectKind.Player || forceRedrawOnForcedApply)
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.Glamourer);
|
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (hasNewAndOldFileReplacements)
|
|
||||||
{
|
{
|
||||||
var oldList = oldData.FileReplacements[objectKind];
|
set.Add(PlayerChanges.ForcedRedraw);
|
||||||
var newList = newData.FileReplacements[objectKind];
|
|
||||||
var listsAreEqual = oldList.SequenceEqual(newList, PlayerData.Data.FileReplacementDataComparer.Instance);
|
|
||||||
if (!listsAreEqual || forceApplyMods)
|
|
||||||
{
|
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles);
|
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ModFiles);
|
|
||||||
if (objectKind != ObjectKind.Player || forceRedrawOnForcedApply)
|
|
||||||
{
|
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var existingFace = existingFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/face/", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
|
||||||
var existingHair = existingFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/hair/", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
|
||||||
var existingTail = existingFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
|
||||||
var newFace = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/face/", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
|
||||||
var newHair = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/hair/", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
|
||||||
var newTail = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
|
||||||
var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl")))
|
|
||||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
|
||||||
var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl")))
|
|
||||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
|
||||||
|
|
||||||
logger.LogTrace("[BASE-{appbase}] ExistingFace: {of}, NewFace: {fc}; ExistingHair: {eh}, NewHair: {nh}; ExistingTail: {et}, NewTail: {nt}; ExistingTransient: {etr}, NewTransient: {ntr}", applicationBase,
|
|
||||||
existingFace.Count, newFace.Count, existingHair.Count, newHair.Count, existingTail.Count, newTail.Count, existingTransients.Count, newTransients.Count);
|
|
||||||
|
|
||||||
var differentFace = !existingFace.SequenceEqual(newFace, PlayerData.Data.FileReplacementDataComparer.Instance);
|
|
||||||
var differentHair = !existingHair.SequenceEqual(newHair, PlayerData.Data.FileReplacementDataComparer.Instance);
|
|
||||||
var differentTail = !existingTail.SequenceEqual(newTail, PlayerData.Data.FileReplacementDataComparer.Instance);
|
|
||||||
var differenTransients = !existingTransients.SequenceEqual(newTransients, PlayerData.Data.FileReplacementDataComparer.Instance);
|
|
||||||
if (differentFace || differentHair || differentTail || differenTransients)
|
|
||||||
{
|
|
||||||
logger.LogDebug("[BASE-{appbase}] Different Subparts: Face: {face}, Hair: {hair}, Tail: {tail}, Transients: {transients} => {change}", applicationBase,
|
|
||||||
differentFace, differentHair, differentTail, differenTransients, PlayerChanges.ForcedRedraw);
|
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else if (newHasFiles)
|
||||||
|
{
|
||||||
|
var listsAreEqual = oldFileRepls!.SequenceEqual(newFileRepls!, PlayerData.Data.FileReplacementDataComparer.Instance);
|
||||||
|
|
||||||
if (hasNewAndOldGlamourerData)
|
if (!listsAreEqual || forceApplyMods)
|
||||||
{
|
{
|
||||||
bool glamourerDataDifferent = !string.Equals(oldData.GlamourerData[objectKind], newData.GlamourerData[objectKind], StringComparison.Ordinal);
|
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements changed or forceApplyMods) => {change}",
|
||||||
if (glamourerDataDifferent || forceApplyCustomization)
|
applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles);
|
||||||
|
|
||||||
|
set.Add(PlayerChanges.ModFiles);
|
||||||
|
|
||||||
|
if (objectKind != ObjectKind.Player || forceRedrawOnForcedApply)
|
||||||
{
|
{
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Glamourer different) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.Glamourer);
|
set.Add(PlayerChanges.ForcedRedraw);
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.Glamourer);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var existingFace = oldFileRepls.Where(g => g.GamePaths.Any(p => p.Contains("/face/", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
var existingHair = oldFileRepls.Where(g => g.GamePaths.Any(p => p.Contains("/hair/", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
var existingTail = oldFileRepls.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
||||||
|
var newFace = newFileRepls!.Where(g => g.GamePaths.Any(p => p.Contains("/face/", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
var newHair = newFileRepls.Where(g => g.GamePaths.Any(p => p.Contains("/hair/", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
var newTail = newFileRepls.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
||||||
|
var existingTransients = oldFileRepls.Where(g => g.GamePaths.Any(p => !p.EndsWith("mdl", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !p.EndsWith("tex", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !p.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
||||||
|
var newTransients = newFileRepls.Where(g => g.GamePaths.Any(p => !p.EndsWith("mdl", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !p.EndsWith("tex", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !p.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
||||||
|
var differentFace = !existingFace.SequenceEqual(newFace, PlayerData.Data.FileReplacementDataComparer.Instance);
|
||||||
|
var differentHair = !existingHair.SequenceEqual(newHair, PlayerData.Data.FileReplacementDataComparer.Instance);
|
||||||
|
var differentTail = !existingTail.SequenceEqual(newTail, PlayerData.Data.FileReplacementDataComparer.Instance);
|
||||||
|
var differentTransients = !existingTransients.SequenceEqual(newTransients, PlayerData.Data.FileReplacementDataComparer.Instance);
|
||||||
|
|
||||||
|
if (differentFace || differentHair || differentTail || differentTransients)
|
||||||
|
set.Add(PlayerChanges.ForcedRedraw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
oldData.CustomizePlusData.TryGetValue(objectKind, out var oldCustomizePlusData);
|
var oldGlamNorm = Norm(oldGlam);
|
||||||
newData.CustomizePlusData.TryGetValue(objectKind, out var newCustomizePlusData);
|
var newGlamNorm = Norm(newGlam);
|
||||||
|
|
||||||
oldCustomizePlusData ??= string.Empty;
|
if (!string.Equals(oldGlamNorm, newGlamNorm, StringComparison.Ordinal)
|
||||||
newCustomizePlusData ??= string.Empty;
|
|| (forceApplyCustomization && HasText(newGlamNorm)))
|
||||||
|
|
||||||
bool customizeDataDifferent = !string.Equals(oldCustomizePlusData, newCustomizePlusData, StringComparison.Ordinal);
|
|
||||||
if (customizeDataDifferent || (forceApplyCustomization && !string.IsNullOrEmpty(newCustomizePlusData)))
|
|
||||||
{
|
{
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff customize data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.Customize);
|
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Glamourer different) => {change}",
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.Customize);
|
applicationBase, cachedPlayer, objectKind, PlayerChanges.Glamourer);
|
||||||
|
set.Add(PlayerChanges.Glamourer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (objectKind != ObjectKind.Player) continue;
|
oldData.CustomizePlusData.TryGetValue(objectKind, out var oldC);
|
||||||
|
newData.CustomizePlusData.TryGetValue(objectKind, out var newC);
|
||||||
|
|
||||||
bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
var oldCNorm = Norm(oldC);
|
||||||
if (manipDataDifferent || forceRedrawOnForcedApply)
|
var newCNorm = Norm(newC);
|
||||||
|
|
||||||
|
if (!string.Equals(oldCNorm, newCNorm, StringComparison.Ordinal)
|
||||||
|
|| (forceApplyCustomization && HasText(newCNorm)))
|
||||||
{
|
{
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip);
|
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Customize+ different) => {change}",
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);
|
applicationBase, cachedPlayer, objectKind, PlayerChanges.Customize);
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw);
|
set.Add(PlayerChanges.Customize);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool heelsOffsetDifferent = !string.Equals(oldData.HeelsData, newData.HeelsData, StringComparison.Ordinal);
|
if (objectKind == ObjectKind.Player)
|
||||||
if (heelsOffsetDifferent || (forceApplyCustomization && !string.IsNullOrEmpty(newData.HeelsData)))
|
|
||||||
{
|
{
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff heels data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.Heels);
|
var oldManip = Norm(oldData.ManipulationData);
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.Heels);
|
var newManip = Norm(newData.ManipulationData);
|
||||||
|
|
||||||
|
if (!string.Equals(oldManip, newManip, StringComparison.Ordinal) || forceRedrawOnForcedApply)
|
||||||
|
{
|
||||||
|
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Manip different) => {change}",
|
||||||
|
applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip);
|
||||||
|
set.Add(PlayerChanges.ModManip);
|
||||||
|
set.Add(PlayerChanges.ForcedRedraw);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(Norm(oldData.HeelsData), Norm(newData.HeelsData), StringComparison.Ordinal)
|
||||||
|
|| (forceApplyCustomization && HasText(newData.HeelsData)))
|
||||||
|
set.Add(PlayerChanges.Heels);
|
||||||
|
|
||||||
|
if (!string.Equals(Norm(oldData.HonorificData), Norm(newData.HonorificData), StringComparison.Ordinal)
|
||||||
|
|| (forceApplyCustomization && HasText(newData.HonorificData)))
|
||||||
|
set.Add(PlayerChanges.Honorific);
|
||||||
|
|
||||||
|
if (!string.Equals(Norm(oldData.MoodlesData), Norm(newData.MoodlesData), StringComparison.Ordinal)
|
||||||
|
|| (forceApplyCustomization && HasText(newData.MoodlesData)))
|
||||||
|
set.Add(PlayerChanges.Moodles);
|
||||||
|
|
||||||
|
if (!string.Equals(Norm(oldData.PetNamesData), Norm(newData.PetNamesData), StringComparison.Ordinal)
|
||||||
|
|| (forceApplyCustomization && HasText(newData.PetNamesData)))
|
||||||
|
set.Add(PlayerChanges.PetNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool honorificDataDifferent = !string.Equals(oldData.HonorificData, newData.HonorificData, StringComparison.Ordinal);
|
if (set.Count > 0)
|
||||||
if (honorificDataDifferent || (forceApplyCustomization && !string.IsNullOrEmpty(newData.HonorificData)))
|
charaDataToUpdate[objectKind] = set;
|
||||||
{
|
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff honorific data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.Honorific);
|
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.Honorific);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool moodlesDataDifferent = !string.Equals(oldData.MoodlesData, newData.MoodlesData, StringComparison.Ordinal);
|
|
||||||
if (moodlesDataDifferent || (forceApplyCustomization && !string.IsNullOrEmpty(newData.MoodlesData)))
|
|
||||||
{
|
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff moodles data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.Moodles);
|
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.Moodles);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool petNamesDataDifferent = !string.Equals(oldData.PetNamesData, newData.PetNamesData, StringComparison.Ordinal);
|
|
||||||
if (petNamesDataDifferent || (forceApplyCustomization && !string.IsNullOrEmpty(newData.PetNamesData)))
|
|
||||||
{
|
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff petnames data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.PetNames);
|
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.PetNames);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (KeyValuePair<ObjectKind, HashSet<PlayerChanges>> data in charaDataToUpdate.ToList())
|
foreach (var k in charaDataToUpdate.Keys.ToList())
|
||||||
{
|
charaDataToUpdate[k] = [.. charaDataToUpdate[k].OrderBy(p => (int)p)];
|
||||||
if (!data.Value.Any()) charaDataToUpdate.Remove(data.Key);
|
|
||||||
else charaDataToUpdate[data.Key] = [.. data.Value.OrderByDescending(p => (int)p)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return charaDataToUpdate;
|
return charaDataToUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static T DeepClone<T>(this T obj)
|
public static T DeepClone<T>(this T obj)
|
||||||
{
|
{
|
||||||
return JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(obj))!;
|
return JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(obj))!;
|
||||||
|
|||||||
@@ -436,11 +436,9 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
Logger.LogDebug("GUID {requestId} on server {uri} for files {files}",
|
Logger.LogDebug("GUID {requestId} on server {uri} for files {files}",
|
||||||
requestId, transfers[0].DownloadUri, string.Join(", ", transfers.Select(c => c.Hash)));
|
requestId, transfers[0].DownloadUri, string.Join(", ", transfers.Select(c => c.Hash)));
|
||||||
|
|
||||||
// Wait for ready WITHOUT holding a slot
|
|
||||||
SetStatus(statusKey, DownloadStatus.WaitingForQueue);
|
SetStatus(statusKey, DownloadStatus.WaitingForQueue);
|
||||||
await WaitForDownloadReady(transfers, requestId, ct).ConfigureAwait(false);
|
await WaitForDownloadReady(transfers, requestId, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
// Hold slot ONLY for the GET
|
|
||||||
SetStatus(statusKey, DownloadStatus.WaitingForSlot);
|
SetStatus(statusKey, DownloadStatus.WaitingForSlot);
|
||||||
await using ((await AcquireSlotAsync(ct).ConfigureAwait(false)).ConfigureAwait(false))
|
await using ((await AcquireSlotAsync(ct).ConfigureAwait(false)).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
@@ -462,7 +460,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
bool skipDecimation)
|
bool skipDecimation)
|
||||||
{
|
{
|
||||||
SetStatus(downloadStatusKey, DownloadStatus.Decompressing);
|
SetStatus(downloadStatusKey, DownloadStatus.Decompressing);
|
||||||
MarkTransferredFiles(downloadStatusKey, 1);
|
|
||||||
|
var extracted = 0;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -471,6 +470,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
while (fileBlockStream.Position < fileBlockStream.Length)
|
while (fileBlockStream.Position < fileBlockStream.Length)
|
||||||
{
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
(string fileHash, long fileLengthBytes) = ReadBlockFileHeader(fileBlockStream);
|
(string fileHash, long fileLengthBytes) = ReadBlockFileHeader(fileBlockStream);
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -480,72 +481,69 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
var len = checked((int)fileLengthBytes);
|
var len = checked((int)fileLengthBytes);
|
||||||
|
|
||||||
|
if (fileBlockStream.Position + len > fileBlockStream.Length)
|
||||||
|
throw new EndOfStreamException();
|
||||||
|
|
||||||
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}, skipping {len} bytes",
|
||||||
// still need to skip bytes:
|
downloadLabel, fileHash, len);
|
||||||
var skip = checked((int)fileLengthBytes);
|
|
||||||
fileBlockStream.Position += skip;
|
fileBlockStream.Seek(len, SeekOrigin.Current);
|
||||||
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.LogTrace("{dlName}: Extracting {fileHash}:{len} => {dest}",
|
||||||
|
downloadLabel, fileHash, len, filePath);
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
MungeBuffer(compressed);
|
|
||||||
var decompressed = LZ4Wrapper.Unwrap(compressed);
|
|
||||||
|
|
||||||
if (rawSizeLookup.TryGetValue(fileHash, out var expectedRawSize)
|
|
||||||
&& expectedRawSize > 0
|
|
||||||
&& decompressed.LongLength != expectedRawSize)
|
|
||||||
{
|
|
||||||
await _fileCompactor.WriteAllBytesAsync(filePath, Array.Empty<byte>(), ct).ConfigureAwait(false);
|
|
||||||
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale, skipDecimation);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
MungeBuffer(compressed);
|
MungeBuffer(compressed);
|
||||||
|
|
||||||
await _decompressGate.WaitAsync(ct).ConfigureAwait(false);
|
await _decompressGate.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
byte[] decompressed;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// offload CPU-intensive decompression to threadpool to free up worker
|
decompressed = await Task.Run(() => LZ4Wrapper.Unwrap(compressed), ct).ConfigureAwait(false);
|
||||||
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, skipDecimation);
|
|
||||||
}, ct).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_decompressGate.Release();
|
_decompressGate.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rawSizeLookup.TryGetValue(fileHash, out var expectedRawSize)
|
||||||
|
&& expectedRawSize > 0
|
||||||
|
&& decompressed.LongLength != expectedRawSize)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(
|
||||||
|
"{dlName}: Size mismatch for {fileHash} (expected {expected}, got {actual}). Treating as corrupt.",
|
||||||
|
downloadLabel, fileHash, expectedRawSize, decompressed.LongLength);
|
||||||
|
|
||||||
|
try { if (File.Exists(filePath)) File.Delete(filePath); } catch { /* ignore */ }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _fileCompactor.WriteAllBytesAsync(filePath, decompressed, ct, enqueueCompaction: false).ConfigureAwait(false);
|
||||||
|
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale, skipDecimation);
|
||||||
|
|
||||||
|
extracted++;
|
||||||
|
MarkTransferredFiles(downloadStatusKey, extracted);
|
||||||
}
|
}
|
||||||
catch (EndOfStreamException)
|
catch (EndOfStreamException)
|
||||||
{
|
{
|
||||||
Logger.LogWarning("{dlName}: Failure to extract file {fileHash}, stream ended prematurely", downloadLabel, fileHash);
|
Logger.LogWarning("{dlName}: Block ended mid-entry while extracting {fileHash}", downloadLabel, fileHash);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogWarning(e, "{dlName}: Error during decompression", downloadLabel);
|
Logger.LogWarning(ex, "{dlName}: Error extracting {fileHash} from block", downloadLabel, fileHash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
SetStatus(downloadStatusKey, DownloadStatus.Completed);
|
SetStatus(downloadStatusKey, DownloadStatus.Completed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (EndOfStreamException)
|
catch (EndOfStreamException)
|
||||||
{
|
{
|
||||||
@@ -601,11 +599,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
_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;
|
||||||
}
|
}
|
||||||
@@ -1033,48 +1030,58 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private async Task ProcessDeferredCompressionsAsync(CancellationToken ct)
|
private async Task ProcessDeferredCompressionsAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (_deferredCompressionQueue.IsEmpty)
|
if (_deferredCompressionQueue.IsEmpty || !_configService.Current.UseCompactor)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var filesToCompress = new List<string>();
|
// Drain queue into a unique set (same file can be enqueued multiple times)
|
||||||
|
var filesToCompact = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
while (_deferredCompressionQueue.TryDequeue(out var filePath))
|
while (_deferredCompressionQueue.TryDequeue(out var filePath))
|
||||||
{
|
{
|
||||||
if (File.Exists(filePath))
|
if (!string.IsNullOrWhiteSpace(filePath))
|
||||||
filesToCompress.Add(filePath);
|
filesToCompact.Add(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filesToCompress.Count == 0)
|
if (filesToCompact.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Logger.LogDebug("Starting deferred compression of {count} files", filesToCompress.Count);
|
Logger.LogDebug("Starting deferred compaction of {count} files", filesToCompact.Count);
|
||||||
|
|
||||||
var compressionWorkers = Math.Clamp(Environment.ProcessorCount / 4, 2, 4);
|
var enqueueWorkers = Math.Clamp(Environment.ProcessorCount / 4, 1, 2);
|
||||||
|
|
||||||
await Parallel.ForEachAsync(filesToCompress,
|
await Parallel.ForEachAsync(
|
||||||
|
filesToCompact,
|
||||||
new ParallelOptions
|
new ParallelOptions
|
||||||
{
|
{
|
||||||
MaxDegreeOfParallelism = compressionWorkers,
|
MaxDegreeOfParallelism = enqueueWorkers,
|
||||||
CancellationToken = ct
|
CancellationToken = ct
|
||||||
},
|
},
|
||||||
async (filePath, token) =>
|
async (filePath, token) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
return;
|
||||||
|
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
if (_configService.Current.UseCompactor && File.Exists(filePath))
|
|
||||||
{
|
_fileCompactor.RequestCompaction(filePath);
|
||||||
var bytes = await File.ReadAllBytesAsync(filePath, token).ConfigureAwait(false);
|
|
||||||
await _fileCompactor.WriteAllBytesAsync(filePath, bytes, token).ConfigureAwait(false);
|
Logger.LogTrace("Deferred compaction queued: {filePath}", filePath);
|
||||||
Logger.LogTrace("Compressed file: {filePath}", filePath);
|
}
|
||||||
}
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Deferred compaction cancelled for file: {filePath}", filePath);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogWarning(ex, "Failed to compress file: {filePath}", filePath);
|
Logger.LogWarning(ex, "Failed to queue deferred compaction for file: {filePath}", filePath);
|
||||||
}
|
}
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
Logger.LogDebug("Completed deferred compression of {count} files", filesToCompress.Count);
|
Logger.LogDebug("Completed queuing deferred compaction of {count} files", filesToCompact.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class InlineProgress : IProgress<long>
|
private sealed class InlineProgress : IProgress<long>
|
||||||
|
|||||||
Reference in New Issue
Block a user