Initialize migration. (#88)

Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: cake <admin@cakeandbanana.nl>
Reviewed-on: #88
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
Co-authored-by: defnotken <defnotken@noreply.git.lightless-sync.org>
Co-committed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
This commit was merged in pull request #88.
This commit is contained in:
2025-11-29 18:02:39 +01:00
committed by cake
parent 9e12725f89
commit 740b58afc4
63 changed files with 1720 additions and 1005 deletions

View File

@@ -72,7 +72,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
{ {
while (_dalamudUtil.IsOnFrameworkThread && !token.IsCancellationRequested) while (_dalamudUtil.IsOnFrameworkThread && !token.IsCancellationRequested)
{ {
await Task.Delay(1).ConfigureAwait(false); await Task.Delay(1, token).ConfigureAwait(false);
} }
RecalculateFileCacheSize(token); RecalculateFileCacheSize(token);
@@ -101,8 +101,8 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
} }
record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null); record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null);
private readonly Dictionary<string, WatcherChange> _watcherChanges = new Dictionary<string, WatcherChange>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, WatcherChange> _watcherChanges = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, WatcherChange> _lightlessChanges = new Dictionary<string, WatcherChange>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, WatcherChange> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
public void StopMonitoring() public void StopMonitoring()
{ {
@@ -128,7 +128,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
} }
var fsType = FileSystemHelper.GetFilesystemType(_configService.Current.CacheFolder, _dalamudUtil.IsWine); var fsType = FileSystemHelper.GetFilesystemType(_configService.Current.CacheFolder, _dalamudUtil.IsWine);
if (fsType == FileSystemHelper.FilesystemType.NTFS) if (fsType == FileSystemHelper.FilesystemType.NTFS && !_dalamudUtil.IsWine)
{ {
StorageisNTFS = true; StorageisNTFS = true;
Logger.LogInformation("Lightless Storage is on NTFS drive: {isNtfs}", StorageisNTFS); Logger.LogInformation("Lightless Storage is on NTFS drive: {isNtfs}", StorageisNTFS);
@@ -259,6 +259,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
private CancellationTokenSource _penumbraFswCts = new(); private CancellationTokenSource _penumbraFswCts = new();
private CancellationTokenSource _lightlessFswCts = new(); private CancellationTokenSource _lightlessFswCts = new();
public FileSystemWatcher? PenumbraWatcher { get; private set; } public FileSystemWatcher? PenumbraWatcher { get; private set; }
public FileSystemWatcher? LightlessWatcher { get; private set; } public FileSystemWatcher? LightlessWatcher { get; private set; }
@@ -509,13 +510,13 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
} }
FileCacheSize = (totalSize + totalSizeDownscaled); FileCacheSize = (totalSize + totalSizeDownscaled);
} }
else else
{ {
FileCacheSize = totalSize; FileCacheSize = totalSize;
} }
var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d); var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d);
if (FileCacheSize < maxCacheInBytes) if (FileCacheSize < maxCacheInBytes)
return; return;
@@ -556,12 +557,19 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing); base.Dispose(disposing);
_scanCancellationTokenSource?.Cancel(); // Disposing of file system watchers
PenumbraWatcher?.Dispose(); PenumbraWatcher?.Dispose();
LightlessWatcher?.Dispose(); LightlessWatcher?.Dispose();
// Disposing of cancellation token sources
_scanCancellationTokenSource?.CancelDispose();
_scanCancellationTokenSource?.Dispose();
_penumbraFswCts?.CancelDispose(); _penumbraFswCts?.CancelDispose();
_penumbraFswCts?.Dispose();
_lightlessFswCts?.CancelDispose(); _lightlessFswCts?.CancelDispose();
_lightlessFswCts?.Dispose();
_periodicCalculationTokenSource?.CancelDispose(); _periodicCalculationTokenSource?.CancelDispose();
_periodicCalculationTokenSource?.Dispose();
} }
private void FullFileScan(CancellationToken ct) private void FullFileScan(CancellationToken ct)
@@ -639,7 +647,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
List<FileCacheEntity> entitiesToRemove = []; List<FileCacheEntity> entitiesToRemove = [];
List<FileCacheEntity> entitiesToUpdate = []; List<FileCacheEntity> entitiesToUpdate = [];
object sync = new(); Lock sync = new();
Thread[] workerThreads = new Thread[threadCount]; Thread[] workerThreads = new Thread[threadCount];
ConcurrentQueue<FileCacheEntity> fileCaches = new(_fileDbManager.GetAllFileCaches()); ConcurrentQueue<FileCacheEntity> fileCaches = new(_fileDbManager.GetAllFileCaches());

View File

@@ -18,6 +18,7 @@ public sealed class FileCacheManager : IHostedService
public const string PenumbraPrefix = "{penumbra}"; public const string PenumbraPrefix = "{penumbra}";
private const int FileCacheVersion = 1; private const int FileCacheVersion = 1;
private const string FileCacheVersionHeaderPrefix = "#lightless-file-cache-version:"; private const string FileCacheVersionHeaderPrefix = "#lightless-file-cache-version:";
private readonly SemaphoreSlim _fileWriteSemaphore = new(1, 1);
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly LightlessMediator _lightlessMediator; private readonly LightlessMediator _lightlessMediator;
private readonly string _csvPath; private readonly string _csvPath;
@@ -41,11 +42,8 @@ public sealed class FileCacheManager : IHostedService
private string CsvBakPath => _csvPath + ".bak"; private string CsvBakPath => _csvPath + ".bak";
private static string NormalizeSeparators(string path) private static string NormalizeSeparators(string path) => path.Replace("/", "\\", StringComparison.Ordinal)
{
return path.Replace("/", "\\", StringComparison.Ordinal)
.Replace("\\\\", "\\", StringComparison.Ordinal); .Replace("\\\\", "\\", StringComparison.Ordinal);
}
private static string NormalizePrefixedPathKey(string prefixedPath) private static string NormalizePrefixedPathKey(string prefixedPath)
{ {
@@ -134,13 +132,9 @@ public sealed class FileCacheManager : IHostedService
chosenLength = penumbraMatch; chosenLength = penumbraMatch;
} }
if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch)) if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch) && cacheMatch > chosenLength)
{ {
if (cacheMatch > chosenLength) chosenPrefixed = cachePrefixed;
{
chosenPrefixed = cachePrefixed;
chosenLength = cacheMatch;
}
} }
return NormalizePrefixedPathKey(chosenPrefixed ?? normalized); return NormalizePrefixedPathKey(chosenPrefixed ?? normalized);
@@ -176,27 +170,53 @@ public sealed class FileCacheManager : IHostedService
return CreateFileCacheEntity(fi, prefixedPath); return CreateFileCacheEntity(fi, prefixedPath);
} }
public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null)).ToList(); public List<FileCacheEntity> GetAllFileCaches() => [.. _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null))];
public List<FileCacheEntity> GetAllFileCachesByHash(string hash, bool ignoreCacheEntries = false, bool validate = true) public List<FileCacheEntity> GetAllFileCachesByHash(string hash, bool ignoreCacheEntries = false, bool validate = true)
{ {
List<FileCacheEntity> output = []; var output = new List<FileCacheEntity>();
if (_fileCaches.TryGetValue(hash, out var fileCacheEntities))
if (!_fileCaches.TryGetValue(hash, out var fileCacheEntities))
return output;
foreach (var fileCache in fileCacheEntities.Values
.Where(c => !ignoreCacheEntries || !c.IsCacheEntry))
{ {
foreach (var fileCache in fileCacheEntities.Values.Where(c => !ignoreCacheEntries || !c.IsCacheEntry).ToList()) if (!validate)
{ {
if (!validate) output.Add(fileCache);
{ continue;
output.Add(fileCache); }
}
else var validated = GetValidatedFileCache(fileCache);
{ if (validated != null)
var validated = GetValidatedFileCache(fileCache); output.Add(validated);
if (validated != null) }
{
output.Add(validated); return output;
} }
}
public async Task<List<FileCacheEntity>> GetAllFileCachesByHashAsync(string hash, bool ignoreCacheEntries = false, bool validate = true,CancellationToken token = default)
{
var output = new List<FileCacheEntity>();
if (!_fileCaches.TryGetValue(hash, out var fileCacheEntities))
return output;
foreach (var fileCache in fileCacheEntities.Values.Where(c => !ignoreCacheEntries || !c.IsCacheEntry))
{
token.ThrowIfCancellationRequested();
if (!validate)
{
output.Add(fileCache);
}
else
{
var validated = await GetValidatedFileCacheAsync(fileCache, token).ConfigureAwait(false);
if (validated != null)
output.Add(validated);
} }
} }
@@ -237,11 +257,11 @@ public sealed class FileCacheManager : IHostedService
brokenEntities.Add(fileCache); brokenEntities.Add(fileCache);
return; return;
} }
var algo = Crypto.DetectAlgo(fileCache.Hash);
string computedHash; string computedHash;
try try
{ {
computedHash = await Crypto.GetFileHashAsync(fileCache.ResolvedFilepath, token).ConfigureAwait(false); computedHash = await Crypto.ComputeFileHashAsync(fileCache.ResolvedFilepath, algo, token).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -253,8 +273,8 @@ public sealed class FileCacheManager : IHostedService
if (!string.Equals(computedHash, fileCache.Hash, StringComparison.Ordinal)) if (!string.Equals(computedHash, fileCache.Hash, StringComparison.Ordinal))
{ {
_logger.LogInformation( _logger.LogInformation(
"Hash mismatch: {file} (got {computedHash}, expected {expected})", "Hash mismatch: {file} (got {computedHash}, expected {expected} : hash {hash})",
fileCache.ResolvedFilepath, computedHash, fileCache.Hash); fileCache.ResolvedFilepath, computedHash, fileCache.Hash, algo);
brokenEntities.Add(fileCache); brokenEntities.Add(fileCache);
} }
@@ -429,12 +449,13 @@ public sealed class FileCacheManager : IHostedService
_logger.LogTrace("Updating hash for {path}", fileCache.ResolvedFilepath); _logger.LogTrace("Updating hash for {path}", fileCache.ResolvedFilepath);
var oldHash = fileCache.Hash; var oldHash = fileCache.Hash;
var prefixedPath = fileCache.PrefixedFilePath; var prefixedPath = fileCache.PrefixedFilePath;
var algo = Crypto.DetectAlgo(fileCache.ResolvedFilepath);
if (computeProperties) if (computeProperties)
{ {
var fi = new FileInfo(fileCache.ResolvedFilepath); var fi = new FileInfo(fileCache.ResolvedFilepath);
fileCache.Size = fi.Length; fileCache.Size = fi.Length;
fileCache.CompressedSize = null; fileCache.CompressedSize = null;
fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath); fileCache.Hash = Crypto.ComputeFileHash(fileCache.ResolvedFilepath, algo);
fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture); fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
} }
RemoveHashedFile(oldHash, prefixedPath); RemoveHashedFile(oldHash, prefixedPath);
@@ -485,6 +506,44 @@ public sealed class FileCacheManager : IHostedService
} }
} }
public async Task WriteOutFullCsvAsync(CancellationToken cancellationToken = default)
{
await _fileWriteSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var sb = new StringBuilder();
sb.AppendLine(BuildVersionHeader());
foreach (var entry in _fileCaches.Values
.SelectMany(k => k.Values)
.OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
{
sb.AppendLine(entry.CsvEntry);
}
if (File.Exists(_csvPath))
{
File.Copy(_csvPath, CsvBakPath, overwrite: true);
}
try
{
await File.WriteAllTextAsync(_csvPath, sb.ToString(), cancellationToken).ConfigureAwait(false);
File.Delete(CsvBakPath);
}
catch
{
await File.WriteAllTextAsync(CsvBakPath, sb.ToString(), cancellationToken).ConfigureAwait(false);
}
}
finally
{
_fileWriteSemaphore.Release();
}
}
private void EnsureCsvHeaderLocked() private void EnsureCsvHeaderLocked()
{ {
if (!File.Exists(_csvPath)) if (!File.Exists(_csvPath))
@@ -577,7 +636,8 @@ public sealed class FileCacheManager : IHostedService
private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null) private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null)
{ {
hash ??= Crypto.GetFileHash(fileInfo.FullName); var algo = Crypto.DetectAlgo(Path.GetFileNameWithoutExtension(fileInfo.Name));
hash ??= Crypto.ComputeFileHash(fileInfo.FullName, algo);
var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileInfo.Length); var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileInfo.Length);
entity = ReplacePathPrefixes(entity); entity = ReplacePathPrefixes(entity);
AddHashedFile(entity); AddHashedFile(entity);
@@ -585,13 +645,13 @@ public sealed class FileCacheManager : IHostedService
{ {
if (!File.Exists(_csvPath)) if (!File.Exists(_csvPath))
{ {
File.WriteAllLines(_csvPath, new[] { BuildVersionHeader(), entity.CsvEntry }); File.WriteAllLines(_csvPath, [BuildVersionHeader(), entity.CsvEntry]);
_csvHeaderEnsured = true; _csvHeaderEnsured = true;
} }
else else
{ {
EnsureCsvHeaderLockedCached(); EnsureCsvHeaderLockedCached();
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry }); File.AppendAllLines(_csvPath, [entity.CsvEntry]);
} }
} }
var result = GetFileCacheByPath(fileInfo.FullName); var result = GetFileCacheByPath(fileInfo.FullName);
@@ -602,11 +662,17 @@ public sealed class FileCacheManager : IHostedService
private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache) private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache)
{ {
var resultingFileCache = ReplacePathPrefixes(fileCache); var resultingFileCache = ReplacePathPrefixes(fileCache);
//_logger.LogTrace("Validating {path}", fileCache.PrefixedFilePath);
resultingFileCache = Validate(resultingFileCache); resultingFileCache = Validate(resultingFileCache);
return resultingFileCache; return resultingFileCache;
} }
private async Task<FileCacheEntity?> GetValidatedFileCacheAsync(FileCacheEntity fileCache, CancellationToken token = default)
{
var resultingFileCache = ReplacePathPrefixes(fileCache);
resultingFileCache = await ValidateAsync(resultingFileCache, token).ConfigureAwait(false);
return resultingFileCache;
}
private FileCacheEntity ReplacePathPrefixes(FileCacheEntity fileCache) private FileCacheEntity ReplacePathPrefixes(FileCacheEntity fileCache)
{ {
if (fileCache.PrefixedFilePath.StartsWith(PenumbraPrefix, StringComparison.OrdinalIgnoreCase)) if (fileCache.PrefixedFilePath.StartsWith(PenumbraPrefix, StringComparison.OrdinalIgnoreCase))
@@ -629,6 +695,7 @@ public sealed class FileCacheManager : IHostedService
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath); RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
return null; return null;
} }
var file = new FileInfo(fileCache.ResolvedFilepath); var file = new FileInfo(fileCache.ResolvedFilepath);
if (!file.Exists) if (!file.Exists)
{ {
@@ -636,7 +703,8 @@ public sealed class FileCacheManager : IHostedService
return null; return null;
} }
if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal)) var lastWriteTicks = file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
if (!string.Equals(lastWriteTicks, fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
{ {
UpdateHashedFile(fileCache); UpdateHashedFile(fileCache);
} }
@@ -644,7 +712,34 @@ public sealed class FileCacheManager : IHostedService
return fileCache; return fileCache;
} }
public Task StartAsync(CancellationToken cancellationToken) private async Task<FileCacheEntity?> ValidateAsync(FileCacheEntity fileCache, CancellationToken token)
{
if (string.IsNullOrWhiteSpace(fileCache.ResolvedFilepath))
{
_logger.LogWarning("FileCacheEntity has empty ResolvedFilepath for hash {hash}, prefixed path {prefixed}", fileCache.Hash, fileCache.PrefixedFilePath);
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
return null;
}
return await Task.Run(() =>
{
var file = new FileInfo(fileCache.ResolvedFilepath);
if (!file.Exists)
{
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
return null;
}
if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
{
UpdateHashedFile(fileCache);
}
return fileCache;
}, token).ConfigureAwait(false);
}
public async Task StartAsync(CancellationToken cancellationToken)
{ {
_logger.LogInformation("Starting FileCacheManager"); _logger.LogInformation("Starting FileCacheManager");
@@ -695,14 +790,14 @@ public sealed class FileCacheManager : IHostedService
try try
{ {
_logger.LogInformation("Attempting to read {csvPath}", _csvPath); _logger.LogInformation("Attempting to read {csvPath}", _csvPath);
entries = File.ReadAllLines(_csvPath); entries = await File.ReadAllLinesAsync(_csvPath, cancellationToken).ConfigureAwait(false);
success = true; success = true;
} }
catch (Exception ex) catch (Exception ex)
{ {
attempts++; attempts++;
_logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath); _logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath);
Task.Delay(100, cancellationToken); await Task.Delay(100, cancellationToken).ConfigureAwait(false);
} }
} }
@@ -729,7 +824,7 @@ public sealed class FileCacheManager : IHostedService
BackupUnsupportedCache("invalid-version"); BackupUnsupportedCache("invalid-version");
parseEntries = false; parseEntries = false;
rewriteRequired = true; rewriteRequired = true;
entries = Array.Empty<string>(); entries = [];
} }
else if (parsedVersion != FileCacheVersion) else if (parsedVersion != FileCacheVersion)
{ {
@@ -737,7 +832,7 @@ public sealed class FileCacheManager : IHostedService
BackupUnsupportedCache($"v{parsedVersion}"); BackupUnsupportedCache($"v{parsedVersion}");
parseEntries = false; parseEntries = false;
rewriteRequired = true; rewriteRequired = true;
entries = Array.Empty<string>(); entries = [];
} }
else else
{ {
@@ -817,20 +912,18 @@ public sealed class FileCacheManager : IHostedService
if (rewriteRequired) if (rewriteRequired)
{ {
WriteOutFullCsv(); await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false);
} }
} }
_logger.LogInformation("Started FileCacheManager"); _logger.LogInformation("Started FileCacheManager");
_lightlessMediator.Publish(new FileCacheInitializedMessage());
_lightlessMediator.Publish(new FileCacheInitializedMessage()); await Task.CompletedTask.ConfigureAwait(false);
return Task.CompletedTask;
} }
public Task StopAsync(CancellationToken cancellationToken) public async Task StopAsync(CancellationToken cancellationToken)
{ {
WriteOutFullCsv(); await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false);
return Task.CompletedTask; await Task.CompletedTask.ConfigureAwait(false);
} }
} }

View File

@@ -12,12 +12,11 @@ using static LightlessSync.Utils.FileSystemHelper;
namespace LightlessSync.FileCache; namespace LightlessSync.FileCache;
public sealed class FileCompactor : IDisposable public sealed partial class FileCompactor : IDisposable
{ {
public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U; public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U;
public const ulong WOF_PROVIDER_FILE = 2UL; public const ulong WOF_PROVIDER_FILE = 2UL;
public const int _maxRetries = 3; public const int _maxRetries = 3;
private readonly bool _isWindows;
private readonly ConcurrentDictionary<string, byte> _pendingCompactions; private readonly ConcurrentDictionary<string, byte> _pendingCompactions;
private readonly ILogger<FileCompactor> _logger; private readonly ILogger<FileCompactor> _logger;
@@ -31,23 +30,26 @@ public sealed class FileCompactor : IDisposable
private readonly SemaphoreSlim _globalGate; private readonly SemaphoreSlim _globalGate;
//Limit btrfs gate on half of threads given to compactor. //Limit btrfs gate on half of threads given to compactor.
private static readonly SemaphoreSlim _btrfsGate = new(4, 4); private readonly SemaphoreSlim _btrfsGate;
private readonly BatchFilefragService _fragBatch; private readonly BatchFilefragService _fragBatch;
private readonly WOF_FILE_COMPRESSION_INFO_V1 _efInfo = new() private readonly bool _isWindows;
private readonly int _workerCount;
private readonly WofFileCompressionInfoV1 _efInfo = new()
{ {
Algorithm = (int)CompressionAlgorithm.XPRESS8K, Algorithm = (int)CompressionAlgorithm.XPRESS8K,
Flags = 0 Flags = 0
}; };
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct WOF_FILE_COMPRESSION_INFO_V1 private struct WofFileCompressionInfoV1
{ {
public int Algorithm; public int Algorithm;
public ulong Flags; public ulong Flags;
} }
private enum CompressionAlgorithm private enum CompressionAlgorithm
{ {
NO_COMPRESSION = -2, NO_COMPRESSION = -2,
LZNT1 = -1, LZNT1 = -1,
@@ -71,29 +73,36 @@ public sealed class FileCompactor : IDisposable
SingleWriter = false SingleWriter = false
}); });
//Amount of threads given for the compactor
int workers = Math.Clamp(Math.Min(Environment.ProcessorCount / 2, 4), 1, 8); int workers = Math.Clamp(Math.Min(Environment.ProcessorCount / 2, 4), 1, 8);
//Setup gates for the threads and setup worker count
_globalGate = new SemaphoreSlim(workers, workers); _globalGate = new SemaphoreSlim(workers, workers);
int workerCount = Math.Max(workers * 2, workers); _btrfsGate = new SemaphoreSlim(workers / 2, workers / 2);
_workerCount = Math.Max(workers * 2, workers);
for (int i = 0; i < workerCount; i++) //Setup workers on the queue
for (int i = 0; i < _workerCount; i++)
{ {
int workerId = i;
_workers.Add(Task.Factory.StartNew( _workers.Add(Task.Factory.StartNew(
() => ProcessQueueWorkerAsync(_compactionCts.Token), () => ProcessQueueWorkerAsync(workerId, _compactionCts.Token),
_compactionCts.Token, _compactionCts.Token,
TaskCreationOptions.LongRunning, TaskCreationOptions.LongRunning,
TaskScheduler.Default).Unwrap()); TaskScheduler.Default).Unwrap());
} }
//Uses an batching service for the filefrag command on Linux
_fragBatch = new BatchFilefragService( _fragBatch = new BatchFilefragService(
useShell: _dalamudUtilService.IsWine, useShell: _dalamudUtilService.IsWine,
log: _logger, log: _logger,
batchSize: 64, batchSize: 64,
flushMs: 25, flushMs: 25,
runDirect: RunProcessDirect, runDirect: RunProcessDirect,
runShell: RunProcessShell runShell: RunProcessShell
); );
_logger.LogInformation("FileCompactor started with {workers} workers", workerCount); _logger.LogInformation("FileCompactor started with {workers} workers", _workerCount);
} }
public bool MassCompactRunning { get; private set; } public bool MassCompactRunning { get; private set; }
@@ -103,37 +112,91 @@ public sealed class FileCompactor : IDisposable
/// Compact the storage of the Cache Folder /// Compact the storage of the Cache Folder
/// </summary> /// </summary>
/// <param name="compress">Used to check if files needs to be compressed</param> /// <param name="compress">Used to check if files needs to be compressed</param>
public void CompactStorage(bool compress) public void CompactStorage(bool compress, int? maxDegree = null)
{ {
MassCompactRunning = true; MassCompactRunning = true;
try try
{ {
var allFiles = Directory.EnumerateFiles(_lightlessConfigService.Current.CacheFolder).ToList(); var folder = _lightlessConfigService.Current.CacheFolder;
int total = allFiles.Count; if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder))
int current = 0;
foreach (var file in allFiles)
{ {
current++; if (_logger.IsEnabled(LogLevel.Warning))
Progress = $"{current}/{total}"; _logger.LogWarning("Filecompacator couldnt find your Cache folder: {folder}", folder);
Progress = "0/0";
return;
}
var files = Directory.EnumerateFiles(folder).ToArray();
var total = files.Length;
Progress = $"0/{total}";
if (total == 0) return;
var degree = maxDegree ?? Math.Clamp(Environment.ProcessorCount / 2, 1, 8);
var done = 0;
int workerCounter = -1;
var po = new ParallelOptions
{
MaxDegreeOfParallelism = degree,
CancellationToken = _compactionCts.Token
};
Parallel.ForEach(files, po, localInit: () => Interlocked.Increment(ref workerCounter), body: (file, state, workerId) =>
{
_globalGate.WaitAsync(po.CancellationToken).GetAwaiter().GetResult();
if (!_pendingCompactions.TryAdd(file, 0))
return -1;
try try
{ {
// Compress or decompress files try
if (compress) {
CompactFile(file); if (compress)
else {
DecompressFile(file); if (_lightlessConfigService.Current.UseCompactor)
CompactFile(file, workerId);
}
else
{
DecompressFile(file, workerId);
}
}
catch (IOException ioEx)
{
_logger.LogDebug(ioEx, "[W{worker}] File being read/written, skipping file: {file}", workerId, file);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[W{worker}] Error processing file: {file}", workerId, file);
}
finally
{
var n = Interlocked.Increment(ref done);
Progress = $"{n}/{total}";
}
} }
catch (IOException ioEx) finally
{ {
_logger.LogDebug(ioEx, "File {file} locked or busy, skipping", file); _pendingCompactions.TryRemove(file, out _);
_globalGate.Release();
} }
catch (Exception ex)
{ return workerId;
_logger.LogWarning(ex, "Error compacting/decompressing file {file}", file); },
} localFinally: _ =>
} {
//Ignore local finally for now
});
}
catch (OperationCanceledException ex)
{
_logger.LogDebug(ex, "Mass compaction call got cancelled, shutting off compactor.");
} }
finally finally
{ {
@@ -142,6 +205,7 @@ public sealed class FileCompactor : IDisposable
} }
} }
/// <summary> /// <summary>
/// Write all bytes into a directory async /// Write all bytes into a directory async
/// </summary> /// </summary>
@@ -207,16 +271,13 @@ public sealed class FileCompactor : IDisposable
? RunProcessShell($"stat -c='%b' {QuoteSingle(linuxPath)}", workingDir: null, 10000) ? RunProcessShell($"stat -c='%b' {QuoteSingle(linuxPath)}", workingDir: null, 10000)
: RunProcessDirect("stat", ["-c='%b'", linuxPath], workingDir: null, 10000); : RunProcessDirect("stat", ["-c='%b'", linuxPath], workingDir: null, 10000);
if (ok && long.TryParse(output.Trim(), out long blocks)) return (flowControl: false, value: fileInfo.Length);
return (false, blocks * 512L); // st_blocks are always 512B units
_logger.LogDebug("Btrfs size probe failed for {linux} (stat {code}, err {err}). Falling back to Length.", linuxPath, code, err);
return (false, fileInfo.Length);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogDebug(ex, "Failed Btrfs size probe for {file}, using Length", fileInfo.FullName); if (_logger.IsEnabled(LogLevel.Debug))
return (false, fileInfo.Length); _logger.LogDebug(ex, "Failed Btrfs size probe for {file}, using Length", fileInfo.FullName);
return (flowControl: true, value: fileInfo.Length);
} }
} }
@@ -257,19 +318,21 @@ public sealed class FileCompactor : IDisposable
/// <summary> /// <summary>
/// Compressing the given path with BTRFS or NTFS file system. /// Compressing the given path with BTRFS or NTFS file system.
/// </summary> /// </summary>
/// <param name="path">Path of the decompressed/normal file</param> /// <param name="filePath">Path of the decompressed/normal file</param>
private void CompactFile(string filePath) /// <param name="workerId">Worker/Process Id</param>
private void CompactFile(string filePath, int workerId)
{ {
var fi = new FileInfo(filePath); var fi = new FileInfo(filePath);
if (!fi.Exists) if (!fi.Exists)
{ {
_logger.LogTrace("Skip compaction: missing {file}", filePath); if (_logger.IsEnabled(LogLevel.Trace))
_logger.LogTrace("[W{worker}] Skip compaction: missing {file}", workerId, filePath);
return; return;
} }
var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine); var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine);
var oldSize = fi.Length; var oldSize = fi.Length;
int blockSize = GetBlockSizeForPath(fi.FullName, _logger, _dalamudUtilService.IsWine); int blockSize = (int)(GetFileSizeOnDisk(fi) / 512);
// We skipping small files (128KiB) as they slow down the system a lot for BTRFS. as BTRFS has a different blocksize it requires an different calculation. // We skipping small files (128KiB) as they slow down the system a lot for BTRFS. as BTRFS has a different blocksize it requires an different calculation.
long minSizeBytes = fsType == FilesystemType.Btrfs long minSizeBytes = fsType == FilesystemType.Btrfs
@@ -278,7 +341,8 @@ public sealed class FileCompactor : IDisposable
if (oldSize < minSizeBytes) if (oldSize < minSizeBytes)
{ {
_logger.LogTrace("Skip compaction: {file} ({size} B) < threshold ({th} B)", filePath, oldSize, minSizeBytes); if (_logger.IsEnabled(LogLevel.Trace))
_logger.LogTrace("[W{worker}] Skip compaction: {file} ({size} B) < threshold ({th} B)", workerId, filePath, oldSize, minSizeBytes);
return; return;
} }
@@ -286,20 +350,20 @@ public sealed class FileCompactor : IDisposable
{ {
if (!IsWOFCompactedFile(filePath)) if (!IsWOFCompactedFile(filePath))
{ {
_logger.LogDebug("NTFS compaction XPRESS8K: {file}", filePath);
if (WOFCompressFile(filePath)) if (WOFCompressFile(filePath))
{ {
var newSize = GetFileSizeOnDisk(fi); var newSize = GetFileSizeOnDisk(fi);
_logger.LogDebug("NTFS compressed {file} {old} -> {new}", filePath, oldSize, newSize); _logger.LogDebug("[W{worker}] NTFS compressed XPRESS8K {file} {old} -> {new}", workerId, filePath, oldSize, newSize);
} }
else else
{ {
_logger.LogWarning("NTFS compression failed or unavailable for {file}", filePath); _logger.LogWarning("[W{worker}] NTFS compression failed or unavailable for {file}", workerId, filePath);
} }
} }
else else
{ {
_logger.LogTrace("Already NTFS-compressed: {file}", filePath); if (_logger.IsEnabled(LogLevel.Trace))
_logger.LogTrace("[W{worker}] Already NTFS-compressed with XPRESS8K: {file}", workerId, filePath);
} }
return; return;
} }
@@ -308,41 +372,43 @@ public sealed class FileCompactor : IDisposable
{ {
if (!IsBtrfsCompressedFile(filePath)) if (!IsBtrfsCompressedFile(filePath))
{ {
_logger.LogDebug("Btrfs compression zstd: {file}", filePath);
if (BtrfsCompressFile(filePath)) if (BtrfsCompressFile(filePath))
{ {
var newSize = GetFileSizeOnDisk(fi); var newSize = GetFileSizeOnDisk(fi);
_logger.LogDebug("Btrfs compressed {file} {old} -> {new}", filePath, oldSize, newSize); _logger.LogDebug("[W{worker}] Btrfs compressed clzo {file} {old} -> {new}", workerId, filePath, oldSize, newSize);
} }
else else
{ {
_logger.LogWarning("Btrfs compression failed or unavailable for {file}", filePath); _logger.LogWarning("[W{worker}] Btrfs compression failed or unavailable for {file}", workerId, filePath);
} }
} }
else else
{ {
_logger.LogTrace("Already Btrfs-compressed: {file}", filePath); if (_logger.IsEnabled(LogLevel.Trace))
_logger.LogTrace("[W{worker}] Already Btrfs-compressed with clzo: {file}", workerId, filePath);
} }
return; return;
} }
_logger.LogTrace("Skip compact: unsupported FS for {file}", filePath); if (_logger.IsEnabled(LogLevel.Trace))
_logger.LogTrace("[W{worker}] Skip compact: unsupported FS for {file}", workerId, filePath);
} }
/// <summary> /// <summary>
/// Decompressing the given path with BTRFS file system or NTFS file system. /// Decompressing the given path with BTRFS file system or NTFS file system.
/// </summary> /// </summary>
/// <param name="path">Path of the compressed file</param> /// <param name="filePath">Path of the decompressed/normal file</param>
private void DecompressFile(string path) /// <param name="workerId">Worker/Process Id</param>
private void DecompressFile(string filePath, int workerId)
{ {
_logger.LogDebug("Decompress request: {file}", path); _logger.LogDebug("[W{worker}] Decompress request: {file}", workerId, filePath);
var fsType = GetFilesystemType(path, _dalamudUtilService.IsWine); var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine);
if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine) if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine)
{ {
try try
{ {
bool flowControl = DecompressWOFFile(path); bool flowControl = DecompressWOFFile(filePath, workerId);
if (!flowControl) if (!flowControl)
{ {
return; return;
@@ -350,7 +416,7 @@ public sealed class FileCompactor : IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "NTFS decompress error {file}", path); _logger.LogWarning(ex, "[W{worker}] NTFS decompress error {file}", workerId, filePath);
} }
} }
@@ -358,7 +424,7 @@ public sealed class FileCompactor : IDisposable
{ {
try try
{ {
bool flowControl = DecompressBtrfsFile(path); bool flowControl = DecompressBtrfsFile(filePath);
if (!flowControl) if (!flowControl)
{ {
return; return;
@@ -366,7 +432,7 @@ public sealed class FileCompactor : IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "Btrfs decompress error {file}", path); _logger.LogWarning(ex, "[W{worker}] Btrfs decompress error {file}", workerId, filePath);
} }
} }
} }
@@ -386,51 +452,48 @@ public sealed class FileCompactor : IDisposable
string linuxPath = isWine ? ToLinuxPathIfWine(path, isWine) : path; string linuxPath = isWine ? ToLinuxPathIfWine(path, isWine) : path;
var opts = GetMountOptionsForPath(linuxPath); var opts = GetMountOptionsForPath(linuxPath);
bool hasCompress = opts.Contains("compress", StringComparison.OrdinalIgnoreCase); if (!string.IsNullOrEmpty(opts))
bool hasCompressForce = opts.Contains("compress-force", StringComparison.OrdinalIgnoreCase); _logger.LogTrace("Mount opts for {file}: {opts}", linuxPath, opts);
if (hasCompressForce) var probe = RunProcessShell("command -v btrfs || which btrfs", timeoutMs: 5000);
var _btrfsAvailable = probe.ok && !string.IsNullOrWhiteSpace(probe.stdout);
if (!_btrfsAvailable)
_logger.LogWarning("btrfs cli not found in path. Compression will be skipped.");
var prop = isWine
? RunProcessShell($"btrfs property set -- {QuoteSingle(linuxPath)} compression none", timeoutMs: 15000)
: RunProcessDirect("btrfs", ["property", "set", "--", linuxPath, "compression", "none"], "/", 15000);
if (prop.ok) _logger.LogTrace("Set per-file 'compression none' on {file}", linuxPath);
else _logger.LogTrace("btrfs property set failed for {file} (exit {code}): {err}", linuxPath, prop.exitCode, prop.stderr);
var defrag = isWine
? RunProcessShell($"btrfs filesystem defragment -f -- {QuoteSingle(linuxPath)}", timeoutMs: 60000)
: RunProcessDirect("btrfs", ["filesystem", "defragment", "-f", "--", linuxPath], "/", 60000);
if (!defrag.ok)
{ {
_logger.LogWarning("Cannot safely decompress {file}: mount options contains compress-force ({opts}).", linuxPath, opts); _logger.LogWarning("btrfs defragment (decompress) failed for {file} (exit {code}): {err}",
linuxPath, defrag.exitCode, defrag.stderr);
return false; return false;
} }
if (hasCompress) if (!string.IsNullOrWhiteSpace(defrag.stdout))
{ _logger.LogTrace("btrfs defragment output for {file}: {out}", linuxPath, defrag.stdout.Trim());
var setCmd = $"btrfs property set -- {QuoteDouble(linuxPath)} compression none";
var (okSet, _, errSet, codeSet) = isWine
? RunProcessShell(setCmd)
: RunProcessDirect("btrfs", ["property", "set", "--", linuxPath, "compression", "none"]);
if (!okSet)
{
_logger.LogWarning("Failed to set 'compression none' on {file}, please check drive options (exit code is: {code}): {err}", linuxPath, codeSet, errSet);
return false;
}
_logger.LogTrace("Set per-file 'compression none' on {file}", linuxPath);
}
if (!IsBtrfsCompressedFile(linuxPath))
{
_logger.LogTrace("{file} is not compressed, skipping decompression completely", linuxPath);
return true;
}
var (ok, stdout, stderr, code) = isWine
? RunProcessShell($"btrfs filesystem defragment -- {QuoteDouble(linuxPath)}")
: RunProcessDirect("btrfs", ["filesystem", "defragment", "--", linuxPath]);
if (!ok)
{
_logger.LogWarning("btrfs defragment (decompress) failed for {file} (exit code is: {code}): {stderr}",
linuxPath, code, stderr);
return false;
}
if (!string.IsNullOrWhiteSpace(stdout))
_logger.LogTrace("btrfs defragment output for {file}: {out}", linuxPath, stdout.Trim());
_logger.LogInformation("Decompressed (rewritten uncompressed) Btrfs file: {file}", linuxPath); _logger.LogInformation("Decompressed (rewritten uncompressed) Btrfs file: {file}", linuxPath);
try
{
if (_fragBatch != null)
{
var compressed = _fragBatch.IsCompressedAsync(linuxPath, _compactionCts.Token).GetAwaiter().GetResult();
if (compressed)
_logger.LogTrace("Post-check: {file} still shows 'compressed' flag (may be stale).", linuxPath);
}
}
catch { /* ignore verification noisy */ }
return true; return true;
} }
catch (Exception ex) catch (Exception ex)
@@ -446,18 +509,18 @@ public sealed class FileCompactor : IDisposable
/// </summary> /// </summary>
/// <param name="path">Path of the compressed file</param> /// <param name="path">Path of the compressed file</param>
/// <returns>Decompressing state</returns> /// <returns>Decompressing state</returns>
private bool DecompressWOFFile(string path) private bool DecompressWOFFile(string path, int workerID)
{ {
//Check if its already been compressed //Check if its already been compressed
if (TryIsWofExternal(path, out bool isExternal, out int algo)) if (TryIsWofExternal(path, out bool isExternal, out int algo))
{ {
if (!isExternal) if (!isExternal)
{ {
_logger.LogTrace("Already decompressed file: {file}", path); _logger.LogTrace("[W{worker}] Already decompressed file: {file}", workerID, path);
return true; return true;
} }
var compressString = ((CompressionAlgorithm)algo).ToString(); var compressString = ((CompressionAlgorithm)algo).ToString();
_logger.LogTrace("WOF compression (algo={algo}) detected for {file}", compressString, path); _logger.LogTrace("[W{worker}] WOF compression (algo={algo}) detected for {file}", workerID, compressString, path);
} }
//This will attempt to start WOF thread. //This will attempt to start WOF thread.
@@ -471,15 +534,15 @@ public sealed class FileCompactor : IDisposable
// 342 error code means its been decompressed after the control, we handle it as it succesfully been decompressed. // 342 error code means its been decompressed after the control, we handle it as it succesfully been decompressed.
if (err == 342) if (err == 342)
{ {
_logger.LogTrace("Successfully decompressed NTFS file {file}", path); _logger.LogTrace("[W{worker}] Successfully decompressed NTFS file {file}", workerID, path);
return true; return true;
} }
_logger.LogWarning("DeviceIoControl failed for {file} with Win32 error {err}", path, err); _logger.LogWarning("[W{worker}] DeviceIoControl failed for {file} with Win32 error {err}", workerID, path, err);
return false; return false;
} }
_logger.LogTrace("Successfully decompressed NTFS file {file}", path); _logger.LogTrace("[W{worker}] Successfully decompressed NTFS file {file}", workerID, path);
return true; return true;
}); });
} }
@@ -492,6 +555,7 @@ public sealed class FileCompactor : IDisposable
/// <returns>Converted path to be used in Linux</returns> /// <returns>Converted path to be used in Linux</returns>
private string ToLinuxPathIfWine(string path, bool isWine, bool preferShell = true) private string ToLinuxPathIfWine(string path, bool isWine, bool preferShell = true)
{ {
//Return if not wine
if (!isWine || !IsProbablyWine()) if (!isWine || !IsProbablyWine())
return path; return path;
@@ -553,7 +617,7 @@ public sealed class FileCompactor : IDisposable
/// <returns>Compessing state</returns> /// <returns>Compessing state</returns>
private bool WOFCompressFile(string path) private bool WOFCompressFile(string path)
{ {
int size = Marshal.SizeOf<WOF_FILE_COMPRESSION_INFO_V1>(); int size = Marshal.SizeOf<WofFileCompressionInfoV1>();
IntPtr efInfoPtr = Marshal.AllocHGlobal(size); IntPtr efInfoPtr = Marshal.AllocHGlobal(size);
try try
@@ -606,7 +670,7 @@ public sealed class FileCompactor : IDisposable
{ {
try try
{ {
uint buf = (uint)Marshal.SizeOf<WOF_FILE_COMPRESSION_INFO_V1>(); uint buf = (uint)Marshal.SizeOf<WofFileCompressionInfoV1>();
int result = WofIsExternalFile(filePath, out int isExternal, out _, out var info, ref buf); int result = WofIsExternalFile(filePath, out int isExternal, out _, out var info, ref buf);
if (result != 0 || isExternal == 0) if (result != 0 || isExternal == 0)
return false; return false;
@@ -635,7 +699,7 @@ public sealed class FileCompactor : IDisposable
algorithm = 0; algorithm = 0;
try try
{ {
uint buf = (uint)Marshal.SizeOf<WOF_FILE_COMPRESSION_INFO_V1>(); uint buf = (uint)Marshal.SizeOf<WofFileCompressionInfoV1>();
int hr = WofIsExternalFile(path, out int ext, out _, out var info, ref buf); int hr = WofIsExternalFile(path, out int ext, out _, out var info, ref buf);
if (hr == 0 && ext != 0) if (hr == 0 && ext != 0)
{ {
@@ -644,13 +708,13 @@ public sealed class FileCompactor : IDisposable
} }
return true; return true;
} }
catch (DllNotFoundException) catch (DllNotFoundException)
{ {
return false; return false;
} }
catch (EntryPointNotFoundException) catch (EntryPointNotFoundException)
{ {
return false; return false;
} }
} }
@@ -665,8 +729,7 @@ public sealed class FileCompactor : IDisposable
{ {
try try
{ {
bool windowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); string linuxPath = _isWindows ? ResolveLinuxPathForWine(path) : path;
string linuxPath = windowsProc ? ResolveLinuxPathForWine(path) : path;
var task = _fragBatch.IsCompressedAsync(linuxPath, _compactionCts.Token); var task = _fragBatch.IsCompressedAsync(linuxPath, _compactionCts.Token);
@@ -712,6 +775,11 @@ public sealed class FileCompactor : IDisposable
return false; return false;
} }
var probe = RunProcessShell("command -v btrfs || which btrfs", timeoutMs: 5000);
var _btrfsAvailable = probe.ok && !string.IsNullOrWhiteSpace(probe.stdout);
if (!_btrfsAvailable)
_logger.LogWarning("btrfs cli not found in path. Compression will be skipped.");
(bool ok, string stdout, string stderr, int code) = (bool ok, string stdout, string stderr, int code) =
_isWindows _isWindows
? RunProcessShell($"btrfs filesystem defragment -clzo -- {QuoteSingle(linuxPath)}") ? RunProcessShell($"btrfs filesystem defragment -clzo -- {QuoteSingle(linuxPath)}")
@@ -796,9 +864,10 @@ public sealed class FileCompactor : IDisposable
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true CreateNoWindow = true,
WorkingDirectory = workingDir ?? "/",
}; };
if (!string.IsNullOrEmpty(workingDir)) psi.WorkingDirectory = workingDir;
foreach (var a in args) psi.ArgumentList.Add(a); foreach (var a in args) psi.ArgumentList.Add(a);
EnsureUnixPathEnv(psi); EnsureUnixPathEnv(psi);
@@ -812,8 +881,18 @@ public sealed class FileCompactor : IDisposable
} }
int code; int code;
try { code = proc.ExitCode; } catch { code = -1; } try { code = proc.ExitCode; }
return (code == 0, so2, se2, code); catch { code = -1; }
bool ok = code == 0;
if (!ok && code == -1 &&
string.IsNullOrWhiteSpace(se2) && !string.IsNullOrWhiteSpace(so2))
{
ok = true;
}
return (ok, so2, se2, code);
} }
/// <summary> /// <summary>
@@ -824,15 +903,14 @@ public sealed class FileCompactor : IDisposable
/// <returns>State of the process, output of the process and error with exit code</returns> /// <returns>State of the process, output of the process and error with exit code</returns>
private (bool ok, string stdout, string stderr, int exitCode) RunProcessShell(string command, string? workingDir = null, int timeoutMs = 60000) private (bool ok, string stdout, string stderr, int exitCode) RunProcessShell(string command, string? workingDir = null, int timeoutMs = 60000)
{ {
var psi = new ProcessStartInfo("/bin/bash") var psi = new ProcessStartInfo("/bin/bash")
{ {
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true CreateNoWindow = true,
WorkingDirectory = workingDir ?? "/",
}; };
if (!string.IsNullOrEmpty(workingDir)) psi.WorkingDirectory = workingDir;
// Use a Login shell so PATH includes /usr/sbin etc. AKA -lc for login shell // Use a Login shell so PATH includes /usr/sbin etc. AKA -lc for login shell
psi.ArgumentList.Add("-lc"); psi.ArgumentList.Add("-lc");
@@ -849,65 +927,72 @@ public sealed class FileCompactor : IDisposable
} }
int code; int code;
try { code = proc.ExitCode; } catch { code = -1; } try { code = proc.ExitCode; }
return (code == 0, so2, se2, code); catch { code = -1; }
bool ok = code == 0;
if (!ok && code == -1 && string.IsNullOrWhiteSpace(se2) && !string.IsNullOrWhiteSpace(so2))
{
ok = true;
}
return (ok, so2, se2, code);
} }
/// <summary> /// <summary>
/// Checking the process result for shell or direct processes /// Checking the process result for shell or direct processes
/// </summary> /// </summary>
/// <param name="proc">Process</param> /// <param name="proc">Process</param>
/// <param name="timeoutMs">How long when timeout is gotten</param> /// <param name="timeoutMs">How long when timeout goes over threshold</param>
/// <param name="token">Cancellation Token</param> /// <param name="token">Cancellation Token</param>
/// <returns>Multiple variables</returns> /// <returns>Multiple variables</returns>
private (bool success, string testy, string testi) CheckProcessResult(Process proc, int timeoutMs, CancellationToken token) private (bool success, string output, string errorCode) CheckProcessResult(Process proc, int timeoutMs, CancellationToken token)
{ {
var outTask = proc.StandardOutput.ReadToEndAsync(token); var outTask = proc.StandardOutput.ReadToEndAsync(token);
var errTask = proc.StandardError.ReadToEndAsync(token); var errTask = proc.StandardError.ReadToEndAsync(token);
var bothTasks = Task.WhenAll(outTask, errTask); var bothTasks = Task.WhenAll(outTask, errTask);
//On wine, we dont wanna use waitforexit as it will be always broken and giving an error. var finished = Task.WhenAny(bothTasks, Task.Delay(timeoutMs, token)).GetAwaiter().GetResult();
if (_dalamudUtilService.IsWine)
{
var finished = Task.WhenAny(bothTasks, Task.Delay(timeoutMs, token)).GetAwaiter().GetResult();
if (finished != bothTasks)
{
try
{
proc.Kill(entireProcessTree: true);
Task.WaitAll([outTask, errTask], 1000, token);
}
catch
{
// ignore this
}
var so = outTask.IsCompleted ? outTask.Result : "";
var se = errTask.IsCompleted ? errTask.Result : "timeout";
return (false, so, se);
}
var stderr = errTask.Result; if (token.IsCancellationRequested)
var ok = string.IsNullOrWhiteSpace(stderr); return KillProcess(proc, outTask, errTask, token);
return (ok, outTask.Result, stderr);
if (finished != bothTasks)
return KillProcess(proc, outTask, errTask, token);
bool isWine = _dalamudUtilService?.IsWine ?? false;
if (!isWine)
{
try { proc.WaitForExit(); } catch { /* ignore quirks */ }
}
else
{
var sw = Stopwatch.StartNew();
while (!proc.HasExited && sw.ElapsedMilliseconds < 75)
Thread.Sleep(5);
} }
// On linux, we can use it as we please var stdout = outTask.Status == TaskStatus.RanToCompletion ? outTask.Result : "";
if (!proc.WaitForExit(timeoutMs)) var stderr = errTask.Status == TaskStatus.RanToCompletion ? errTask.Result : "";
{
try
{
proc.Kill(entireProcessTree: true);
Task.WaitAll([outTask, errTask], 1000, token);
}
catch
{
// ignore this
}
return (false, outTask.IsCompleted ? outTask.Result : "", "timeout");
}
Task.WaitAll(outTask, errTask); int code = -1;
return (true, outTask.Result, errTask.Result); try { if (proc.HasExited) code = proc.ExitCode; } catch { /* Wine may still throw */ }
bool ok = code == 0 || (isWine && string.IsNullOrWhiteSpace(stderr));
return (ok, stdout, stderr);
static (bool success, string output, string errorCode) KillProcess(
Process proc, Task<string> outTask, Task<string> errTask, CancellationToken token)
{
try { proc.Kill(entireProcessTree: true); } catch { /* ignore */ }
try { Task.WaitAll([outTask, errTask], 1000, token); } catch { /* ignore */ }
var so = outTask.IsCompleted ? outTask.Result : "";
var se = errTask.IsCompleted ? errTask.Result : "canceled/timeout";
return (false, so, se);
}
} }
/// <summary> /// <summary>
@@ -967,10 +1052,10 @@ public sealed class FileCompactor : IDisposable
} }
/// <summary> /// <summary>
/// Process the queue with, meant for a worker/thread /// Process the queue, meant for a worker/thread
/// </summary> /// </summary>
/// <param name="token">Cancellation token for the worker whenever it needs to be stopped</param> /// <param name="token">Cancellation token for the worker whenever it needs to be stopped</param>
private async Task ProcessQueueWorkerAsync(CancellationToken token) private async Task ProcessQueueWorkerAsync(int workerId, CancellationToken token)
{ {
try try
{ {
@@ -986,7 +1071,7 @@ public sealed class FileCompactor : IDisposable
try try
{ {
if (_lightlessConfigService.Current.UseCompactor && File.Exists(filePath)) if (_lightlessConfigService.Current.UseCompactor && File.Exists(filePath))
CompactFile(filePath); CompactFile(filePath, workerId);
} }
finally finally
{ {
@@ -1005,8 +1090,8 @@ public sealed class FileCompactor : IDisposable
} }
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// Shutting down worker, this exception is expected // Shutting down worker, this exception is expected
} }
} }
@@ -1018,7 +1103,7 @@ public sealed class FileCompactor : IDisposable
/// <returns>Linux path to be used in Linux</returns> /// <returns>Linux path to be used in Linux</returns>
private string ResolveLinuxPathForWine(string windowsPath) private string ResolveLinuxPathForWine(string windowsPath)
{ {
var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(windowsPath)}", null, 5000); var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(windowsPath)}", workingDir: null, 5000);
if (ok && !string.IsNullOrWhiteSpace(outp)) return outp.Trim(); if (ok && !string.IsNullOrWhiteSpace(outp)) return outp.Trim();
return ToLinuxPathIfWine(windowsPath, isWine: true); return ToLinuxPathIfWine(windowsPath, isWine: true);
} }
@@ -1071,7 +1156,11 @@ public sealed class FileCompactor : IDisposable
} }
return true; return true;
} }
catch { return false; } catch (Exception ex)
{
_logger.LogTrace(ex, "Probe open failed for {file} (linux={linux})", winePath, linuxPath);
return false;
}
} }
/// <summary> /// <summary>
@@ -1096,17 +1185,18 @@ public sealed class FileCompactor : IDisposable
} }
[DllImport("kernel32.dll", SetLastError = true)] [LibraryImport("kernel32.dll", SetLastError = true)]
private static extern bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode, IntPtr lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize, out uint lpBytesReturned, IntPtr lpOverlapped); private static partial uint GetCompressedFileSizeW([MarshalAs(UnmanagedType.LPWStr)] string lpFileName, out uint lpFileSizeHigh);
[DllImport("kernel32.dll")] [LibraryImport("kernel32.dll", SetLastError = true)]
private static extern uint GetCompressedFileSizeW([In, MarshalAs(UnmanagedType.LPWStr)] string lpFileName, [Out, MarshalAs(UnmanagedType.U4)] out uint lpFileSizeHigh); [return: MarshalAs(UnmanagedType.Bool)]
private static partial bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode, IntPtr lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize, out uint lpBytesReturned, IntPtr lpOverlapped);
[DllImport("WofUtil.dll")] [LibraryImport("WofUtil.dll")]
private static extern int WofIsExternalFile([MarshalAs(UnmanagedType.LPWStr)] string Filepath, out int IsExternalFile, out uint Provider, out WOF_FILE_COMPRESSION_INFO_V1 Info, ref uint BufferLength); private static partial int WofIsExternalFile([MarshalAs(UnmanagedType.LPWStr)] string Filepath, out int IsExternalFile, out uint Provider, out WofFileCompressionInfoV1 Info, ref uint BufferLength);
[DllImport("WofUtil.dll", SetLastError = true)] [LibraryImport("WofUtil.dll")]
private static extern int WofSetFileDataLocation(SafeFileHandle FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length); private static partial int WofSetFileDataLocation(SafeFileHandle FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length);
private static string QuoteSingle(string s) => "'" + s.Replace("'", "'\\''", StringComparison.Ordinal) + "'"; private static string QuoteSingle(string s) => "'" + s.Replace("'", "'\\''", StringComparison.Ordinal) + "'";
@@ -1114,7 +1204,11 @@ public sealed class FileCompactor : IDisposable
public void Dispose() public void Dispose()
{ {
//Cleanup of gates and frag service
_fragBatch?.Dispose(); _fragBatch?.Dispose();
_btrfsGate?.Dispose();
_globalGate?.Dispose();
_compactionQueue.Writer.TryComplete(); _compactionQueue.Writer.TryComplete();
_compactionCts.Cancel(); _compactionCts.Cancel();
@@ -1122,8 +1216,8 @@ public sealed class FileCompactor : IDisposable
{ {
Task.WaitAll([.. _workers.Where(t => t != null)], TimeSpan.FromSeconds(5)); Task.WaitAll([.. _workers.Where(t => t != null)], TimeSpan.FromSeconds(5));
} }
catch catch
{ {
// Ignore this catch on the dispose // Ignore this catch on the dispose
} }
finally finally

View File

@@ -5,4 +5,5 @@ public enum FileState
Valid, Valid,
RequireUpdate, RequireUpdate,
RequireDeletion, RequireDeletion,
RequireRehash
} }

View File

@@ -20,7 +20,10 @@ internal sealed class DalamudLogger : ILogger
_hasModifiedGameFiles = hasModifiedGameFiles; _hasModifiedGameFiles = hasModifiedGameFiles;
} }
public IDisposable BeginScope<TState>(TState state) => default!; IDisposable? ILogger.BeginScope<TState>(TState state)
{
return default!;
}
public bool IsEnabled(LogLevel logLevel) public bool IsEnabled(LogLevel logLevel)
{ {

View File

@@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0-windows</TargetFramework> <TargetFramework>net9.0-windows7.0</TargetFramework>
<Platforms>x64</Platforms> <Platforms>x64</Platforms>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
@@ -27,6 +27,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Blake3" Version="2.0.0" />
<PackageReference Include="Downloader" Version="4.0.3" /> <PackageReference Include="Downloader" Version="4.0.3" />
<PackageReference Include="K4os.Compression.LZ4.Legacy" Version="1.3.8" /> <PackageReference Include="K4os.Compression.LZ4.Legacy" Version="1.3.8" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.212"> <PackageReference Include="Meziantou.Analyzer" Version="2.0.212">

View File

@@ -288,7 +288,7 @@ public sealed class Plugin : IDalamudPlugin
clientState, clientState,
sp.GetRequiredService<LightlessMediator>())); sp.GetRequiredService<LightlessMediator>()));
collection.AddSingleton<HubFactory>(); collection.AddSingleton<HubFactory>();
collection.AddSingleton(s => new BroadcastScannerService( s.GetRequiredService<ILogger<BroadcastScannerService>>(), framework, s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<NameplateHandler>(), s.GetRequiredService<ActorObjectService>())); collection.AddSingleton(s => new BroadcastScannerService(s.GetRequiredService<ILogger<BroadcastScannerService>>(), framework, s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<NameplateHandler>(), s.GetRequiredService<ActorObjectService>()));
// add scoped services // add scoped services
@@ -342,7 +342,7 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<LightlessMediator>())); s.GetRequiredService<LightlessMediator>()));
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), clientState, gameGui, objectTable, gameInteropProvider, collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), clientState, gameGui, objectTable, gameInteropProvider,
s.GetRequiredService<LightlessMediator>(),s.GetRequiredService<PairUiService>())); s.GetRequiredService<LightlessMediator>(),s.GetRequiredService<PairUiService>()));
collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService<ILogger<NameplateHandler>>(), addonLifecycle, gameGui, s.GetRequiredService<DalamudUtilService>(), collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService<ILogger<NameplateHandler>>(), addonLifecycle, gameGui,
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), clientState, s.GetRequiredService<PairUiService>())); s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), clientState, s.GetRequiredService<PairUiService>()));
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>()); collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());

View File

@@ -1,6 +1,5 @@
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.ActorTracking; using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -8,7 +7,7 @@ using System.Collections.Concurrent;
namespace LightlessSync.Services; namespace LightlessSync.Services;
public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDisposable public class BroadcastScannerService : DisposableMediatorSubscriberBase
{ {
private readonly ILogger<BroadcastScannerService> _logger; private readonly ILogger<BroadcastScannerService> _logger;
private readonly ActorObjectService _actorTracker; private readonly ActorObjectService _actorTracker;
@@ -17,22 +16,21 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
private readonly BroadcastService _broadcastService; private readonly BroadcastService _broadcastService;
private readonly NameplateHandler _nameplateHandler; private readonly NameplateHandler _nameplateHandler;
private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new(); private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new(StringComparer.Ordinal);
private readonly Queue<string> _lookupQueue = new(); private readonly Queue<string> _lookupQueue = new();
private readonly HashSet<string> _lookupQueuedCids = new(); private readonly HashSet<string> _lookupQueuedCids = [];
private readonly HashSet<string> _syncshellCids = new(); private readonly HashSet<string> _syncshellCids = [];
private static readonly TimeSpan MaxAllowedTtl = TimeSpan.FromMinutes(4); private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4);
private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1); private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1);
private readonly CancellationTokenSource _cleanupCts = new(); private readonly CancellationTokenSource _cleanupCts = new();
private Task? _cleanupTask; private readonly Task? _cleanupTask;
private readonly int _checkEveryFrames = 20; private readonly int _checkEveryFrames = 20;
private int _frameCounter = 0; private int _frameCounter = 0;
private int _lookupsThisFrame = 0; private const int _maxLookupsPerFrame = 30;
private const int MaxLookupsPerFrame = 30; private const int _maxQueueSize = 100;
private const int MaxQueueSize = 100;
private volatile bool _batchRunning = false; private volatile bool _batchRunning = false;
@@ -59,6 +57,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
_cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop); _cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop);
_nameplateHandler.Init(); _nameplateHandler.Init();
_actorTracker = actorTracker;
} }
private void OnFrameworkUpdate(IFramework framework) => Update(); private void OnFrameworkUpdate(IFramework framework) => Update();
@@ -66,7 +65,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
public void Update() public void Update()
{ {
_frameCounter++; _frameCounter++;
_lookupsThisFrame = 0; var lookupsThisFrame = 0;
if (!_broadcastService.IsBroadcasting) if (!_broadcastService.IsBroadcasting)
return; return;
@@ -81,19 +80,19 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address); var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address);
var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now; var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now;
if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < MaxQueueSize) if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < _maxQueueSize)
_lookupQueue.Enqueue(cid); _lookupQueue.Enqueue(cid);
} }
if (_frameCounter % _checkEveryFrames == 0 && _lookupQueue.Count > 0) if (_frameCounter % _checkEveryFrames == 0 && _lookupQueue.Count > 0)
{ {
var cidsToLookup = new List<string>(); var cidsToLookup = new List<string>();
while (_lookupQueue.Count > 0 && _lookupsThisFrame < MaxLookupsPerFrame) while (_lookupQueue.Count > 0 && lookupsThisFrame < _maxLookupsPerFrame)
{ {
var cid = _lookupQueue.Dequeue(); var cid = _lookupQueue.Dequeue();
_lookupQueuedCids.Remove(cid); _lookupQueuedCids.Remove(cid);
cidsToLookup.Add(cid); cidsToLookup.Add(cid);
_lookupsThisFrame++; lookupsThisFrame++;
} }
if (cidsToLookup.Count > 0 && !_batchRunning) if (cidsToLookup.Count > 0 && !_batchRunning)
@@ -115,8 +114,8 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
continue; continue;
var ttl = info.IsBroadcasting && info.TTL.HasValue var ttl = info.IsBroadcasting && info.TTL.HasValue
? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, MaxAllowedTtl.Ticks)) ? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, _maxAllowedTtl.Ticks))
: RetryDelay; : _retryDelay;
var expiry = now + ttl; var expiry = now + ttl;
@@ -153,7 +152,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
var newSet = _broadcastCache var newSet = _broadcastCache
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID)) .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
.Select(e => e.Key) .Select(e => e.Key)
.ToHashSet(); .ToHashSet(StringComparer.Ordinal);
if (!_syncshellCids.SetEquals(newSet)) if (!_syncshellCids.SetEquals(newSet))
{ {
@@ -169,7 +168,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
return _broadcastCache return [.. _broadcastCache
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID)) .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
.Select(e => new BroadcastStatusInfoDto .Select(e => new BroadcastStatusInfoDto
{ {
@@ -177,8 +176,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
IsBroadcasting = true, IsBroadcasting = true,
TTL = e.Value.ExpiryTime - now, TTL = e.Value.ExpiryTime - now,
GID = e.Value.GID GID = e.Value.GID
}) })];
.ToList();
} }
private async Task ExpiredBroadcastCleanupLoop() private async Task ExpiredBroadcastCleanupLoop()
@@ -189,7 +187,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
{ {
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)
{ {
await Task.Delay(TimeSpan.FromSeconds(10), token); await Task.Delay(TimeSpan.FromSeconds(10), token).ConfigureAwait(false);
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
foreach (var (cid, entry) in _broadcastCache.ToArray()) foreach (var (cid, entry) in _broadcastCache.ToArray())
@@ -199,7 +197,10 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
} }
} }
} }
catch (OperationCanceledException) { } catch (OperationCanceledException)
{
// No action needed when cancelled
}
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Broadcast cleanup loop crashed"); _logger.LogError(ex, "Broadcast cleanup loop crashed");
@@ -232,7 +233,14 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
{ {
base.Dispose(disposing); base.Dispose(disposing);
_framework.Update -= OnFrameworkUpdate; _framework.Update -= OnFrameworkUpdate;
if (_cleanupTask != null)
{
_cleanupTask?.Wait(100, _cleanupCts.Token);
}
_cleanupCts.Cancel(); _cleanupCts.Cancel();
_cleanupCts.Dispose();
_cleanupTask?.Wait(100); _cleanupTask?.Wait(100);
_cleanupCts.Dispose(); _cleanupCts.Dispose();
_nameplateHandler.Uninit(); _nameplateHandler.Uninit();

View File

@@ -11,7 +11,6 @@ using LightlessSync.WebAPI;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Threading;
namespace LightlessSync.Services; namespace LightlessSync.Services;
public class BroadcastService : IHostedService, IMediatorSubscriber public class BroadcastService : IHostedService, IMediatorSubscriber
@@ -58,7 +57,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
{ {
if (!_apiController.IsConnected) if (!_apiController.IsConnected)
{ {
_logger.LogDebug(context + " skipped, not connected"); _logger.LogDebug("{context} skipped, not connected", context);
return; return;
} }
await action().ConfigureAwait(false); await action().ConfigureAwait(false);
@@ -372,7 +371,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
public async Task<Dictionary<string, BroadcastStatusInfoDto?>> AreUsersBroadcastingAsync(List<string> hashedCids) public async Task<Dictionary<string, BroadcastStatusInfoDto?>> AreUsersBroadcastingAsync(List<string> hashedCids)
{ {
Dictionary<string, BroadcastStatusInfoDto?> result = new(); Dictionary<string, BroadcastStatusInfoDto?> result = new(StringComparer.Ordinal);
await RequireConnectionAsync(nameof(AreUsersBroadcastingAsync), async () => await RequireConnectionAsync(nameof(AreUsersBroadcastingAsync), async () =>
{ {
@@ -397,8 +396,6 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
return result; return result;
} }
public async void ToggleBroadcast() public async void ToggleBroadcast()
{ {

View File

@@ -0,0 +1,19 @@
using LightlessSync.API.Data.Enum;
using LightlessSync.Services.CharaData.Models;
using System.Collections.Immutable;
namespace LightlessSync.Services.CharaData;
public sealed class CharacterAnalysisSummary
{
public static CharacterAnalysisSummary Empty { get; } =
new(ImmutableDictionary<ObjectKind, CharacterAnalysisObjectSummary>.Empty);
internal CharacterAnalysisSummary(IImmutableDictionary<ObjectKind, CharacterAnalysisObjectSummary> objects)
{
Objects = objects;
}
public IImmutableDictionary<ObjectKind, CharacterAnalysisObjectSummary> Objects { get; }
public bool HasData => Objects.Any(kvp => kvp.Value.HasEntries);
}

View File

@@ -0,0 +1,8 @@
using System.Runtime.InteropServices;
namespace LightlessSync.Services.CharaData.Models;
[StructLayout(LayoutKind.Auto)]
public readonly record struct CharacterAnalysisObjectSummary(int EntryCount, long TotalTriangles, long TexOriginalBytes, long TexCompressedBytes)
{
public bool HasEntries => EntryCount > 0;
}

View File

@@ -1,16 +1,14 @@
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.Services.CharaData;
using LightlessSync.Services.CharaData.Models;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.UI; using LightlessSync.UI;
using LightlessSync.Utils; using LightlessSync.Utils;
using Lumina.Data.Files; using Lumina.Data.Files;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace LightlessSync.Services; namespace LightlessSync.Services;
public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
@@ -51,31 +49,47 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
_analysisCts = _analysisCts?.CancelRecreate() ?? new(); _analysisCts = _analysisCts?.CancelRecreate() ?? new();
var cancelToken = _analysisCts.Token; var cancelToken = _analysisCts.Token;
var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList(); var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList();
if (allFiles.Exists(c => !c.IsComputed || recalculate))
var remaining = allFiles.Where(c => !c.IsComputed || recalculate).ToList();
if (remaining.Count == 0)
return;
TotalFiles = remaining.Count;
CurrentFile = 0;
Logger.LogDebug("=== Computing {amount} remaining files ===", remaining.Count);
Mediator.Publish(new HaltScanMessage(nameof(CharacterAnalyzer)));
try
{ {
var remaining = allFiles.Where(c => !c.IsComputed || recalculate).ToList(); foreach (var file in remaining)
TotalFiles = remaining.Count;
CurrentFile = 1;
Logger.LogDebug("=== Computing {amount} remaining files ===", remaining.Count);
Mediator.Publish(new HaltScanMessage(nameof(CharacterAnalyzer)));
try
{ {
foreach (var file in remaining) cancelToken.ThrowIfCancellationRequested();
{
Logger.LogDebug("Computing file {file}", file.FilePaths[0]); var path = file.FilePaths.FirstOrDefault() ?? "<unknown>";
await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false); Logger.LogDebug("Computing file {file}", path);
CurrentFile++;
} await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false);
_fileCacheManager.WriteOutFullCsv();
} CurrentFile++;
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to analyze files");
}
finally
{
Mediator.Publish(new ResumeScanMessage(nameof(CharacterAnalyzer)));
} }
await _fileCacheManager.WriteOutFullCsvAsync(cancelToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
Logger.LogInformation("File analysis cancelled");
throw;
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to analyze files");
}
finally
{
Mediator.Publish(new ResumeScanMessage(nameof(CharacterAnalyzer)));
} }
RecalculateSummary(); RecalculateSummary();
@@ -87,6 +101,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
public void Dispose() public void Dispose()
{ {
_analysisCts.CancelDispose(); _analysisCts.CancelDispose();
_baseAnalysisCts.Dispose();
} }
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token) public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token)
{ {
@@ -120,7 +135,8 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
foreach (var fileEntry in obj.Value) foreach (var fileEntry in obj.Value)
{ {
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
var fileCacheEntries = _fileCacheManager.GetAllFileCachesByHash(fileEntry.Hash, ignoreCacheEntries: true, validate: false).ToList();
var fileCacheEntries = (await _fileCacheManager.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false)).ToList();
if (fileCacheEntries.Count == 0) continue; if (fileCacheEntries.Count == 0) continue;
var filePath = fileCacheEntries[0].ResolvedFilepath; var filePath = fileCacheEntries[0].ResolvedFilepath;
FileInfo fi = new(filePath); FileInfo fi = new(filePath);
@@ -138,7 +154,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
{ {
data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext, data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext,
[.. fileEntry.GamePaths], [.. fileEntry.GamePaths],
fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct().ToList(), [.. fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct(StringComparer.Ordinal)],
entry.Size > 0 ? entry.Size.Value : 0, entry.Size > 0 ? entry.Size.Value : 0,
entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0, entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0,
tris); tris);
@@ -226,7 +242,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
{ {
var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false); var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false);
var normalSize = new FileInfo(FilePaths[0]).Length; var normalSize = new FileInfo(FilePaths[0]).Length;
var entries = fileCacheManager.GetAllFileCachesByHash(Hash, ignoreCacheEntries: true, validate: false); var entries = await fileCacheManager.GetAllFileCachesByHashAsync(Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false);
foreach (var entry in entries) foreach (var entry in entries)
{ {
entry.Size = normalSize; entry.Size = normalSize;
@@ -263,23 +279,3 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
}); });
} }
} }
public readonly record struct CharacterAnalysisObjectSummary(int EntryCount, long TotalTriangles, long TexOriginalBytes, long TexCompressedBytes)
{
public bool HasEntries => EntryCount > 0;
}
public sealed class CharacterAnalysisSummary
{
public static CharacterAnalysisSummary Empty { get; } =
new(ImmutableDictionary<ObjectKind, CharacterAnalysisObjectSummary>.Empty);
internal CharacterAnalysisSummary(IImmutableDictionary<ObjectKind, CharacterAnalysisObjectSummary> objects)
{
Objects = objects;
}
public IImmutableDictionary<ObjectKind, CharacterAnalysisObjectSummary> Objects { get; }
public bool HasData => Objects.Any(kvp => kvp.Value.HasEntries);
}

View File

@@ -92,13 +92,13 @@ namespace LightlessSync.Services.Compactor
} }
if ((flushAt - DateTime.UtcNow) <= TimeSpan.Zero) break; if ((flushAt - DateTime.UtcNow) <= TimeSpan.Zero) break;
try try
{ {
await Task.Delay(TimeSpan.FromMilliseconds(5), _cts.Token).ConfigureAwait(false); await Task.Delay(TimeSpan.FromMilliseconds(5), _cts.Token).ConfigureAwait(false);
} }
catch catch
{ {
break; break;
} }
} }
@@ -124,8 +124,8 @@ namespace LightlessSync.Services.Compactor
} }
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
//Shutting down worker, exception called //Shutting down worker, exception called
} }
} }
@@ -145,17 +145,13 @@ namespace LightlessSync.Services.Compactor
if (_useShell) if (_useShell)
{ {
var inner = "filefrag -v " + string.Join(' ', list.Select(QuoteSingle)); var inner = "filefrag -v -- " + string.Join(' ', list.Select(QuoteSingle));
res = _runShell(inner, timeoutMs: 15000, workingDir: "/"); res = _runShell(inner, timeoutMs: 15000, workingDir: "/");
} }
else else
{ {
var args = new List<string> { "-v" }; var args = new List<string> { "-v", "--" };
foreach (var path in list) args.AddRange(list);
{
args.Add(' ' + path);
}
res = _runDirect("filefrag", args, workingDir: "/", timeoutMs: 15000); res = _runDirect("filefrag", args, workingDir: "/", timeoutMs: 15000);
} }
@@ -200,7 +196,7 @@ namespace LightlessSync.Services.Compactor
/// Regex of the File Size return on the Linux/Wine systems, giving back the amount /// Regex of the File Size return on the Linux/Wine systems, giving back the amount
/// </summary> /// </summary>
/// <returns>Regex of the File Size</returns> /// <returns>Regex of the File Size</returns>
[GeneratedRegex(@"^File size of (/.+?) is ", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant,matchTimeoutMilliseconds: 500)] [GeneratedRegex(@"^File size of (/.+?) is ", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 500)]
private static partial Regex SizeRegex(); private static partial Regex SizeRegex();
/// <summary> /// <summary>

View File

@@ -106,7 +106,7 @@ internal class ContextMenuService : IHostedService
return; return;
IPlayerCharacter? targetData = GetPlayerFromObjectTable(target); IPlayerCharacter? targetData = GetPlayerFromObjectTable(target);
if (targetData == null || targetData.Address == nint.Zero) if (targetData == null || targetData.Address == nint.Zero || _clientState.LocalPlayer == null)
return; return;
//Check if user is directly paired or is own. //Check if user is directly paired or is own.
@@ -161,7 +161,7 @@ internal class ContextMenuService : IHostedService
PrefixChar = 'L', PrefixChar = 'L',
UseDefaultPrefix = false, UseDefaultPrefix = false,
PrefixColor = 708, PrefixColor = 708,
OnClicked = async _ => await HandleSelection(args).ConfigureAwait(false) OnClicked = _ => HandleSelection(args).ConfigureAwait(false).GetAwaiter().GetResult()
}); });
} }
@@ -190,7 +190,7 @@ internal class ContextMenuService : IHostedService
return; return;
} }
var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetBlake3Hash();
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address); var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
@@ -286,8 +286,6 @@ internal class ContextMenuService : IHostedService
private static bool IsChineseJapaneseKoreanCharacter(char c) => c >= 0x4E00 && c <= 0x9FFF; private static bool IsChineseJapaneseKoreanCharacter(char c) => c >= 0x4E00 && c <= 0x9FFF;
public bool IsWorldValid(uint worldId) => IsWorldValid(GetWorld(worldId));
public static bool IsWorldValid(World world) public static bool IsWorldValid(World world)
{ {
var name = world.Name.ToString(); var name = world.Name.ToString();

View File

@@ -531,15 +531,11 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
{ {
logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler); logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler);
curWaitTime += tick; curWaitTime += tick;
await Task.Delay(tick).ConfigureAwait(true); await Task.Delay(tick, ct.Value).ConfigureAwait(true);
} }
logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime); logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime);
} }
catch (NullReferenceException ex)
{
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
}
catch (AccessViolationException ex) catch (AccessViolationException ex)
{ {
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler); logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
@@ -707,76 +703,75 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
_lastGlobalBlockReason = string.Empty; _lastGlobalBlockReason = string.Empty;
} }
if (_clientState.IsGPosing && !IsInGpose) // Checks on conditions
{ var shouldBeInGpose = _clientState.IsGPosing;
_logger.LogDebug("Gpose start"); var shouldBeInCombat = _condition[ConditionFlag.InCombat] && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat;
IsInGpose = true; var shouldBePerforming = _condition[ConditionFlag.Performing] && _playerPerformanceConfigService.Current.PauseWhilePerforming;
Mediator.Publish(new GposeStartMessage()); var shouldBeInInstance = _condition[ConditionFlag.BoundByDuty] && _playerPerformanceConfigService.Current.PauseInInstanceDuty;
} var shouldBeInCutscene = _condition[ConditionFlag.WatchingCutscene];
else if (!_clientState.IsGPosing && IsInGpose)
{
_logger.LogDebug("Gpose end");
IsInGpose = false;
Mediator.Publish(new GposeEndMessage());
}
if ((_condition[ConditionFlag.InCombat]) && !IsInCombat && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat) // Gpose
{ HandleStateTransition(() => IsInGpose, v => IsInGpose = v, shouldBeInGpose, "Gpose",
_logger.LogDebug("Combat start"); onEnter: () =>
IsInCombat = true; {
Mediator.Publish(new CombatStartMessage()); Mediator.Publish(new GposeStartMessage());
Mediator.Publish(new HaltScanMessage(nameof(IsInCombat))); },
} onExit: () =>
else if ((!_condition[ConditionFlag.InCombat]) && IsInCombat && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat) {
{ Mediator.Publish(new GposeEndMessage());
_logger.LogDebug("Combat end"); });
IsInCombat = false;
Mediator.Publish(new CombatEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(IsInCombat)));
}
if (_condition[ConditionFlag.Performing] && !IsPerforming && _playerPerformanceConfigService.Current.PauseWhilePerforming)
{
_logger.LogDebug("Performance start");
IsInCombat = true;
Mediator.Publish(new PerformanceStartMessage());
Mediator.Publish(new HaltScanMessage(nameof(IsPerforming)));
}
else if (!_condition[ConditionFlag.Performing] && IsPerforming && _playerPerformanceConfigService.Current.PauseWhilePerforming)
{
_logger.LogDebug("Performance end");
IsInCombat = false;
Mediator.Publish(new PerformanceEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(IsPerforming)));
}
if ((_condition[ConditionFlag.BoundByDuty]) && !IsInInstance && _playerPerformanceConfigService.Current.PauseInInstanceDuty)
{
_logger.LogDebug("Instance start");
IsInInstance = true;
Mediator.Publish(new InstanceOrDutyStartMessage());
Mediator.Publish(new HaltScanMessage(nameof(IsInInstance)));
}
else if (((!_condition[ConditionFlag.BoundByDuty]) && IsInInstance && _playerPerformanceConfigService.Current.PauseInInstanceDuty) || ((_condition[ConditionFlag.BoundByDuty]) && IsInInstance && !_playerPerformanceConfigService.Current.PauseInInstanceDuty))
{
_logger.LogDebug("Instance end");
IsInInstance = false;
Mediator.Publish(new InstanceOrDutyEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(IsInInstance)));
}
if (_condition[ConditionFlag.WatchingCutscene] && !IsInCutscene) // Combat
{ HandleStateTransition(() => IsInCombat, v => IsInCombat = v, shouldBeInCombat, "Combat",
_logger.LogDebug("Cutscene start"); onEnter: () =>
IsInCutscene = true; {
Mediator.Publish(new CutsceneStartMessage()); Mediator.Publish(new CombatStartMessage());
Mediator.Publish(new HaltScanMessage(nameof(IsInCutscene))); Mediator.Publish(new HaltScanMessage(nameof(IsInCombat)));
} },
else if (!_condition[ConditionFlag.WatchingCutscene] && IsInCutscene) onExit: () =>
{ {
_logger.LogDebug("Cutscene end"); Mediator.Publish(new CombatEndMessage());
IsInCutscene = false; Mediator.Publish(new ResumeScanMessage(nameof(IsInCombat)));
Mediator.Publish(new CutsceneEndMessage()); });
Mediator.Publish(new ResumeScanMessage(nameof(IsInCutscene)));
} // Performance
HandleStateTransition(() => IsPerforming, v => IsPerforming = v, shouldBePerforming, "Performance",
onEnter: () =>
{
Mediator.Publish(new PerformanceStartMessage());
Mediator.Publish(new HaltScanMessage(nameof(IsPerforming)));
},
onExit: () =>
{
Mediator.Publish(new PerformanceEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(IsPerforming)));
});
// Instance / Duty
HandleStateTransition(() => IsInInstance, v => IsInInstance = v, shouldBeInInstance, "Instance",
onEnter: () =>
{
Mediator.Publish(new InstanceOrDutyStartMessage());
Mediator.Publish(new HaltScanMessage(nameof(IsInInstance)));
},
onExit: () =>
{
Mediator.Publish(new InstanceOrDutyEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(IsInInstance)));
});
// Cutscene
HandleStateTransition(() => IsInCutscene,v => IsInCutscene = v, shouldBeInCutscene, "Cutscene",
onEnter: () =>
{
Mediator.Publish(new CutsceneStartMessage());
Mediator.Publish(new HaltScanMessage(nameof(IsInCutscene)));
},
onExit: () =>
{
Mediator.Publish(new CutsceneEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(IsInCutscene)));
});
if (IsInCutscene) if (IsInCutscene)
{ {
@@ -867,4 +862,31 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
_delayedFrameworkUpdateCheck = DateTime.UtcNow; _delayedFrameworkUpdateCheck = DateTime.UtcNow;
}); });
} }
/// <summary>
/// Handler for the transition of different states of game
/// </summary>
/// <param name="getState">Get state of condition</param>
/// <param name="setState">Set state of condition</param>
/// <param name="shouldBeActive">Correction of the state of the condition</param>
/// <param name="stateName">Condition name</param>
/// <param name="onEnter">Function for on entering the state</param>
/// <param name="onExit">Function for on leaving the state</param>
private void HandleStateTransition(Func<bool> getState, Action<bool> setState, bool shouldBeActive, string stateName, System.Action onEnter, System.Action onExit)
{
var isActive = getState();
if (shouldBeActive && !isActive)
{
_logger.LogDebug("{stateName} start", stateName);
setState(true);
onEnter();
}
else if (!shouldBeActive && isActive)
{
_logger.LogDebug("{stateName} end", stateName);
setState(false);
onExit();
}
}
} }

View File

@@ -3,6 +3,7 @@ using LightlessSync.API.Data.Comparer;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.Profiles;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Serilog.Core; using Serilog.Core;

View File

@@ -123,6 +123,8 @@ public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData)
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase; public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase; public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase; public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
public record UserLeftSyncshell(string gid) : MessageBase;
public record UserJoinedSyncshell(string gid) : MessageBase;
public record SyncshellBroadcastsUpdatedMessage : MessageBase; public record SyncshellBroadcastsUpdatedMessage : MessageBase;
public record PairRequestReceivedMessage(string HashedCid, string Message) : MessageBase; public record PairRequestReceivedMessage(string HashedCid, string Message) : MessageBase;
public record PairRequestsUpdatedMessage : MessageBase; public record PairRequestsUpdatedMessage : MessageBase;

View File

@@ -16,7 +16,6 @@ using LightlessSync.UtilsEnum.Enum;
// Created using https://github.com/PunishedPineapple/Distance as a reference, thank you! // Created using https://github.com/PunishedPineapple/Distance as a reference, thank you!
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Globalization; using System.Globalization;
@@ -28,7 +27,6 @@ public unsafe class NameplateHandler : IMediatorSubscriber
private readonly IAddonLifecycle _addonLifecycle; private readonly IAddonLifecycle _addonLifecycle;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly DalamudUtilService _dalamudUtil;
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly PairUiService _pairUiService; private readonly PairUiService _pairUiService;
private readonly LightlessMediator _mediator; private readonly LightlessMediator _mediator;
@@ -46,17 +44,15 @@ public unsafe class NameplateHandler : IMediatorSubscriber
internal const uint mNameplateNodeIDBase = 0x7D99D500; internal const uint mNameplateNodeIDBase = 0x7D99D500;
private const string DefaultLabelText = "LightFinder"; private const string DefaultLabelText = "LightFinder";
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn; private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
private const int _containerOffsetX = 50;
private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon); private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
private ImmutableHashSet<string> _activeBroadcastingCids = []; private ImmutableHashSet<string> _activeBroadcastingCids = [];
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairUiService pairUiService) public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairUiService pairUiService)
{ {
_logger = logger; _logger = logger;
_addonLifecycle = addonLifecycle; _addonLifecycle = addonLifecycle;
_gameGui = gameGui; _gameGui = gameGui;
_dalamudUtil = dalamudUtil;
_configService = configService; _configService = configService;
_mediator = mediator; _mediator = mediator;
_clientState = clientState; _clientState = clientState;
@@ -118,7 +114,8 @@ public unsafe class NameplateHandler : IMediatorSubscriber
{ {
if (args.Addon.Address == nint.Zero) if (args.Addon.Address == nint.Zero)
{ {
_logger.LogWarning("Nameplate draw detour received a null addon address, skipping update."); if (_logger.IsEnabled(LogLevel.Warning))
_logger.LogWarning("Nameplate draw detour received a null addon address, skipping update.");
return; return;
} }
@@ -177,7 +174,8 @@ public unsafe class NameplateHandler : IMediatorSubscriber
var currentHandle = _gameGui.GetAddonByName("NamePlate", 1); var currentHandle = _gameGui.GetAddonByName("NamePlate", 1);
if (currentHandle.Address == nint.Zero) if (currentHandle.Address == nint.Zero)
{ {
_logger.LogWarning("Unable to destroy nameplate nodes because the NamePlate addon is not available."); if (_logger.IsEnabled(LogLevel.Warning))
_logger.LogWarning("Unable to destroy nameplate nodes because the NamePlate addon is not available.");
return; return;
} }
@@ -187,7 +185,8 @@ public unsafe class NameplateHandler : IMediatorSubscriber
if (_mpNameplateAddon != pCurrentNameplateAddon) if (_mpNameplateAddon != pCurrentNameplateAddon)
{ {
_logger.LogWarning("Skipping nameplate node destroy due to addon address mismatch (cached {Cached:X}, current {Current:X}).", (IntPtr)_mpNameplateAddon, (IntPtr)pCurrentNameplateAddon); if (_logger.IsEnabled(LogLevel.Warning))
_logger.LogWarning("Skipping nameplate node destroy due to addon address mismatch (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)pCurrentNameplateAddon);
return; return;
} }
@@ -197,7 +196,8 @@ public unsafe class NameplateHandler : IMediatorSubscriber
var pNameplateNode = GetNameplateComponentNode(i); var pNameplateNode = GetNameplateComponentNode(i);
if (pTextNode != null && (pNameplateNode == null || pNameplateNode->Component == null)) if (pTextNode != null && (pNameplateNode == null || pNameplateNode->Component == null))
{ {
_logger.LogDebug("Skipping destroy for nameplate {Index} because its component node is unavailable.", i); if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("Skipping destroy for nameplate {Index} because its component node is unavailable.", i);
continue; continue;
} }
@@ -210,12 +210,13 @@ public unsafe class NameplateHandler : IMediatorSubscriber
if (pTextNode->AtkResNode.NextSiblingNode != null) if (pTextNode->AtkResNode.NextSiblingNode != null)
pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode; pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode;
pNameplateNode->Component->UldManager.UpdateDrawNodeList(); pNameplateNode->Component->UldManager.UpdateDrawNodeList();
pTextNode->AtkResNode.Destroy(true); pTextNode->AtkResNode.Destroy(free: true);
_mTextNodes[i] = null; _mTextNodes[i] = null;
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError($"Unknown error while removing text node 0x{(IntPtr)pTextNode:X} for nameplate {i} on component node 0x{(IntPtr)pNameplateNode:X}:\n{e}"); if (_logger.IsEnabled(LogLevel.Error))
_logger.LogError("Unknown error while removing text node 0x{textNode} for nameplate {i} on component node 0x{nameplateNode}:\n{e}", (IntPtr)pTextNode, i, (IntPtr)pNameplateNode, e);
} }
} }
} }
@@ -239,36 +240,40 @@ public unsafe class NameplateHandler : IMediatorSubscriber
var currentHandle = _gameGui.GetAddonByName("NamePlate"); var currentHandle = _gameGui.GetAddonByName("NamePlate");
if (currentHandle.Address == nint.Zero) if (currentHandle.Address == nint.Zero)
{ {
_logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh."); if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh.");
return; return;
} }
var currentAddon = (AddonNamePlate*)currentHandle.Address; var currentAddon = (AddonNamePlate*)currentHandle.Address;
if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon) if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon)
{ {
if (_mpNameplateAddon != null) if (_mpNameplateAddon != null && _logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached:X}, current {Current:X}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon); _logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon);
return; return;
} }
var framework = Framework.Instance(); var framework = Framework.Instance();
if (framework == null) if (framework == null)
{ {
_logger.LogDebug("Framework instance unavailable during nameplate update, skipping."); if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("Framework instance unavailable during nameplate update, skipping.");
return; return;
} }
var uiModule = framework->GetUIModule(); var uiModule = framework->GetUIModule();
if (uiModule == null) if (uiModule == null)
{ {
_logger.LogDebug("UI module unavailable during nameplate update, skipping."); if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("UI module unavailable during nameplate update, skipping.");
return; return;
} }
var ui3DModule = uiModule->GetUI3DModule(); var ui3DModule = uiModule->GetUI3DModule();
if (ui3DModule == null) if (ui3DModule == null)
{ {
_logger.LogDebug("UI3D module unavailable during nameplate update, skipping."); if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("UI3D module unavailable during nameplate update, skipping.");
return; return;
} }
@@ -280,7 +285,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
var safeCount = System.Math.Min( var safeCount = System.Math.Min(
ui3DModule->NamePlateObjectInfoCount, ui3DModule->NamePlateObjectInfoCount,
vec.Length vec.Length
); );
for (int i = 0; i < safeCount; ++i) for (int i = 0; i < safeCount; ++i)
@@ -347,7 +352,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
pNode->AtkResNode.ToggleVisibility(enable: false); pNode->AtkResNode.ToggleVisibility(enable: false);
continue; continue;
} }
root->Component->UldManager.UpdateDrawNodeList(); root->Component->UldManager.UpdateDrawNodeList();
bool isVisible = bool isVisible =
@@ -449,10 +454,6 @@ public unsafe class NameplateHandler : IMediatorSubscriber
{ {
_cachedNameplateTextOffsets[nameplateIndex] = textOffset; _cachedNameplateTextOffsets[nameplateIndex] = textOffset;
} }
else if (_cachedNameplateTextOffsets[nameplateIndex] != int.MinValue)
{
textOffset = _cachedNameplateTextOffsets[nameplateIndex];
}
else else
{ {
hasValidOffset = false; hasValidOffset = false;
@@ -534,7 +535,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
pNode->EdgeColor.A = (byte)(edgeColor.W * 255); pNode->EdgeColor.A = (byte)(edgeColor.W * 255);
if(!config.LightfinderLabelUseIcon) if (!config.LightfinderLabelUseIcon)
{ {
pNode->AlignmentType = AlignmentType.Bottom; pNode->AlignmentType = AlignmentType.Bottom;
} }
@@ -642,10 +643,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
{ {
return _mpNameplateAddon->NamePlateObjectArray[i]; return _mpNameplateAddon->NamePlateObjectArray[i];
} }
else return null;
{
return null;
}
} }
private AtkComponentNode* GetNameplateComponentNode(int i) private AtkComponentNode* GetNameplateComponentNode(int i)
@@ -653,12 +651,12 @@ public unsafe class NameplateHandler : IMediatorSubscriber
var nameplateObject = GetNameplateObject(i); var nameplateObject = GetNameplateObject(i);
return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null; return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null;
} }
private HashSet<ulong> VisibleUserIds private HashSet<ulong> VisibleUserIds
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values => [.. _pairUiService.GetSnapshot().PairsByUid.Values
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
.Select(u => (ulong)u.PlayerCharacterId)]; .Select(u => (ulong)u.PlayerCharacterId)];
public void FlagRefresh() public void FlagRefresh()
{ {
_needsLabelRefresh = true; _needsLabelRefresh = true;
@@ -680,7 +678,8 @@ public unsafe class NameplateHandler : IMediatorSubscriber
return; return;
_activeBroadcastingCids = newSet; _activeBroadcastingCids = newSet;
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids)); if (_logger.IsEnabled(LogLevel.Information))
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids));
FlagRefresh(); FlagRefresh();
} }

View File

@@ -70,7 +70,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
{ {
var notification = CreateNotification(title, message, type, duration, actions, soundEffectId); var notification = CreateNotification(title, message, type, duration, actions, soundEffectId);
if (_configService.Current.AutoDismissOnAction && notification.Actions.Any()) if (_configService.Current.AutoDismissOnAction && notification.Actions.Count != 0)
{ {
WrapActionsWithAutoDismiss(notification); WrapActionsWithAutoDismiss(notification);
} }
@@ -115,7 +115,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
} }
} }
private void DismissNotification(LightlessNotification notification) private static void DismissNotification(LightlessNotification notification)
{ {
notification.IsDismissed = true; notification.IsDismissed = true;
notification.IsAnimatingOut = true; notification.IsAnimatingOut = true;
@@ -219,10 +219,12 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
Mediator.Publish(new LightlessNotificationMessage(notification)); Mediator.Publish(new LightlessNotificationMessage(notification));
} }
private string FormatDownloadCompleteMessage(string fileName, int fileCount) => private static string FormatDownloadCompleteMessage(string fileName, int fileCount)
fileCount > 1 {
return fileCount > 1
? $"Downloaded {fileCount} files successfully." ? $"Downloaded {fileCount} files successfully."
: $"Downloaded {fileName} successfully."; : $"Downloaded {fileName} successfully.";
}
private List<LightlessNotificationAction> CreateDownloadCompleteActions(Action? onOpenFolder) private List<LightlessNotificationAction> CreateDownloadCompleteActions(Action? onOpenFolder)
{ {
@@ -268,8 +270,10 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
Mediator.Publish(new LightlessNotificationMessage(notification)); Mediator.Publish(new LightlessNotificationMessage(notification));
} }
private string FormatErrorMessage(string message, Exception? exception) => private static string FormatErrorMessage(string message, Exception? exception)
exception != null ? $"{message}\n\nError: {exception.Message}" : message; {
return exception != null ? $"{message}\n\nError: {exception.Message}" : message;
}
private List<LightlessNotificationAction> CreateErrorActions(Action? onRetry, Action? onViewLog) private List<LightlessNotificationAction> CreateErrorActions(Action? onRetry, Action? onViewLog)
{ {
@@ -343,8 +347,9 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
return string.Join("\n", activeDownloads.Select(x => $"• {x.PlayerName}: {FormatDownloadStatus(x)}")); return string.Join("\n", activeDownloads.Select(x => $"• {x.PlayerName}: {FormatDownloadStatus(x)}"));
} }
private string FormatDownloadStatus((string PlayerName, float Progress, string Status) download) => private static string FormatDownloadStatus((string PlayerName, float Progress, string Status) download)
download.Status switch {
return download.Status switch
{ {
"downloading" => $"{download.Progress:P0}", "downloading" => $"{download.Progress:P0}",
"decompressing" => "decompressing", "decompressing" => "decompressing",
@@ -352,6 +357,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
"waiting" => "waiting for slot", "waiting" => "waiting for slot",
_ => download.Status _ => download.Status
}; };
}
private TimeSpan GetDefaultDurationForType(NotificationType type) => type switch private TimeSpan GetDefaultDurationForType(NotificationType type) => type switch
{ {
@@ -500,13 +506,16 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
}); });
} }
private Dalamud.Interface.ImGuiNotification.NotificationType private static Dalamud.Interface.ImGuiNotification.NotificationType
ConvertToDalamudNotificationType(NotificationType type) => type switch ConvertToDalamudNotificationType(NotificationType type)
{ {
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error, return type switch
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning, {
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
}; NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
};
}
private void ShowChat(NotificationMessage msg) private void ShowChat(NotificationMessage msg)
{ {
@@ -590,7 +599,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _) private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _)
{ {
var activeRequests = _pairRequestService.GetActiveRequests(); var activeRequests = _pairRequestService.GetActiveRequests();
var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet(); var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet(StringComparer.Ordinal);
// Dismiss notifications for requests that are no longer active (expired) // Dismiss notifications for requests that are no longer active (expired)
var notificationsToRemove = _shownPairRequestNotifications var notificationsToRemove = _shownPairRequestNotifications
@@ -607,7 +616,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
private void HandlePairDownloadStatus(PairDownloadStatusMessage msg) private void HandlePairDownloadStatus(PairDownloadStatusMessage msg)
{ {
var userDownloads = msg.DownloadStatus.Where(x => x.PlayerName != "Pair Queue").ToList(); var userDownloads = msg.DownloadStatus.Where(x => !string.Equals(x.PlayerName, "Pair Queue", StringComparison.Ordinal)).ToList();
var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.Progress) : 0f; var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.Progress) : 0f;
var message = BuildPairDownloadMessage(userDownloads, msg.QueueWaiting); var message = BuildPairDownloadMessage(userDownloads, msg.QueueWaiting);
@@ -763,7 +772,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
return actions; return actions;
} }
private string GetUserDisplayName(UserData userData, string playerName) private static string GetUserDisplayName(UserData userData, string playerName)
{ {
if (!string.IsNullOrEmpty(userData.Alias) && !string.Equals(userData.Alias, userData.UID, StringComparison.Ordinal)) if (!string.IsNullOrEmpty(userData.Alias) && !string.Equals(userData.Alias, userData.UID, StringComparison.Ordinal))
{ {

View File

@@ -0,0 +1,9 @@
using System.Runtime.InteropServices;
namespace LightlessSync.Services.PairProcessing;
[StructLayout(LayoutKind.Auto)]
public readonly record struct PairProcessingLimiterSnapshot(bool IsEnabled, int Limit, int InFlight, int Waiting)
{
public int Remaining => Math.Max(0, Limit - InFlight);
}

View File

@@ -1,15 +1,13 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.PairProcessing;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace LightlessSync.Services; namespace LightlessSync.Services;
public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
{ {
private const int HardLimit = 32; private const int _hardLimit = 32;
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly object _limitLock = new(); private readonly object _limitLock = new();
private readonly SemaphoreSlim _semaphore; private readonly SemaphoreSlim _semaphore;
@@ -24,8 +22,8 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
{ {
_configService = configService; _configService = configService;
_currentLimit = CalculateLimit(); _currentLimit = CalculateLimit();
var initialCount = _configService.Current.EnablePairProcessingLimiter ? _currentLimit : HardLimit; var initialCount = _configService.Current.EnablePairProcessingLimiter ? _currentLimit : _hardLimit;
_semaphore = new SemaphoreSlim(initialCount, HardLimit); _semaphore = new SemaphoreSlim(initialCount, _hardLimit);
Mediator.Subscribe<PairProcessingLimitChangedMessage>(this, _ => UpdateSemaphoreLimit()); Mediator.Subscribe<PairProcessingLimitChangedMessage>(this, _ => UpdateSemaphoreLimit());
} }
@@ -88,7 +86,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
if (!enabled) if (!enabled)
{ {
var releaseAmount = HardLimit - _semaphore.CurrentCount; var releaseAmount = _hardLimit - _semaphore.CurrentCount;
if (releaseAmount > 0) if (releaseAmount > 0)
{ {
TryReleaseSemaphore(releaseAmount); TryReleaseSemaphore(releaseAmount);
@@ -110,7 +108,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
var increment = desiredLimit - _currentLimit; var increment = desiredLimit - _currentLimit;
_pendingIncrements += increment; _pendingIncrements += increment;
var available = HardLimit - _semaphore.CurrentCount; var available = _hardLimit - _semaphore.CurrentCount;
var toRelease = Math.Min(_pendingIncrements, available); var toRelease = Math.Min(_pendingIncrements, available);
if (toRelease > 0 && TryReleaseSemaphore(toRelease)) if (toRelease > 0 && TryReleaseSemaphore(toRelease))
{ {
@@ -148,7 +146,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
private int CalculateLimit() private int CalculateLimit()
{ {
var configured = _configService.Current.MaxConcurrentPairApplications; var configured = _configService.Current.MaxConcurrentPairApplications;
return Math.Clamp(configured, 1, HardLimit); return Math.Clamp(configured, 1, _hardLimit);
} }
private bool TryReleaseSemaphore(int count = 1) private bool TryReleaseSemaphore(int count = 1)
@@ -248,8 +246,3 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
} }
} }
} }
public readonly record struct PairProcessingLimiterSnapshot(bool IsEnabled, int Limit, int InFlight, int Waiting)
{
public int Remaining => Math.Max(0, Limit - InFlight);
}

View File

@@ -135,13 +135,13 @@ public sealed class PerformanceCollectorService : IHostedService
if (pastEntries.Any()) if (pastEntries.Any())
{ {
sb.Append((" " + TimeSpan.FromTicks(pastEntries.LastOrDefault() == default ? 0 : pastEntries.Last().Item2).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15)); sb.Append((" " + TimeSpan.FromTicks(pastEntries.LastOrDefault() == default ? 0 : pastEntries[^1].Item2).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15));
sb.Append('|'); sb.Append('|');
sb.Append((" " + TimeSpan.FromTicks(pastEntries.Max(m => m.Item2)).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15)); sb.Append((" " + TimeSpan.FromTicks(pastEntries.Max(m => m.Item2)).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15));
sb.Append('|'); sb.Append('|');
sb.Append((" " + TimeSpan.FromTicks((long)pastEntries.Average(m => m.Item2)).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15)); sb.Append((" " + TimeSpan.FromTicks((long)pastEntries.Average(m => m.Item2)).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15));
sb.Append('|'); sb.Append('|');
sb.Append((" " + (pastEntries.LastOrDefault() == default ? "-" : pastEntries.Last().Item1.ToString("HH:mm:ss.ffff", CultureInfo.InvariantCulture))).PadRight(15, ' ')); sb.Append((" " + (pastEntries.LastOrDefault() == default ? "-" : pastEntries[^1].Item1.ToString("HH:mm:ss.ffff", CultureInfo.InvariantCulture))).PadRight(15, ' '));
sb.Append('|'); sb.Append('|');
sb.Append((" " + pastEntries.Count).PadRight(10)); sb.Append((" " + pastEntries.Count).PadRight(10));
sb.Append('|'); sb.Append('|');
@@ -183,7 +183,7 @@ public sealed class PerformanceCollectorService : IHostedService
{ {
try try
{ {
var last = entries.Value.ToList().Last(); var last = entries.Value.ToList()[^1];
if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now) && !PerformanceCounters.TryRemove(entries.Key, out _)) if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now) && !PerformanceCounters.TryRemove(entries.Key, out _))
{ {
_logger.LogDebug("Could not remove performance counter {counter}", entries.Key); _logger.LogDebug("Could not remove performance counter {counter}", entries.Key);

View File

@@ -1,7 +1,4 @@
using System; namespace LightlessSync.Services.Profiles;
using System.Collections.Generic;
namespace LightlessSync.Services;
public record LightlessGroupProfileData( public record LightlessGroupProfileData(
bool IsDisabled, bool IsDisabled,

View File

@@ -1,7 +1,4 @@
using System; namespace LightlessSync.Services.Profiles;
using System.Collections.Generic;
namespace LightlessSync.Services;
public record LightlessUserProfileData( public record LightlessUserProfileData(
bool IsFlagged, bool IsFlagged,

View File

@@ -22,7 +22,6 @@ public class UiFactory
private readonly ServerConfigurationManager _serverConfigManager; private readonly ServerConfigurationManager _serverConfigManager;
private readonly LightlessProfileManager _lightlessProfileManager; private readonly LightlessProfileManager _lightlessProfileManager;
private readonly PerformanceCollectorService _performanceCollectorService; private readonly PerformanceCollectorService _performanceCollectorService;
private readonly FileDialogManager _fileDialogManager;
private readonly ProfileTagService _profileTagService; private readonly ProfileTagService _profileTagService;
public UiFactory( public UiFactory(
@@ -34,7 +33,6 @@ public class UiFactory
ServerConfigurationManager serverConfigManager, ServerConfigurationManager serverConfigManager,
LightlessProfileManager lightlessProfileManager, LightlessProfileManager lightlessProfileManager,
PerformanceCollectorService performanceCollectorService, PerformanceCollectorService performanceCollectorService,
FileDialogManager fileDialogManager,
ProfileTagService profileTagService) ProfileTagService profileTagService)
{ {
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
@@ -45,7 +43,6 @@ public class UiFactory
_serverConfigManager = serverConfigManager; _serverConfigManager = serverConfigManager;
_lightlessProfileManager = lightlessProfileManager; _lightlessProfileManager = lightlessProfileManager;
_performanceCollectorService = performanceCollectorService; _performanceCollectorService = performanceCollectorService;
_fileDialogManager = fileDialogManager;
_profileTagService = profileTagService; _profileTagService = profileTagService;
} }
@@ -59,8 +56,7 @@ public class UiFactory
_pairUiService, _pairUiService,
dto, dto,
_performanceCollectorService, _performanceCollectorService,
_lightlessProfileManager, _lightlessProfileManager);
_fileDialogManager);
} }
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair) public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)

View File

@@ -46,7 +46,7 @@ public sealed class XivDataAnalyzer
if (handle->FileName.Length > 1024) continue; if (handle->FileName.Length > 1024) continue;
var skeletonName = handle->FileName.ToString(); var skeletonName = handle->FileName.ToString();
if (string.IsNullOrEmpty(skeletonName)) continue; if (string.IsNullOrEmpty(skeletonName)) continue;
outputIndices[skeletonName] = new(); outputIndices[skeletonName] = [];
for (ushort boneIdx = 0; boneIdx < curBones; boneIdx++) for (ushort boneIdx = 0; boneIdx < curBones; boneIdx++)
{ {
var boneName = handle->HavokSkeleton->Bones[boneIdx].Name.String; var boneName = handle->HavokSkeleton->Bones[boneIdx].Name.String;
@@ -70,7 +70,7 @@ public sealed class XivDataAnalyzer
var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash); var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash);
if (cacheEntity == null) return null; if (cacheEntity == null) return null;
using BinaryReader reader = new BinaryReader(File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read)); using BinaryReader reader = new(File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read));
// most of this shit is from vfxeditor, surely nothing will change in the pap format :copium: // most of this shit is from vfxeditor, surely nothing will change in the pap format :copium:
reader.ReadInt32(); // ignore reader.ReadInt32(); // ignore
@@ -177,17 +177,18 @@ public sealed class XivDataAnalyzer
} }
long tris = 0; long tris = 0;
for (int i = 0; i < file.LodCount; i++) foreach (var lod in file.Lods)
{ {
try try
{ {
var meshIdx = file.Lods[i].MeshIndex; var meshIdx = lod.MeshIndex;
var meshCnt = file.Lods[i].MeshCount; var meshCnt = lod.MeshCount;
tris = file.Meshes.Skip(meshIdx).Take(meshCnt).Sum(p => p.IndexCount) / 3; tris = file.Meshes.Skip(meshIdx).Take(meshCnt).Sum(p => p.IndexCount) / 3;
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogDebug(ex, "Could not load lod mesh {mesh} from path {path}", i, filePath); _logger.LogDebug(ex, "Could not load lod mesh {mesh} from path {path}", lod.MeshIndex, filePath);
continue; continue;
} }

View File

@@ -2,6 +2,7 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Utility;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.Services; using LightlessSync.Services;
@@ -21,7 +22,7 @@ namespace LightlessSync.UI
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly BroadcastScannerService _broadcastScannerService; private readonly BroadcastScannerService _broadcastScannerService;
private IReadOnlyList<GroupFullInfoDto> _allSyncshells; private IReadOnlyList<GroupFullInfoDto> _allSyncshells = Array.Empty<GroupFullInfoDto>();
private string _userUid = string.Empty; private string _userUid = string.Empty;
private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new(); private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new();
@@ -191,7 +192,7 @@ namespace LightlessSync.UI
ImGui.PopStyleVar(); ImGui.PopStyleVar();
ImGuiHelpers.ScaledDummy(3f); ImGuiHelpers.ScaledDummy(3f);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
if (_configService.Current.BroadcastEnabled) if (_configService.Current.BroadcastEnabled)
{ {
@@ -287,7 +288,7 @@ namespace LightlessSync.UI
_uiSharedService.MediumText("Syncshell Finder", UIColors.Get("PairBlue")); _uiSharedService.MediumText("Syncshell Finder", UIColors.Get("PairBlue"));
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
ImGui.PushTextWrapPos(); ImGui.PushTextWrapPos();
ImGui.Text("Allow your owned Syncshell to be indexed by the Nearby Syncshell Finder."); ImGui.Text("Allow your owned Syncshell to be indexed by the Nearby Syncshell Finder.");
@@ -295,7 +296,7 @@ namespace LightlessSync.UI
ImGui.PopTextWrapPos(); ImGui.PopTextWrapPos();
ImGuiHelpers.ScaledDummy(0.2f); ImGuiHelpers.ScaledDummy(0.2f);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled; bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled;
bool isBroadcasting = _broadcastService.IsBroadcasting; bool isBroadcasting = _broadcastService.IsBroadcasting;

View File

@@ -13,13 +13,15 @@ internal sealed partial class CharaDataHubUi
AccessTypeDto.AllPairs => "All Pairs", AccessTypeDto.AllPairs => "All Pairs",
AccessTypeDto.ClosePairs => "Direct Pairs", AccessTypeDto.ClosePairs => "Direct Pairs",
AccessTypeDto.Individuals => "Specified", AccessTypeDto.Individuals => "Specified",
AccessTypeDto.Public => "Everyone" AccessTypeDto.Public => "Everyone",
_ => throw new NotSupportedException()
}; };
private static string GetShareTypeString(ShareTypeDto dto) => dto switch private static string GetShareTypeString(ShareTypeDto dto) => dto switch
{ {
ShareTypeDto.Private => "Code Only", ShareTypeDto.Private => "Code Only",
ShareTypeDto.Shared => "Shared" ShareTypeDto.Shared => "Shared",
_ => throw new NotSupportedException()
}; };
private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry) private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry)
@@ -31,7 +33,7 @@ internal sealed partial class CharaDataHubUi
private void GposeMetaInfoAction(Action<CharaDataMetaInfoExtendedDto?> gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning) private void GposeMetaInfoAction(Action<CharaDataMetaInfoExtendedDto?> gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning)
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new();
sb.AppendLine(actionDescription); sb.AppendLine(actionDescription);
bool isDisabled = false; bool isDisabled = false;

View File

@@ -406,7 +406,7 @@ internal sealed partial class CharaDataHubUi
{ {
_uiSharedService.BigText("Poses"); _uiSharedService.BigText("Poses");
var poseCount = updateDto.PoseList.Count(); var poseCount = updateDto.PoseList.Count();
using (ImRaii.Disabled(poseCount >= maxPoses)) using (ImRaii.Disabled(poseCount >= _maxPoses))
{ {
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add new Pose")) if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add new Pose"))
{ {
@@ -414,8 +414,8 @@ internal sealed partial class CharaDataHubUi
} }
} }
ImGui.SameLine(); ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"), poseCount == maxPoses)) using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"), poseCount == _maxPoses))
ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached"); ImGui.TextUnformatted($"{poseCount}/{_maxPoses} poses attached");
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
using var indent = ImRaii.PushIndent(10f); using var indent = ImRaii.PushIndent(10f);
@@ -463,12 +463,16 @@ internal sealed partial class CharaDataHubUi
else else
{ {
var desc = pose.Description; var desc = pose.Description;
if (ImGui.InputTextWithHint("##description", "Description", ref desc, 100)) if (desc != null)
{ {
pose.Description = desc; if (ImGui.InputTextWithHint("##description", "Description", ref desc, 100))
updateDto.UpdatePoseList(); {
pose.Description = desc;
updateDto.UpdatePoseList();
}
ImGui.SameLine();
} }
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete")) if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete"))
{ {
updateDto.RemovePose(pose); updateDto.RemovePose(pose);

View File

@@ -21,7 +21,7 @@ namespace LightlessSync.UI;
internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
{ {
private const int maxPoses = 10; private const int _maxPoses = 10;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly CharaDataNearbyManager _charaDataNearbyManager; private readonly CharaDataNearbyManager _charaDataNearbyManager;
private readonly CharaDataConfigService _configService; private readonly CharaDataConfigService _configService;
@@ -33,7 +33,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private CancellationTokenSource _closalCts = new(); private CancellationTokenSource _closalCts = new();
private bool _disableUI = false; private bool _disableUI = false;
private CancellationTokenSource _disposalCts = new(); private readonly CancellationTokenSource _disposalCts = new();
private string _exportDescription = string.Empty; private string _exportDescription = string.Empty;
private string _filterCodeNote = string.Empty; private string _filterCodeNote = string.Empty;
private string _filterDescription = string.Empty; private string _filterDescription = string.Empty;
@@ -145,6 +145,8 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
{ {
_closalCts.CancelDispose(); _closalCts.CancelDispose();
_disposalCts.CancelDispose(); _disposalCts.CancelDispose();
_disposalCts.Dispose();
_closalCts.Dispose();
} }
base.Dispose(disposing); base.Dispose(disposing);

View File

@@ -27,6 +27,7 @@ using System.Collections.Immutable;
using System.Globalization; using System.Globalization;
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices;
namespace LightlessSync.UI; namespace LightlessSync.UI;
@@ -310,7 +311,7 @@ public class CompactUi : WindowMediatorSubscriberBase
private void DrawPairs() private void DrawPairs()
{ {
var ySize = _transferPartHeight == 0 float ySize = Math.Abs(_transferPartHeight) < 0.0001f
? 1 ? 1
: (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y
+ ImGui.GetTextLineHeight() - ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().WindowBorderSize) - _transferPartHeight - ImGui.GetCursorPosY(); + ImGui.GetTextLineHeight() - ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().WindowBorderSize) - _transferPartHeight - ImGui.GetCursorPosY();
@@ -510,6 +511,7 @@ public class CompactUi : WindowMediatorSubscriberBase
return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes); return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes);
} }
[StructLayout(LayoutKind.Auto)]
private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes) private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes)
{ {
public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0; public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0;
@@ -590,7 +592,7 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGui.PopStyleColor(); ImGui.PopStyleColor();
ImGuiHelpers.ScaledDummy(0.2f); ImGuiHelpers.ScaledDummy(0.2f);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
if (_configService.Current.BroadcastEnabled) if (_configService.Current.BroadcastEnabled)
{ {

View File

@@ -119,6 +119,7 @@ public class DrawFolderGroup : DrawFolderBase
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell", menuWidth, true) && UiSharedService.CtrlPressed()) if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell", menuWidth, true) && UiSharedService.CtrlPressed())
{ {
_ = _apiController.GroupLeave(_groupFullInfoDto); _ = _apiController.GroupLeave(_groupFullInfoDto);
_lightlessMediator.Publish(new UserLeftSyncshell(_groupFullInfoDto.GID));
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
} }
UiSharedService.AttachToolTip("Hold CTRL and click to leave this Syncshell" + (!string.Equals(_groupFullInfoDto.OwnerUID, _apiController.UID, StringComparison.Ordinal) UiSharedService.AttachToolTip("Hold CTRL and click to leave this Syncshell" + (!string.Equals(_groupFullInfoDto.OwnerUID, _apiController.UID, StringComparison.Ordinal)

View File

@@ -5,6 +5,7 @@ using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.PairProcessing;
using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files;
using LightlessSync.WebAPI.Files.Models; using LightlessSync.WebAPI.Files.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -22,6 +23,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
private readonly UiSharedService _uiShared; private readonly UiSharedService _uiShared;
private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new(); private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
private readonly Dictionary<GameObjectHandler, Vector2> _smoothed = [];
private bool _notificationDismissed = true; private bool _notificationDismissed = true;
private int _lastDownloadStateHash = 0; private int _lastDownloadStateHash = 0;
@@ -203,8 +205,18 @@ public class DownloadUi : WindowMediatorSubscriberBase
foreach (var transfer in _currentDownloads.ToList()) foreach (var transfer in _currentDownloads.ToList())
{ {
var screenPos = _dalamudUtilService.WorldToScreen(transfer.Key.GetGameObject()); var transferKey = transfer.Key;
if (screenPos == Vector2.Zero) continue; var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());
//If RawPos is zero, remove it from smoothed dictionary
if (rawPos == Vector2.Zero)
{
_smoothed.Remove(transferKey);
continue;
}
//Smoothing out the movement and fix jitter around the position.
Vector2 screenPos = _smoothed.TryGetValue(transferKey, out var lastPos) ? (rawPos - lastPos).Length() < 4f ? lastPos : rawPos : rawPos;
_smoothed[transferKey] = screenPos;
var totalBytes = transfer.Value.Sum(c => c.Value.TotalBytes); var totalBytes = transfer.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = transfer.Value.Sum(c => c.Value.TransferredBytes); var transferredBytes = transfer.Value.Sum(c => c.Value.TransferredBytes);

View File

@@ -347,7 +347,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
try try
{ {
var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult(); var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult();
var hashedCid = cid.ToString().GetHash256(); var hashedCid = cid.ToString().GetBlake3Hash();
_localHashedCid = hashedCid; _localHashedCid = hashedCid;
_localHashedCidFetchedAt = now; _localHashedCidFetchedAt = now;
return hashedCid; return hashedCid;
@@ -445,7 +445,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
return ($"{icon} OFF", colors, tooltip.ToString()); return ($"{icon} OFF", colors, tooltip.ToString());
} }
private (string, Colors, string) FormatTooltip(string title, IEnumerable<string> names, string icon, Colors color) private static (string, Colors, string) FormatTooltip(string title, IEnumerable<string> names, string icon, Colors color)
{ {
var list = names.Where(x => !string.IsNullOrEmpty(x)).ToList(); var list = names.Where(x => !string.IsNullOrEmpty(x)).ToList();
var tooltip = new StringBuilder() var tooltip = new StringBuilder()

View File

@@ -9,6 +9,7 @@ using LightlessSync.API.Data;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.Profiles;
using LightlessSync.UI.Tags; using LightlessSync.UI.Tags;
using LightlessSync.Utils; using LightlessSync.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;

View File

@@ -25,6 +25,7 @@ using System.IO;
using System.Numerics; using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Linq; using System.Linq;
using LightlessSync.Services.Profiles;
namespace LightlessSync.UI; namespace LightlessSync.UI;
@@ -91,14 +92,13 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase
private bool _wasOpen; private bool _wasOpen;
private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f); private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f);
private bool vanityInitialized; // useless for now
private bool textEnabled; private bool textEnabled;
private bool glowEnabled; private bool glowEnabled;
private Vector4 textColor; private Vector4 textColor;
private Vector4 glowColor; private Vector4 glowColor;
private record VanityState(bool TextEnabled, bool GlowEnabled, Vector4 TextColor, Vector4 GlowColor); private sealed record VanityState(bool TextEnabled, bool GlowEnabled, Vector4 TextColor, Vector4 GlowColor);
private VanityState _savedVanity; private VanityState? _savedVanity;
public EditProfileUi(ILogger<EditProfileUi> logger, LightlessMediator mediator, public EditProfileUi(ILogger<EditProfileUi> logger, LightlessMediator mediator,
ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager, ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager,
@@ -161,7 +161,6 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase
glowColor = glowEnabled ? UIColors.HexToRgba(_apiController.TextGlowColorHex!) : Vector4.Zero; glowColor = glowEnabled ? UIColors.HexToRgba(_apiController.TextGlowColorHex!) : Vector4.Zero;
_savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor); _savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor);
vanityInitialized = true;
} }
public override async void OnOpen() public override async void OnOpen()

View File

@@ -177,13 +177,11 @@ public class IdDisplayHandler
Vector2 itemMin; Vector2 itemMin;
Vector2 itemMax; Vector2 itemMax;
Vector2 textSize;
using (ImRaii.PushFont(font, textIsUid)) using (ImRaii.PushFont(font, textIsUid))
{ {
SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font, pair.UserData.UID); SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font, pair.UserData.UID);
itemMin = ImGui.GetItemRectMin(); itemMin = ImGui.GetItemRectMin();
itemMax = ImGui.GetItemRectMax(); itemMax = ImGui.GetItemRectMax();
//textSize = itemMax - itemMin;
} }
if (useHighlight) if (useHighlight)
@@ -227,7 +225,7 @@ public class IdDisplayHandler
var nameRectMax = ImGui.GetItemRectMax(); var nameRectMax = ImGui.GetItemRectMax();
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
{ {
if (!string.Equals(_lastMouseOverUid, id)) if (!string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal))
{ {
_popupTime = DateTime.UtcNow.AddSeconds(_lightlessConfigService.Current.ProfileDelay); _popupTime = DateTime.UtcNow.AddSeconds(_lightlessConfigService.Current.ProfileDelay);
} }
@@ -248,7 +246,7 @@ public class IdDisplayHandler
} }
else else
{ {
if (string.Equals(_lastMouseOverUid, id)) if (string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal))
{ {
_mediator.Publish(new ProfilePopoutToggle(Pair: null)); _mediator.Publish(new ProfilePopoutToggle(Pair: null));
_lastMouseOverUid = string.Empty; _lastMouseOverUid = string.Empty;

View File

@@ -267,7 +267,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
{ {
UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed); UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed);
} }
else if (_secretKey.Length == 64 && !HexRegex().IsMatch(_secretKey)) else if (_secretKey.Length == 64 && !SecretRegex().IsMatch(_secretKey))
{ {
UiSharedService.ColorTextWrapped("Your secret key can only contain ABCDEF and the numbers 0-9.", ImGuiColors.DalamudRed); UiSharedService.ColorTextWrapped("Your secret key can only contain ABCDEF and the numbers 0-9.", ImGuiColors.DalamudRed);
} }
@@ -360,6 +360,6 @@ public partial class IntroUi : WindowMediatorSubscriberBase
_tosParagraphs = [Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6]; _tosParagraphs = [Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6];
} }
[GeneratedRegex("^([A-F0-9]{2})+")] [GeneratedRegex("^[A-F0-9]{64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant)]
private static partial Regex HexRegex(); private static partial Regex SecretRegex();
} }

View File

@@ -1,5 +1,4 @@
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
@@ -174,6 +173,7 @@ internal class JoinSyncshellUI : WindowMediatorSubscriberBase
joinPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations); joinPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations);
joinPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); joinPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX);
_ = _apiController.GroupJoinFinalize(new GroupJoinDto(_groupJoinInfo.Group, _previousPassword, joinPermissions)); _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_groupJoinInfo.Group, _previousPassword, joinPermissions));
Mediator.Publish(new UserJoinedSyncshell(_groupJoinInfo.Group.GID));
IsOpen = false; IsOpen = false;
} }
} }

View File

@@ -1,8 +1,6 @@
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models; using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services; using LightlessSync.Services;
@@ -27,11 +25,11 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
private const float _titleMessageSpacing = 4f; private const float _titleMessageSpacing = 4f;
private const float _actionButtonSpacing = 8f; private const float _actionButtonSpacing = 8f;
private readonly List<LightlessNotification> _notifications = new(); private readonly List<LightlessNotification> _notifications = [];
private readonly object _notificationLock = new(); private readonly object _notificationLock = new();
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly Dictionary<string, float> _notificationYOffsets = new(); private readonly Dictionary<string, float> _notificationYOffsets = [];
private readonly Dictionary<string, float> _notificationTargetYOffsets = new(); private readonly Dictionary<string, float> _notificationTargetYOffsets = [];
public LightlessNotificationUi(ILogger<LightlessNotificationUi> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService) public LightlessNotificationUi(ILogger<LightlessNotificationUi> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService)
: base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector) : base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector)
@@ -45,7 +43,6 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoBackground |
ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoCollapse |
ImGuiWindowFlags.NoInputs |
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoTitleBar |
ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollbar |
ImGuiWindowFlags.AlwaysAutoResize; ImGuiWindowFlags.AlwaysAutoResize;
@@ -68,7 +65,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
{ {
lock (_notificationLock) lock (_notificationLock)
{ {
var existingNotification = _notifications.FirstOrDefault(n => n.Id == notification.Id); var existingNotification = _notifications.FirstOrDefault(n => string.Equals(n.Id, notification.Id, StringComparison.Ordinal));
if (existingNotification != null) if (existingNotification != null)
{ {
UpdateExistingNotification(existingNotification, notification); UpdateExistingNotification(existingNotification, notification);
@@ -103,7 +100,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
{ {
lock (_notificationLock) lock (_notificationLock)
{ {
var notification = _notifications.FirstOrDefault(n => n.Id == id); var notification = _notifications.FirstOrDefault(n => string.Equals(n.Id, id, StringComparison.Ordinal));
if (notification != null) if (notification != null)
{ {
StartOutAnimation(notification); StartOutAnimation(notification);
@@ -122,13 +119,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
} }
} }
private void StartOutAnimation(LightlessNotification notification) private static void StartOutAnimation(LightlessNotification notification)
{ {
notification.IsAnimatingOut = true; notification.IsAnimatingOut = true;
notification.IsAnimatingIn = false; notification.IsAnimatingIn = false;
} }
private bool ShouldRemoveNotification(LightlessNotification notification) => private static bool ShouldRemoveNotification(LightlessNotification notification) =>
notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f; notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f;
protected override void DrawInternal() protected override void DrawInternal()
@@ -185,7 +182,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
ImGui.SetCursorPosY(startY + yOffset); ImGui.SetCursorPosY(startY + yOffset);
} }
DrawNotification(notification, i); DrawNotification(notification);
} }
} }
@@ -304,7 +301,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
return corner == NotificationCorner.Left ? new Vector2(-distance, 0) : new Vector2(distance, 0); return corner == NotificationCorner.Left ? new Vector2(-distance, 0) : new Vector2(distance, 0);
} }
private void DrawNotification(LightlessNotification notification, int index) private void DrawNotification(LightlessNotification notification)
{ {
var alpha = notification.AnimationProgress; var alpha = notification.AnimationProgress;
if (alpha <= 0f) return; if (alpha <= 0f) return;
@@ -339,7 +336,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
var bgColor = CalculateBackgroundColor(alpha, ImGui.IsWindowHovered()); var bgColor = CalculateBackgroundColor(alpha, ImGui.IsWindowHovered());
var accentColor = GetNotificationAccentColor(notification.Type); var accentColor = GetNotificationAccentColor(notification.Type);
accentColor.W *= alpha; accentColor.W *= alpha;
DrawShadow(drawList, windowPos, windowSize, alpha); DrawShadow(drawList, windowPos, windowSize, alpha);
HandleClickToDismiss(notification); HandleClickToDismiss(notification);
DrawBackground(drawList, windowPos, windowSize, bgColor); DrawBackground(drawList, windowPos, windowSize, bgColor);
@@ -370,7 +367,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
return bgColor; return bgColor;
} }
private void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha) private static void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha)
{ {
var shadowOffset = new Vector2(1f, 1f); var shadowOffset = new Vector2(1f, 1f);
var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha); var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha);
@@ -384,9 +381,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
private void HandleClickToDismiss(LightlessNotification notification) private void HandleClickToDismiss(LightlessNotification notification)
{ {
if (ImGui.IsWindowHovered() && var pos = ImGui.GetWindowPos();
var size = ImGui.GetWindowSize();
bool hovered = ImGui.IsMouseHoveringRect(pos, new Vector2(pos.X + size.X, pos.Y + size.Y));
if ((hovered || ImGui.IsWindowHovered()) &&
_configService.Current.DismissNotificationOnClick && _configService.Current.DismissNotificationOnClick &&
!notification.Actions.Any() && notification.Actions.Count == 0 &&
ImGui.IsMouseClicked(ImGuiMouseButton.Left)) ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{ {
notification.IsDismissed = true; notification.IsDismissed = true;
@@ -394,7 +395,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
} }
} }
private void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor) private static void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor)
{ {
drawList.AddRectFilled( drawList.AddRectFilled(
windowPos, windowPos,
@@ -431,14 +432,14 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
); );
} }
private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList) private static void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList)
{ {
var progress = CalculateDurationProgress(notification); var progress = CalculateDurationProgress(notification);
var progressBarColor = UIColors.Get("LightlessBlue"); var progressBarColor = UIColors.Get("LightlessBlue");
var progressHeight = 2f; var progressHeight = 2f;
var progressY = windowPos.Y + windowSize.Y - progressHeight; var progressY = windowPos.Y + windowSize.Y - progressHeight;
var progressWidth = windowSize.X * progress; var progressWidth = windowSize.X * progress;
DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha); DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha);
if (progress > 0) if (progress > 0)
@@ -447,7 +448,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
} }
} }
private void DrawDownloadProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList) private static void DrawDownloadProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList)
{ {
var progress = Math.Clamp(notification.Progress, 0f, 1f); var progress = Math.Clamp(notification.Progress, 0f, 1f);
var progressBarColor = UIColors.Get("LightlessGreen"); var progressBarColor = UIColors.Get("LightlessGreen");
@@ -455,7 +456,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
// Position above the duration bar (2px duration bar + 1px spacing) // Position above the duration bar (2px duration bar + 1px spacing)
var progressY = windowPos.Y + windowSize.Y - progressHeight - 3f; var progressY = windowPos.Y + windowSize.Y - progressHeight - 3f;
var progressWidth = windowSize.X * progress; var progressWidth = windowSize.X * progress;
DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha); DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha);
if (progress > 0) if (progress > 0)
@@ -464,14 +465,14 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
} }
} }
private float CalculateDurationProgress(LightlessNotification notification) private static float CalculateDurationProgress(LightlessNotification notification)
{ {
// Calculate duration timer progress // Calculate duration timer progress
var elapsed = DateTime.UtcNow - notification.CreatedAt; var elapsed = DateTime.UtcNow - notification.CreatedAt;
return Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds)); return Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds));
} }
private void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha) private static void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha)
{ {
var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha); var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha);
drawList.AddRectFilled( drawList.AddRectFilled(
@@ -482,7 +483,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
); );
} }
private void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha) private static void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha)
{ {
var progressColor = progressBarColor; var progressColor = progressBarColor;
progressColor.W *= alpha; progressColor.W *= alpha;
@@ -512,13 +513,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
} }
} }
private float CalculateContentWidth(float windowWidth) => private static float CalculateContentWidth(float windowWidth) =>
windowWidth - (_contentPaddingX * 2); windowWidth - (_contentPaddingX * 2);
private bool HasActions(LightlessNotification notification) => private static bool HasActions(LightlessNotification notification) =>
notification.Actions.Count > 0; notification.Actions.Count > 0;
private void PositionActionsAtBottom(float windowHeight) private static void PositionActionsAtBottom(float windowHeight)
{ {
var actionHeight = ImGui.GetFrameHeight(); var actionHeight = ImGui.GetFrameHeight();
var bottomY = windowHeight - _contentPaddingY - actionHeight; var bottomY = windowHeight - _contentPaddingY - actionHeight;
@@ -546,7 +547,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
return $"[{timestamp}] {notification.Title}"; return $"[{timestamp}] {notification.Title}";
} }
private float DrawWrappedText(string text, float wrapWidth) private static float DrawWrappedText(string text, float wrapWidth)
{ {
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + wrapWidth); ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + wrapWidth);
var startY = ImGui.GetCursorPosY(); var startY = ImGui.GetCursorPosY();
@@ -556,7 +557,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
return height; return height;
} }
private void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha) private static void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha)
{ {
if (string.IsNullOrEmpty(notification.Message)) return; if (string.IsNullOrEmpty(notification.Message)) return;
@@ -591,13 +592,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
} }
} }
private float CalculateActionButtonWidth(int actionCount, float availableWidth) private static float CalculateActionButtonWidth(int actionCount, float availableWidth)
{ {
var totalSpacing = (actionCount - 1) * _actionButtonSpacing; var totalSpacing = (actionCount - 1) * _actionButtonSpacing;
return (availableWidth - totalSpacing) / actionCount; return (availableWidth - totalSpacing) / actionCount;
} }
private void PositionActionButton(int index, float startX, float buttonWidth) private static void PositionActionButton(int index, float startX, float buttonWidth)
{ {
var xPosition = startX + index * (buttonWidth + _actionButtonSpacing); var xPosition = startX + index * (buttonWidth + _actionButtonSpacing);
ImGui.SetCursorPosX(xPosition); ImGui.SetCursorPosX(xPosition);
@@ -625,7 +626,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
if (action.Icon != FontAwesomeIcon.None) if (action.Icon != FontAwesomeIcon.None)
{ {
buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth, alpha); buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth);
} }
else else
{ {
@@ -650,10 +651,10 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
} }
} }
private bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width, float alpha) private static bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width)
{ {
var drawList = ImGui.GetWindowDrawList(); var drawList = ImGui.GetWindowDrawList();
var cursorPos = ImGui.GetCursorScreenPos(); ImGui.GetCursorScreenPos();
var frameHeight = ImGui.GetFrameHeight(); var frameHeight = ImGui.GetFrameHeight();
Vector2 iconSize; Vector2 iconSize;
@@ -729,7 +730,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
return ImGui.CalcTextSize(titleText, true, contentWidth).Y; return ImGui.CalcTextSize(titleText, true, contentWidth).Y;
} }
private float CalculateMessageHeight(LightlessNotification notification, float contentWidth) private static float CalculateMessageHeight(LightlessNotification notification, float contentWidth)
{ {
if (string.IsNullOrEmpty(notification.Message)) return 0f; if (string.IsNullOrEmpty(notification.Message)) return 0f;
@@ -737,7 +738,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
return 4f + messageHeight; return 4f + messageHeight;
} }
private Vector4 GetNotificationAccentColor(NotificationType type) private static Vector4 GetNotificationAccentColor(NotificationType type)
{ {
return type switch return type switch
{ {

View File

@@ -1,43 +0,0 @@
namespace LightlessSync.UI.Models
{
public class ChangelogFile
{
public string Tagline { get; init; } = string.Empty;
public string Subline { get; init; } = string.Empty;
public List<ChangelogEntry> Changelog { get; init; } = new();
public List<CreditCategory>? Credits { get; init; }
}
public class ChangelogEntry
{
public string Name { get; init; } = string.Empty;
public string Date { get; init; } = string.Empty;
public string Tagline { get; init; } = string.Empty;
public bool? IsCurrent { get; init; }
public string? Message { get; init; }
public List<ChangelogVersion>? Versions { get; init; }
}
public class ChangelogVersion
{
public string Number { get; init; } = string.Empty;
public List<string> Items { get; init; } = new();
}
public class CreditCategory
{
public string Category { get; init; } = string.Empty;
public List<CreditItem> Items { get; init; } = new();
}
public class CreditItem
{
public string Name { get; init; } = string.Empty;
public string Role { get; init; } = string.Empty;
}
public class CreditsFile
{
public List<CreditCategory> Credits { get; init; } = new();
}
}

View File

@@ -0,0 +1,12 @@
namespace LightlessSync.UI.Models
{
public class ChangelogEntry
{
public string Name { get; init; } = string.Empty;
public string Date { get; init; } = string.Empty;
public string Tagline { get; init; } = string.Empty;
public bool? IsCurrent { get; init; }
public string? Message { get; init; }
public List<ChangelogVersion>? Versions { get; init; }
}
}

View File

@@ -0,0 +1,10 @@
namespace LightlessSync.UI.Models
{
public class ChangelogFile
{
public string Tagline { get; init; } = string.Empty;
public string Subline { get; init; } = string.Empty;
public List<ChangelogEntry> Changelog { get; init; } = new();
public List<CreditCategory>? Credits { get; init; }
}
}

View File

@@ -0,0 +1,8 @@
namespace LightlessSync.UI.Models
{
public class ChangelogVersion
{
public string Number { get; init; } = string.Empty;
public List<string> Items { get; init; } = [];
}
}

View File

@@ -0,0 +1,8 @@
namespace LightlessSync.UI.Models
{
public class CreditCategory
{
public string Category { get; init; } = string.Empty;
public List<CreditItem> Items { get; init; } = [];
}
}

View File

@@ -0,0 +1,8 @@
namespace LightlessSync.UI.Models
{
public class CreditItem
{
public string Name { get; init; } = string.Empty;
public string Role { get; init; } = string.Empty;
}
}

View File

@@ -0,0 +1,7 @@
namespace LightlessSync.UI.Models
{
public class CreditsFile
{
public List<CreditCategory> Credits { get; init; } = [];
}
}

View File

@@ -1,7 +1,7 @@
using Dalamud.Interface;
using LightlessSync.LightlessConfiguration.Models; using LightlessSync.LightlessConfiguration.Models;
using System.Numerics;
namespace LightlessSync.UI.Models; namespace LightlessSync.UI.Models;
public class LightlessNotification public class LightlessNotification
{ {
public string Id { get; set; } = Guid.NewGuid().ToString(); public string Id { get; set; } = Guid.NewGuid().ToString();
@@ -20,13 +20,3 @@ public class LightlessNotification
public bool IsAnimatingOut { get; set; } = false; public bool IsAnimatingOut { get; set; } = false;
public uint? SoundEffectId { get; set; } = null; public uint? SoundEffectId { get; set; } = null;
} }
public class LightlessNotificationAction
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Label { get; set; } = string.Empty;
public FontAwesomeIcon Icon { get; set; } = FontAwesomeIcon.None;
public Vector4 Color { get; set; } = Vector4.One;
public Action<LightlessNotification> OnClick { get; set; } = _ => { };
public bool IsPrimary { get; set; } = false;
public bool IsDestructive { get; set; } = false;
}

View File

@@ -0,0 +1,15 @@
using Dalamud.Interface;
using System.Numerics;
namespace LightlessSync.UI.Models;
public class LightlessNotificationAction
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Label { get; set; } = string.Empty;
public FontAwesomeIcon Icon { get; set; } = FontAwesomeIcon.None;
public Vector4 Color { get; set; } = Vector4.One;
public Action<LightlessNotification> OnClick { get; set; } = _ => { };
public bool IsPrimary { get; set; } = false;
public bool IsDestructive { get; set; } = false;
}

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Pairs; using LightlessSync.PlayerData.Pairs;

View File

@@ -612,7 +612,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
} }
private bool DrawStyleResetButton(string key, bool hasOverride, string? tooltipOverride = null) private static bool DrawStyleResetButton(string key, bool hasOverride, string? tooltipOverride = null)
{ {
using var id = ImRaii.PushId($"reset-{key}"); using var id = ImRaii.PushId($"reset-{key}");
using var disabled = ImRaii.Disabled(!hasOverride); using var disabled = ImRaii.Disabled(!hasOverride);
@@ -736,7 +736,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.DrawHelpText("Controls how many uploads can run at once."); _uiShared.DrawHelpText("Controls how many uploads can run at once.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
if (ImGui.Checkbox("Enable Pair Download Limiter", ref limitPairApplications)) if (ImGui.Checkbox("Enable Pair Download Limiter", ref limitPairApplications))
{ {
@@ -783,7 +783,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.TextColored(ImGuiColors.DalamudGrey, "Pair apply limiter is disabled."); ImGui.TextColored(ImGuiColors.DalamudGrey, "Pair apply limiter is disabled.");
} }
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
if (ImGui.Checkbox("Use Alternative Upload Method", ref useAlternativeUpload)) if (ImGui.Checkbox("Use Alternative Upload Method", ref useAlternativeUpload))
{ {
@@ -899,13 +899,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
using var tree = ImRaii.TreeNode("Speed Test to Servers"); using var tree = ImRaii.TreeNode("Speed Test to Servers");
if (tree) if (tree)
{ {
if (_downloadServersTask == null || ((_downloadServersTask?.IsCompleted ?? false) && if ((_downloadServersTask == null || ((_downloadServersTask?.IsCompleted ?? false) &&
(!_downloadServersTask?.IsCompletedSuccessfully ?? false))) (!_downloadServersTask?.IsCompletedSuccessfully ?? false))) && _uiShared.IconTextButton(FontAwesomeIcon.GroupArrowsRotate, "Update Download Server List"))
{ {
if (_uiShared.IconTextButton(FontAwesomeIcon.GroupArrowsRotate, "Update Download Server List")) _downloadServersTask = GetDownloadServerList();
{
_downloadServersTask = GetDownloadServerList();
}
} }
if (_downloadServersTask != null && _downloadServersTask.IsCompleted && if (_downloadServersTask != null && _downloadServersTask.IsCompleted &&
@@ -1136,9 +1133,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
.DeserializeAsync<List<string>>(await result.Content.ReadAsStreamAsync().ConfigureAwait(false)) .DeserializeAsync<List<string>>(await result.Content.ReadAsStreamAsync().ConfigureAwait(false))
.ConfigureAwait(false); .ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception)
{ {
_logger.LogWarning(ex, "Failed to get download server list"); _logger.LogWarning("Failed to get download server list");
throw; throw;
} }
} }
@@ -1219,7 +1216,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
UiSharedService.TooltipSeparator UiSharedService.TooltipSeparator
+ "Keeping LOD enabled can lead to more crashes. Use at your own risk."); + "Keeping LOD enabled can lead to more crashes. Use at your own risk.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 2f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 2f);
} }
private void DrawFileStorageSettings() private void DrawFileStorageSettings()
@@ -1421,7 +1418,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
} }
_uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -1453,7 +1450,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
catch (IOException ex) catch (IOException ex)
{ {
_logger.LogWarning(ex, $"Could not delete file {file} because it is in use."); _logger.LogWarning(ex, "Could not delete file {file} because it is in use.", file);
} }
} }
@@ -1487,7 +1484,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.EndDisabled(); ImGui.EndDisabled();
ImGui.Unindent(); ImGui.Unindent();
_uiShared.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
} }
@@ -1500,8 +1497,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
_lastTab = "General"; _lastTab = "General";
//UiSharedService.FontText("Experimental", _uiShared.UidFont);
//ImGui.Separator();
_uiShared.UnderlinedBigText("General Settings", UIColors.Get("LightlessBlue")); _uiShared.UnderlinedBigText("General Settings", UIColors.Get("LightlessBlue"));
ImGui.Dummy(new Vector2(10)); ImGui.Dummy(new Vector2(10));
@@ -1539,7 +1534,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGuiColors.DalamudRed); ImGuiColors.DalamudRed);
} }
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -1567,7 +1562,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.DrawHelpText( _uiShared.DrawHelpText(
"This will automatically populate user notes using the first encountered player name if the note was not set prior"); "This will automatically populate user notes using the first encountered player name if the note was not set prior");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -1635,7 +1630,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -1675,7 +1670,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
_uiShared.DrawHelpText("When enabled, Lightfinder will automatically turn on after reconnecting to the Lightless server."); _uiShared.DrawHelpText("When enabled, Lightfinder will automatically turn on after reconnecting to the Lightless server.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Lightfinder Nameplate Colors"); ImGui.TextUnformatted("Lightfinder Nameplate Colors");
if (ImGui.BeginTable("##LightfinderColorTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) if (ImGui.BeginTable("##LightfinderColorTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
@@ -1731,7 +1726,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Spacing(); ImGui.Spacing();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Lightfinder Info Bar"); ImGui.TextUnformatted("Lightfinder Info Bar");
if (ImGui.Checkbox("Show Lightfinder status in Server info bar", ref showLightfinderInDtr)) if (ImGui.Checkbox("Show Lightfinder status in Server info bar", ref showLightfinderInDtr))
@@ -1827,7 +1822,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
ImGui.EndDisabled(); ImGui.EndDisabled();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Alignment"); ImGui.TextUnformatted("Alignment");
ImGui.BeginDisabled(autoAlign); ImGui.BeginDisabled(autoAlign);
@@ -1952,7 +1947,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Visibility"); ImGui.TextUnformatted("Visibility");
var showOwn = _configService.Current.LightfinderLabelShowOwn; var showOwn = _configService.Current.LightfinderLabelShowOwn;
@@ -1990,7 +1985,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
_uiShared.DrawHelpText("Toggles Lightfinder label when no nameplate(s) is visible."); _uiShared.DrawHelpText("Toggles Lightfinder label when no nameplate(s) is visible.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Label"); ImGui.TextUnformatted("Label");
var useIcon = _configService.Current.LightfinderLabelUseIcon; var useIcon = _configService.Current.LightfinderLabelUseIcon;
@@ -2096,7 +2091,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_lightfinderIconPresetIndex = -1; _lightfinderIconPresetIndex = -1;
} }
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -2184,7 +2179,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Spacing(); ImGui.Spacing();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Server Info Bar Colors"); ImGui.TextUnformatted("Server Info Bar Colors");
@@ -2236,7 +2231,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Spacing(); ImGui.Spacing();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Nameplate Colors"); ImGui.TextUnformatted("Nameplate Colors");
@@ -2281,7 +2276,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Spacing(); ImGui.Spacing();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("UI Theme"); ImGui.TextUnformatted("UI Theme");
@@ -2303,7 +2298,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
DrawThemeOverridesSection(); DrawThemeOverridesSection();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -2401,7 +2396,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_configService.Save(); _configService.Save();
} }
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -2444,7 +2439,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.DrawHelpText("Will show profiles that have the NSFW tag enabled"); _uiShared.DrawHelpText("Will show profiles that have the NSFW tag enabled");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
ImGui.Separator(); ImGui.Separator();
@@ -2542,7 +2537,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
+ "Default: 165 thousand"); + "Default: 165 thousand");
} }
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -2646,7 +2641,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
+ "Default: 250 thousand"); + "Default: 250 thousand");
} }
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -2726,7 +2721,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Dummy(new Vector2(5)); ImGui.Dummy(new Vector2(5));
_uiShared.ColoredSeparator(UIColors.Get("DimRed"), 3f); UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 3f);
var onlyUncompressed = textureConfig.OnlyDownscaleUncompressedTextures; var onlyUncompressed = textureConfig.OnlyDownscaleUncompressedTextures;
if (ImGui.Checkbox("Only downscale uncompressed textures", ref onlyUncompressed)) if (ImGui.Checkbox("Only downscale uncompressed textures", ref onlyUncompressed))
{ {
@@ -2734,7 +2729,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_playerPerformanceConfigService.Save(); _playerPerformanceConfigService.Save();
} }
_uiShared.DrawHelpText("If disabled, compressed textures will be targeted for downscaling too."); _uiShared.DrawHelpText("If disabled, compressed textures will be targeted for downscaling too.");
_uiShared.ColoredSeparator(UIColors.Get("DimRed"), 3f); UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 3f);
ImGui.Dummy(new Vector2(5)); ImGui.Dummy(new Vector2(5));
@@ -2742,7 +2737,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Dummy(new Vector2(5)); ImGui.Dummy(new Vector2(5));
_uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -2890,7 +2885,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.EndPopup(); ImGui.EndPopup();
} }
_uiShared.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -3468,15 +3463,13 @@ public class SettingsUi : WindowMediatorSubscriberBase
private int _lastSelectedServerIndex = -1; private int _lastSelectedServerIndex = -1;
private Task<(bool Success, bool PartialSuccess, string Result)>? _secretKeysConversionTask = null; private Task<(bool Success, bool PartialSuccess, string Result)>? _secretKeysConversionTask = null;
private CancellationTokenSource _secretKeysConversionCts = new CancellationTokenSource(); private CancellationTokenSource _secretKeysConversionCts = new();
private async Task<(bool Success, bool partialSuccess, string Result)> ConvertSecretKeysToUIDs( private async Task<(bool Success, bool partialSuccess, string Result)> ConvertSecretKeysToUIDs(
ServerStorage serverStorage, CancellationToken token) ServerStorage serverStorage, CancellationToken token)
{ {
List<Authentication> failedConversions = serverStorage.Authentications List<Authentication> failedConversions = [.. serverStorage.Authentications.Where(u => u.SecretKeyIdx == -1 && string.IsNullOrEmpty(u.UID))];
.Where(u => u.SecretKeyIdx == -1 && string.IsNullOrEmpty(u.UID)).ToList(); List<Authentication> conversionsToAttempt = [.. serverStorage.Authentications.Where(u => u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID))];
List<Authentication> conversionsToAttempt = serverStorage.Authentications
.Where(u => u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID)).ToList();
List<Authentication> successfulConversions = []; List<Authentication> successfulConversions = [];
Dictionary<string, List<Authentication>> secretKeyMapping = new(StringComparer.Ordinal); Dictionary<string, List<Authentication>> secretKeyMapping = new(StringComparer.Ordinal);
foreach (var authEntry in conversionsToAttempt) foreach (var authEntry in conversionsToAttempt)
@@ -3546,6 +3539,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
sb.Append(string.Join(", ", failedConversions.Select(k => k.CharacterName))); sb.Append(string.Join(", ", failedConversions.Select(k => k.CharacterName)));
} }
_secretKeysConversionCts.Dispose();
return (true, failedConversions.Count != 0, sb.ToString()); return (true, failedConversions.Count != 0, sb.ToString());
} }
@@ -3914,7 +3908,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Unindent(); ImGui.Unindent();
} }
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -3956,7 +3950,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.DrawHelpText("Click anywhere on a notification to dismiss it. Notifications with action buttons (like pair requests) are excluded."); _uiShared.DrawHelpText("Click anywhere on a notification to dismiss it. Notifications with action buttons (like pair requests) are excluded.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -4119,7 +4113,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.SetTooltip("Right click to reset to default (3)."); ImGui.SetTooltip("Right click to reset to default (3).");
_uiShared.DrawHelpText("Width of the colored accent bar on the left side."); _uiShared.DrawHelpText("Width of the colored accent bar on the left side.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
} }
@@ -4214,7 +4208,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (20)."); ImGui.SetTooltip("Right click to reset to default (20).");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -4229,7 +4223,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.DrawHelpText( _uiShared.DrawHelpText(
"Configure which sounds play for each notification type. Use the play button to preview sounds."); "Configure which sounds play for each notification type. Use the play button to preview sounds.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -4277,7 +4271,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
"Only show online notifications for pairs where you have set an individual note."); "Only show online notifications for pairs where you have set an individual note.");
ImGui.Unindent(); ImGui.Unindent();
_uiShared.ColoredSeparator(UIColors.Get("LightlessGreen"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessGreen"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -4293,7 +4287,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.DrawHelpText( _uiShared.DrawHelpText(
"When you receive a pair request, show Accept/Decline buttons in the notification."); "When you receive a pair request, show Accept/Decline buttons in the notification.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -4309,7 +4303,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.DrawHelpText( _uiShared.DrawHelpText(
"When a player exceeds performance thresholds or is auto-paused, show Pause/Unpause buttons in the notification."); "When a player exceeds performance thresholds or is auto-paused, show Pause/Unpause buttons in the notification.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessOrange"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessOrange"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -4324,7 +4318,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.DrawHelpText("Disable warning notifications for missing optional plugins."); _uiShared.DrawHelpText("Disable warning notifications for missing optional plugins.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -4334,32 +4328,32 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
} }
private NotificationLocation[] GetLightlessNotificationLocations() private static NotificationLocation[] GetLightlessNotificationLocations()
{ {
return new[] return
{ [
NotificationLocation.LightlessUi, NotificationLocation.Chat, NotificationLocation.ChatAndLightlessUi, NotificationLocation.Nowhere NotificationLocation.LightlessUi, NotificationLocation.Chat, NotificationLocation.ChatAndLightlessUi, NotificationLocation.Nowhere
}; ];
} }
private NotificationLocation[] GetDownloadNotificationLocations() private static NotificationLocation[] GetDownloadNotificationLocations()
{ {
return new[] return
{ [
NotificationLocation.LightlessUi, NotificationLocation.TextOverlay, NotificationLocation.Nowhere NotificationLocation.LightlessUi, NotificationLocation.TextOverlay, NotificationLocation.Nowhere
}; ];
} }
private NotificationLocation[] GetClassicNotificationLocations() private static NotificationLocation[] GetClassicNotificationLocations()
{ {
return new[] return
{ [
NotificationLocation.Toast, NotificationLocation.Chat, NotificationLocation.Both, NotificationLocation.Toast, NotificationLocation.Chat, NotificationLocation.Both,
NotificationLocation.Nowhere NotificationLocation.Nowhere
}; ];
} }
private string GetNotificationLocationLabel(NotificationLocation location) private static string GetNotificationLocationLabel(NotificationLocation location)
{ {
return location switch return location switch
{ {
@@ -4374,7 +4368,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
}; };
} }
private string GetNotificationCornerLabel(NotificationCorner corner) private static string GetNotificationCornerLabel(NotificationCorner corner)
{ {
return corner switch return corner switch
{ {

View File

@@ -1,18 +1,20 @@
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Profiles;
using LightlessSync.WebAPI;
using LightlessSync.UI.Services; using LightlessSync.UI.Services;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using System.Globalization; using System.Globalization;
namespace LightlessSync.UI; namespace LightlessSync.UI;
@@ -23,12 +25,12 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
private readonly bool _isModerator = false; private readonly bool _isModerator = false;
private readonly bool _isOwner = false; private readonly bool _isOwner = false;
private readonly List<string> _oneTimeInvites = []; private readonly List<string> _oneTimeInvites = [];
private readonly PairUiService _pairUiService;
private readonly LightlessProfileManager _lightlessProfileManager; private readonly LightlessProfileManager _lightlessProfileManager;
private readonly FileDialogManager _fileDialogManager;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly PairUiService _pairUiService;
private List<BannedGroupUserDto> _bannedUsers = []; private List<BannedGroupUserDto> _bannedUsers = [];
private LightlessGroupProfileData? _profileData = null; private LightlessGroupProfileData? _profileData = null;
private IDalamudTextureWrap? _pfpTextureWrap;
private string _profileDescription = string.Empty; private string _profileDescription = string.Empty;
private int _multiInvites; private int _multiInvites;
private string _newPassword; private string _newPassword;
@@ -38,27 +40,34 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
private int _pruneDays = 14; private int _pruneDays = 14;
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, LightlessMediator mediator, ApiController apiController, public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, LightlessMediator mediator, ApiController apiController,
UiSharedService uiSharedService, PairUiService pairUiService, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager, FileDialogManager fileDialogManager) UiSharedService uiSharedService, PairUiService pairUiService, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager)
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService) : base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
{ {
GroupFullInfo = groupFullInfo; GroupFullInfo = groupFullInfo;
_apiController = apiController; _apiController = apiController;
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_pairUiService = pairUiService;
_lightlessProfileManager = lightlessProfileManager; _lightlessProfileManager = lightlessProfileManager;
_fileDialogManager = fileDialogManager; _pairUiService = pairUiService;
_isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal); _isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal);
_isModerator = GroupFullInfo.GroupUserInfo.IsModerator(); _isModerator = GroupFullInfo.GroupUserInfo.IsModerator();
_newPassword = string.Empty; _newPassword = string.Empty;
_multiInvites = 30; _multiInvites = 30;
_pwChangeSuccess = true; _pwChangeSuccess = true;
IsOpen = true; IsOpen = true;
Mediator.Subscribe<ClearProfileGroupDataMessage>(this, (msg) =>
{
if (msg.GroupData == null || string.Equals(msg.GroupData.AliasOrGID, GroupFullInfo.Group.AliasOrGID, StringComparison.Ordinal))
{
_pfpTextureWrap?.Dispose();
_pfpTextureWrap = null;
}
});
SizeConstraints = new WindowSizeConstraints() SizeConstraints = new WindowSizeConstraints()
{ {
MinimumSize = new(700, 500), MinimumSize = new(700, 500),
MaximumSize = new(700, 2000), MaximumSize = new(700, 2000),
}; };
_pairUiService = pairUiService;
} }
public GroupFullInfoDto GroupFullInfo { get; private set; } public GroupFullInfoDto GroupFullInfo { get; private set; }
@@ -84,7 +93,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var perm = GroupFullInfo.GroupPermissions; var perm = GroupFullInfo.GroupPermissions;
using var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID); using var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID);
if (tabbar) if (tabbar)
{ {
DrawInvites(perm); DrawInvites(perm);
@@ -92,7 +101,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
DrawManagement(); DrawManagement();
DrawPermission(perm); DrawPermission(perm);
DrawProfile(); DrawProfile();
} }
} }
@@ -193,6 +202,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
ownerTab.Dispose(); ownerTab.Dispose();
} }
} }
private void DrawProfile() private void DrawProfile()
{ {
var profileTab = ImRaii.TabItem("Profile"); var profileTab = ImRaii.TabItem("Profile");
@@ -220,7 +230,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
ImGui.BulletText(_profileData.IsDisabled ? "Profile disabled for viewers" : "Profile active"); ImGui.BulletText(_profileData.IsDisabled ? "Profile disabled for viewers" : "Profile active");
ImGuiHelpers.ScaledDummy(2f); ImGuiHelpers.ScaledDummy(2f);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGuiHelpers.ScaledDummy(2f); ImGuiHelpers.ScaledDummy(2f);
UiSharedService.TextWrapped("Open the syncshell profile editor to update images, description, tags, and visibility settings."); UiSharedService.TextWrapped("Open the syncshell profile editor to update images, description, tags, and visibility settings.");
@@ -395,7 +405,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
} }
} }
} }
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
ImGui.Separator(); ImGui.Separator();
@@ -486,7 +496,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
UiSharedService.TextWrapped($"Syncshell was pruned and {_pruneTask.Result} inactive user(s) have been removed."); UiSharedService.TextWrapped($"Syncshell was pruned and {_pruneTask.Result} inactive user(s) have been removed.");
} }
} }
_uiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
ImGui.Separator(); ImGui.Separator();
@@ -532,7 +542,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
} }
ImGui.EndTable(); ImGui.EndTable();
} }
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
ImGui.Separator(); ImGui.Separator();
@@ -584,8 +594,10 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
} }
inviteTab.Dispose(); inviteTab.Dispose();
} }
public override void OnClose() public override void OnClose()
{ {
Mediator.Publish(new RemoveWindowMessage(this)); Mediator.Publish(new RemoveWindowMessage(this));
_pfpTextureWrap?.Dispose();
} }
} }

View File

@@ -3,6 +3,7 @@ using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto; using LightlessSync.API.Dto;
@@ -29,11 +30,15 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
private readonly List<GroupJoinDto> _nearbySyncshells = []; private readonly List<GroupJoinDto> _nearbySyncshells = [];
private List<GroupFullInfoDto> _currentSyncshells = []; private List<GroupFullInfoDto> _currentSyncshells = [];
private int _selectedNearbyIndex = -1; private int _selectedNearbyIndex = -1;
private int _syncshellPageIndex = 0;
private readonly HashSet<string> _recentlyJoined = new(StringComparer.Ordinal); private readonly HashSet<string> _recentlyJoined = new(StringComparer.Ordinal);
private GroupJoinDto? _joinDto; private GroupJoinDto? _joinDto;
private GroupJoinInfoDto? _joinInfo; private GroupJoinInfoDto? _joinInfo;
private DefaultPermissionsDto _ownPermissions = null!; private DefaultPermissionsDto _ownPermissions = null!;
private const bool _useTestSyncshells = false;
private bool _compactView = false;
public SyncshellFinderUI( public SyncshellFinderUI(
ILogger<SyncshellFinderUI> logger, ILogger<SyncshellFinderUI> logger,
@@ -62,6 +67,8 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
Mediator.Subscribe<SyncshellBroadcastsUpdatedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); Mediator.Subscribe<SyncshellBroadcastsUpdatedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false));
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); Mediator.Subscribe<BroadcastStatusChangedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false));
Mediator.Subscribe<UserLeftSyncshell>(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false));
Mediator.Subscribe<UserJoinedSyncshell>(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false));
} }
public override async void OnOpen() public override async void OnOpen()
@@ -72,9 +79,21 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
protected override void DrawInternal() protected override void DrawInternal()
{ {
_uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("PairBlue")); ImGui.BeginGroup();
_uiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); _uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("LightlessPurple"));
ImGui.SameLine();
string checkboxLabel = "Compact view";
float availWidth = ImGui.GetContentRegionAvail().X;
float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight();
float rightX = ImGui.GetCursorPosX() + availWidth - checkboxWidth - 4.0f;
ImGui.SetCursorPosX(rightX);
ImGui.Checkbox(checkboxLabel, ref _compactView);
ImGui.EndGroup();
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
if (_nearbySyncshells.Count == 0) if (_nearbySyncshells.Count == 0)
{ {
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted."); ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted.");
@@ -82,13 +101,13 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
if (!_broadcastService.IsBroadcasting) if (!_broadcastService.IsBroadcasting)
{ {
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow")); UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"));
ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder is currently disabled, to locate nearby syncshells, Lightfinder must be active."); ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder is currently disabled, to locate nearby syncshells, Lightfinder must be active.");
ImGuiHelpers.ScaledDummy(0.5f); ImGuiHelpers.ScaledDummy(0.5f);
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f);
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("PairBlue")); ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessPurple"));
if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0))) if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0)))
{ {
@@ -104,106 +123,295 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
return; return;
} }
DrawSyncshellTable(); var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>();
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
foreach (var shell in _nearbySyncshells)
{
string broadcasterName;
if (_useTestSyncshells)
{
var displayName = !string.IsNullOrEmpty(shell.Group.Alias)
? shell.Group.Alias
: shell.Group.GID;
broadcasterName = $"Tester of {displayName}";
}
else
{
var broadcast = broadcasts
.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal));
if (broadcast == null)
continue;
var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID);
if (string.IsNullOrEmpty(name))
continue;
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address);
broadcasterName = !string.IsNullOrEmpty(worldName)
? $"{name} ({worldName})"
: name;
}
cardData.Add((shell, broadcasterName));
}
if (cardData.Count == 0)
{
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted.");
return;
}
if (_compactView)
{
DrawSyncshellGrid(cardData);
}
else
{
DrawSyncshellList(cardData);
}
if (_joinDto != null && _joinInfo != null && _joinInfo.Success) if (_joinDto != null && _joinInfo != null && _joinInfo.Success)
DrawConfirmation(); DrawConfirmation();
} }
private void DrawSyncshellTable() private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName)> listData)
{ {
if (ImGui.BeginTable("##NearbySyncshellsTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg)) const int shellsPerPage = 3;
var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage);
if (totalPages <= 0)
totalPages = 1;
_syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1);
var firstIndex = _syncshellPageIndex * shellsPerPage;
var lastExclusive = Math.Min(firstIndex + shellsPerPage, listData.Count);
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f);
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f);
for (int index = firstIndex; index < lastExclusive; index++)
{ {
ImGui.TableSetupColumn("Syncshell", ImGuiTableColumnFlags.WidthStretch); var (shell, broadcasterName) = listData[index];
ImGui.TableSetupColumn("Broadcaster", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Join", ImGuiTableColumnFlags.WidthFixed, 80f * ImGuiHelpers.GlobalScale);
ImGui.TableHeadersRow();
foreach (var shell in _nearbySyncshells) ImGui.PushID(shell.Group.GID);
{ float rowHeight = 90f * ImGuiHelpers.GlobalScale;
// Check if there is an active broadcast for this syncshell, if not, skipping this syncshell
var broadcast = _broadcastScannerService.GetActiveSyncshellBroadcasts()
.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal));
if (broadcast == null) ImGui.BeginChild($"ShellRow##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true);
continue; // no active broadcasts
var (Name, Address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID;
if (string.IsNullOrEmpty(Name))
continue; // broadcaster not found in area, skipping
ImGui.TableNextRow(); var style = ImGui.GetStyle();
ImGui.TableNextColumn(); float startX = ImGui.GetCursorPosX();
float regionW = ImGui.GetContentRegionAvail().X;
float rightTxtW = ImGui.CalcTextSize(broadcasterName).X;
var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; _uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
ImGui.TextUnformatted(displayName);
ImGui.TableNextColumn(); float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(Address); ImGui.SameLine();
var broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{Name} ({worldName})" : Name; ImGui.SetCursorPosX(rightX);
ImGui.TextUnformatted(broadcasterName); ImGui.TextUnformatted(broadcasterName);
ImGui.TableNextColumn(); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
var label = $"Join##{shell.Group.GID}"; ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale));
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f));
var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal)); DrawJoinButton(shell);
var isRecentlyJoined = _recentlyJoined.Contains(shell.GID);
if (!isAlreadyMember && !isRecentlyJoined)
{
if (ImGui.Button(label))
{
_logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})");
_ = Task.Run(async () => ImGui.EndChild();
{ ImGui.PopID();
try
{
var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto(
shell.Group,
shell.Password,
shell.GroupUserPreferredPermissions
)).ConfigureAwait(false);
if (info != null && info.Success) ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
{ }
_joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions);
_joinInfo = info;
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
_logger.LogInformation($"Fetched join info for {shell.Group.GID}"); ImGui.PopStyleVar(2);
}
else
{
_logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Join failed for {shell.Group.GID}");
}
});
}
}
else
{
using (ImRaii.Disabled())
{
ImGui.Button(label);
}
UiSharedService.AttachToolTip("Already a member or owner of this Syncshell.");
}
ImGui.PopStyleColor(3);
}
ImGui.EndTable(); DrawPagination(totalPages);
}
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName)> cardData)
{
const int shellsPerPage = 4;
var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage);
if (totalPages <= 0)
totalPages = 1;
_syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1);
var firstIndex = _syncshellPageIndex * shellsPerPage;
var lastExclusive = Math.Min(firstIndex + shellsPerPage, cardData.Count);
var avail = ImGui.GetContentRegionAvail();
var spacing = ImGui.GetStyle().ItemSpacing;
var cardWidth = (avail.X - spacing.X) / 2.0f;
var cardHeight = (avail.Y - spacing.Y - (ImGui.GetFrameHeightWithSpacing() * 2.0f)) / 2.0f;
cardHeight = MathF.Max(110f * ImGuiHelpers.GlobalScale, cardHeight);
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f);
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f);
for (int index = firstIndex; index < lastExclusive; index++)
{
var localIndex = index - firstIndex;
var (shell, broadcasterName) = cardData[index];
if (localIndex % 2 != 0)
ImGui.SameLine();
ImGui.PushID(shell.Group.GID);
ImGui.BeginGroup();
_ = ImGui.BeginChild("ShellCard##" + shell.Group.GID, new Vector2(cardWidth, cardHeight), border: true);
var displayName = !string.IsNullOrEmpty(shell.Group.Alias)
? shell.Group.Alias
: shell.Group.GID;
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
ImGui.TextColored(ImGuiColors.DalamudGrey, "Broadcaster");
ImGui.TextUnformatted(broadcasterName);
ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale));
var buttonHeight = ImGui.GetFrameHeightWithSpacing();
var remainingY = ImGui.GetContentRegionAvail().Y - buttonHeight;
if (remainingY > 0)
ImGui.Dummy(new Vector2(0, remainingY));
DrawJoinButton(shell);
ImGui.EndChild();
ImGui.EndGroup();
ImGui.PopID();
}
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
ImGui.PopStyleVar(2);
DrawPagination(totalPages);
}
private void DrawPagination(int totalPages)
{
if (totalPages > 1)
{
UiSharedService.ColoredSeparator(UIColors.Get("PairBlue"));
var style = ImGui.GetStyle();
string pageLabel = $"Page {_syncshellPageIndex + 1}/{totalPages}";
float prevWidth = ImGui.CalcTextSize("<").X + style.FramePadding.X * 2;
float nextWidth = ImGui.CalcTextSize(">").X + style.FramePadding.X * 2;
float textWidth = ImGui.CalcTextSize(pageLabel).X;
float totalWidth = prevWidth + textWidth + nextWidth + style.ItemSpacing.X * 2;
float availWidth = ImGui.GetContentRegionAvail().X;
float offsetX = (availWidth - totalWidth) * 0.5f;
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offsetX);
if (ImGui.Button("<##PrevSyncshellPage") && _syncshellPageIndex > 0)
_syncshellPageIndex--;
ImGui.SameLine();
ImGui.Text(pageLabel);
ImGui.SameLine();
if (ImGui.Button(">##NextSyncshellPage") && _syncshellPageIndex < totalPages - 1)
_syncshellPageIndex++;
} }
} }
private void DrawJoinButton(dynamic shell)
{
const string visibleLabel = "Join";
var label = $"{visibleLabel}##{shell.Group.GID}";
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f));
var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal));
var isRecentlyJoined = _recentlyJoined.Contains(shell.GID);
Vector2 buttonSize;
if (!_compactView)
{
var style = ImGui.GetStyle();
var textSize = ImGui.CalcTextSize(visibleLabel);
var width = textSize.X + style.FramePadding.X * 20f;
buttonSize = new Vector2(width, 0);
float availX = ImGui.GetContentRegionAvail().X;
float curX = ImGui.GetCursorPosX();
float newX = curX + (availX - buttonSize.X);
ImGui.SetCursorPosX(newX);
}
else
{
buttonSize = new Vector2(-1, 0);
}
if (!isAlreadyMember && !isRecentlyJoined)
{
if (ImGui.Button(label, buttonSize))
{
_logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})");
_ = Task.Run(async () =>
{
try
{
var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto(
shell.Group,
shell.Password,
shell.GroupUserPreferredPermissions
)).ConfigureAwait(false);
if (info != null && info.Success)
{
_joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions);
_joinInfo = info;
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
_logger.LogInformation($"Fetched join info for {shell.Group.GID}");
}
else
{
_logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Join failed for {shell.Group.GID}");
}
});
}
}
else
{
using (ImRaii.Disabled())
{
ImGui.Button(label, buttonSize);
}
UiSharedService.AttachToolTip("Already a member or owner of this Syncshell.");
}
ImGui.PopStyleColor(3);
}
private void DrawConfirmation() private void DrawConfirmation()
{ {
if (_joinDto != null && _joinInfo != null) if (_joinDto != null && _joinInfo != null)
@@ -263,53 +471,97 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
ImGui.NewLine(); ImGui.NewLine();
} }
private async Task RefreshSyncshellsAsync() private async Task RefreshSyncshellsAsync(string? gid = null)
{ {
var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
var snapshot = _pairUiService.GetSnapshot(); var snapshot = _pairUiService.GetSnapshot();
_currentSyncshells = snapshot.GroupPairs.Keys.ToList(); _currentSyncshells = [.. snapshot.GroupPairs.Keys];
_recentlyJoined.RemoveWhere(gid => _currentSyncshells.Any(s => string.Equals(s.GID, gid, StringComparison.Ordinal)));
if (syncshellBroadcasts.Count == 0) _recentlyJoined.RemoveWhere(gid =>
_currentSyncshells.Exists(s => string.Equals(s.GID, gid, StringComparison.Ordinal)));
List<GroupJoinDto>? updatedList = [];
if (_useTestSyncshells)
{
updatedList = BuildTestSyncshells();
}
else
{
if (syncshellBroadcasts.Count == 0)
{
ClearSyncshells();
return;
}
try
{
var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts)
.ConfigureAwait(false);
updatedList = groups?.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to refresh broadcasted syncshells.");
return;
}
}
if (updatedList == null || updatedList.Count == 0)
{ {
ClearSyncshells(); ClearSyncshells();
return; return;
} }
List<GroupJoinDto>? updatedList = []; if (gid != null && _recentlyJoined.Contains(gid))
try
{ {
var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts).ConfigureAwait(false); _recentlyJoined.Clear();
updatedList = groups?.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to refresh broadcasted syncshells.");
return;
} }
if (updatedList != null) var previousGid = GetSelectedGid();
_nearbySyncshells.Clear();
_nearbySyncshells.AddRange(updatedList);
if (previousGid != null)
{ {
var previousGid = GetSelectedGid(); var newIndex = _nearbySyncshells.FindIndex(s =>
string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal));
_nearbySyncshells.Clear(); if (newIndex >= 0)
_nearbySyncshells.AddRange(updatedList);
if (previousGid != null)
{ {
var newIndex = _nearbySyncshells.FindIndex(s => string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal)); _selectedNearbyIndex = newIndex;
if (newIndex >= 0) return;
{
_selectedNearbyIndex = newIndex;
return;
}
} }
} }
ClearSelection(); ClearSelection();
} }
private List<GroupJoinDto> BuildTestSyncshells()
{
var testGroup1 = new GroupData("TEST-ALPHA", "Alpha Shell");
var testGroup2 = new GroupData("TEST-BETA", "Beta Shell");
var testGroup3 = new GroupData("TEST-GAMMA", "Gamma Shell");
var testGroup4 = new GroupData("TEST-DELTA", "Delta Shell");
var testGroup5 = new GroupData("TEST-CHARLIE", "Charlie Shell");
var testGroup6 = new GroupData("TEST-OMEGA", "Omega Shell");
var testGroup7 = new GroupData("TEST-POINT", "Point Shell");
var testGroup8 = new GroupData("TEST-HOTEL", "Hotel Shell");
return
[
new(testGroup1, "", GroupUserPreferredPermissions.NoneSet),
new(testGroup2, "", GroupUserPreferredPermissions.NoneSet),
new(testGroup3, "", GroupUserPreferredPermissions.NoneSet),
new(testGroup4, "", GroupUserPreferredPermissions.NoneSet),
new(testGroup5, "", GroupUserPreferredPermissions.NoneSet),
new(testGroup6, "", GroupUserPreferredPermissions.NoneSet),
new(testGroup7, "", GroupUserPreferredPermissions.NoneSet),
new(testGroup8, "", GroupUserPreferredPermissions.NoneSet),
];
}
private void ClearSyncshells() private void ClearSyncshells()
{ {
if (_nearbySyncshells.Count == 0) if (_nearbySyncshells.Count == 0)
@@ -322,6 +574,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
private void ClearSelection() private void ClearSelection()
{ {
_selectedNearbyIndex = -1; _selectedNearbyIndex = -1;
_syncshellPageIndex = 0;
_joinDto = null; _joinDto = null;
_joinInfo = null; _joinInfo = null;
} }

View File

@@ -123,133 +123,133 @@ public class TopTabMenu
} }
UiSharedService.AttachToolTip("Individual Pair Menu"); UiSharedService.AttachToolTip("Individual Pair Menu");
using (ImRaii.PushFont(UiBuilder.IconFont)) using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Users.ToIconString(), buttonSize))
{ {
TabSelection = TabSelection == SelectedTab.Syncshell ? SelectedTab.None : SelectedTab.Syncshell; var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Users.ToIconString(), buttonSize))
{
TabSelection = TabSelection == SelectedTab.Syncshell ? SelectedTab.None : SelectedTab.Syncshell;
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos();
if (TabSelection == SelectedTab.Syncshell)
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y },
xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
underlineColor, 2);
} }
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive()) UiSharedService.AttachToolTip("Syncshell Menu");
using (ImRaii.PushFont(UiBuilder.IconFont))
{ {
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding); if (ImGui.Button(FontAwesomeIcon.Comments.ToIconString(), buttonSize))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(ZoneChatUi)));
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
} }
UiSharedService.AttachToolTip("Zone Chat");
ImGui.SameLine(); ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos();
if (TabSelection == SelectedTab.Syncshell)
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y },
xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
underlineColor, 2);
}
UiSharedService.AttachToolTip("Syncshell Menu");
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button(FontAwesomeIcon.Comments.ToIconString(), buttonSize))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(ZoneChatUi)));
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
}
UiSharedService.AttachToolTip("Zone Chat");
ImGui.SameLine();
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize))
{
TabSelection = TabSelection == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder;
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
ImGui.SameLine(); ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos(); using (ImRaii.PushFont(UiBuilder.IconFont))
if (TabSelection == SelectedTab.Lightfinder) {
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, var x = ImGui.GetCursorScreenPos();
xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize))
underlineColor, 2); {
} TabSelection = TabSelection == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder;
UiSharedService.AttachToolTip("Lightfinder"); }
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
ImGui.SameLine(); ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont)) var xAfter = ImGui.GetCursorScreenPos();
{ if (TabSelection == SelectedTab.Lightfinder)
var x = ImGui.GetCursorScreenPos(); drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y },
if (ImGui.Button(FontAwesomeIcon.UserCog.ToIconString(), buttonSize)) xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
{ underlineColor, 2);
TabSelection = TabSelection == SelectedTab.UserConfig ? SelectedTab.None : SelectedTab.UserConfig;
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
} }
UiSharedService.AttachToolTip("Lightfinder");
ImGui.SameLine(); ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos(); using (ImRaii.PushFont(UiBuilder.IconFont))
if (TabSelection == SelectedTab.UserConfig) {
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, var x = ImGui.GetCursorScreenPos();
xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, if (ImGui.Button(FontAwesomeIcon.UserCog.ToIconString(), buttonSize))
underlineColor, 2); {
} TabSelection = TabSelection == SelectedTab.UserConfig ? SelectedTab.None : SelectedTab.UserConfig;
UiSharedService.AttachToolTip("Your User Menu"); }
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
ImGui.SameLine(); ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont)) var xAfter = ImGui.GetCursorScreenPos();
{ if (TabSelection == SelectedTab.UserConfig)
var x = ImGui.GetCursorScreenPos(); drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y },
if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize)) xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
{ underlineColor, 2);
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
} }
UiSharedService.AttachToolTip("Your User Menu");
ImGui.SameLine(); ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
ImGui.SameLine();
}
UiSharedService.AttachToolTip("Open Lightless Settings");
ImGui.NewLine();
btncolor.Dispose();
ImGuiHelpers.ScaledDummy(spacing);
if (TabSelection == SelectedTab.Individual)
{
DrawAddPair(availableWidth, spacing.X);
DrawGlobalIndividualButtons(availableWidth, spacing.X);
}
else if (TabSelection == SelectedTab.Syncshell)
{
DrawSyncshellMenu(availableWidth, spacing.X);
DrawGlobalSyncshellButtons(availableWidth, spacing.X);
}
else if (TabSelection == SelectedTab.Lightfinder)
{
DrawLightfinderMenu(availableWidth, spacing.X);
}
else if (TabSelection == SelectedTab.UserConfig)
{
DrawUserConfig(availableWidth, spacing.X);
}
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
DrawIncomingPairRequests(availableWidth);
ImGui.Separator();
DrawFilter(availableWidth, spacing.X);
} }
UiSharedService.AttachToolTip("Open Lightless Settings");
ImGui.NewLine();
btncolor.Dispose();
ImGuiHelpers.ScaledDummy(spacing);
if (TabSelection == SelectedTab.Individual)
{
DrawAddPair(availableWidth, spacing.X);
DrawGlobalIndividualButtons(availableWidth, spacing.X);
}
else if (TabSelection == SelectedTab.Syncshell)
{
DrawSyncshellMenu(availableWidth, spacing.X);
DrawGlobalSyncshellButtons(availableWidth, spacing.X);
}
else if (TabSelection == SelectedTab.Lightfinder)
{
DrawLightfinderMenu(availableWidth, spacing.X);
}
else if (TabSelection == SelectedTab.UserConfig)
{
DrawUserConfig(availableWidth, spacing.X);
}
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
DrawIncomingPairRequests(availableWidth);
ImGui.Separator();
DrawFilter(availableWidth, spacing.X);
}
finally finally
{ {
_currentSnapshot = null; _currentSnapshot = null;

View File

@@ -45,7 +45,7 @@ namespace LightlessSync.UI
return HexToRgba(customColorHex); return HexToRgba(customColorHex);
if (!DefaultHexColors.TryGetValue(name, out var hex)) if (!DefaultHexColors.TryGetValue(name, out var hex))
throw new ArgumentException($"Color '{name}' not found in UIColors."); throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
return HexToRgba(hex); return HexToRgba(hex);
} }
@@ -53,7 +53,7 @@ namespace LightlessSync.UI
public static void Set(string name, Vector4 color) public static void Set(string name, Vector4 color)
{ {
if (!DefaultHexColors.ContainsKey(name)) if (!DefaultHexColors.ContainsKey(name))
throw new ArgumentException($"Color '{name}' not found in UIColors."); throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
if (_configService != null) if (_configService != null)
{ {
@@ -83,7 +83,7 @@ namespace LightlessSync.UI
public static Vector4 GetDefault(string name) public static Vector4 GetDefault(string name)
{ {
if (!DefaultHexColors.TryGetValue(name, out var hex)) if (!DefaultHexColors.TryGetValue(name, out var hex))
throw new ArgumentException($"Color '{name}' not found in UIColors."); throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
return HexToRgba(hex); return HexToRgba(hex);
} }
@@ -101,10 +101,10 @@ namespace LightlessSync.UI
public static Vector4 HexToRgba(string hexColor) public static Vector4 HexToRgba(string hexColor)
{ {
hexColor = hexColor.TrimStart('#'); hexColor = hexColor.TrimStart('#');
int r = int.Parse(hexColor.Substring(0, 2), NumberStyles.HexNumber); int r = int.Parse(hexColor[..2], NumberStyles.HexNumber);
int g = int.Parse(hexColor.Substring(2, 2), NumberStyles.HexNumber); int g = int.Parse(hexColor[2..4], NumberStyles.HexNumber);
int b = int.Parse(hexColor.Substring(4, 2), NumberStyles.HexNumber); int b = int.Parse(hexColor[4..6], NumberStyles.HexNumber);
int a = hexColor.Length == 8 ? int.Parse(hexColor.Substring(6, 2), NumberStyles.HexNumber) : 255; int a = hexColor.Length == 8 ? int.Parse(hexColor[6..8], NumberStyles.HexNumber) : 255;
return new Vector4(r / 255f, g / 255f, b / 255f, a / 255f); return new Vector4(r / 255f, g / 255f, b / 255f, a / 255f);
} }

View File

@@ -71,7 +71,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
private bool _isOneDrive = false; private bool _isOneDrive = false;
private bool _isPenumbraDirectory = false; private bool _isPenumbraDirectory = false;
private bool _moodlesExists = false; private bool _moodlesExists = false;
private Dictionary<string, DateTime> _oauthTokenExpiry = new(); private readonly Dictionary<string, DateTime> _oauthTokenExpiry = [];
private bool _penumbraExists = false; private bool _penumbraExists = false;
private bool _petNamesExists = false; private bool _petNamesExists = false;
private int _serverSelectionIndex = -1; private int _serverSelectionIndex = -1;
@@ -487,7 +487,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
); );
} }
public void ColoredSeparator(Vector4? color = null, float thickness = 1f, float indent = 0f) public static void ColoredSeparator(Vector4? color = null, float thickness = 1f, float indent = 0f)
{ {
var drawList = ImGui.GetWindowDrawList(); var drawList = ImGui.GetWindowDrawList();
var min = ImGui.GetCursorScreenPos(); var min = ImGui.GetCursorScreenPos();
@@ -1080,7 +1080,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
{ {
using (ImRaii.Disabled(_discordOAuthUIDs == null)) using (ImRaii.Disabled(_discordOAuthUIDs == null))
{ {
var aliasPairs = _discordOAuthUIDs?.Result?.Select(t => new UIDAliasPair(t.Key, t.Value)).ToList() ?? [new UIDAliasPair(item.UID ?? null, null)]; var aliasPairs = _discordOAuthUIDs?.Result?.Select(t => new UidAliasPair(t.Key, t.Value)).ToList() ?? [new UidAliasPair(item.UID ?? null, null)];
var uidComboName = "UID###" + item.CharacterName + item.WorldId + serverUri + indexOffset + aliasPairs.Count; var uidComboName = "UID###" + item.CharacterName + item.WorldId + serverUri + indexOffset + aliasPairs.Count;
DrawCombo(uidComboName, aliasPairs, DrawCombo(uidComboName, aliasPairs,
(v) => (v) =>
@@ -1360,6 +1360,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
UidFont.Dispose(); UidFont.Dispose();
GameFont.Dispose(); GameFont.Dispose();
MediumFont.Dispose(); MediumFont.Dispose();
_discordOAuthGetCts.Dispose();
} }
private static void CenterWindow(float width, float height, ImGuiCond cond = ImGuiCond.None) private static void CenterWindow(float width, float height, ImGuiCond cond = ImGuiCond.None)
@@ -1443,6 +1444,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
return result; return result;
} }
public sealed record IconScaleData(Vector2 IconSize, Vector2 NormalizedIconScale, float OffsetX, float IconScaling); public sealed record IconScaleData(Vector2 IconSize, Vector2 NormalizedIconScale, float OffsetX, float IconScaling);
private record UIDAliasPair(string? UID, string? Alias); private sealed record UidAliasPair(string? UID, string? Alias);
} }

View File

@@ -25,7 +25,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
private ChangelogFile _changelog = new(); private ChangelogFile _changelog = new();
private CreditsFile _credits = new(); private CreditsFile _credits = new();
private bool _scrollToTop; private bool _scrollToTop;
private int _selectedTab;
private bool _hasInitializedCollapsingHeaders; private bool _hasInitializedCollapsingHeaders;
private struct Particle private struct Particle
@@ -160,7 +159,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
DrawParticleEffects(headerStart, extendedParticleSize); DrawParticleEffects(headerStart, extendedParticleSize);
} }
private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd) private static void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd)
{ {
var drawList = ImGui.GetWindowDrawList(); var drawList = ImGui.GetWindowDrawList();
@@ -188,7 +187,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
} }
} }
private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width) private static void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width)
{ {
var drawList = ImGui.GetWindowDrawList(); var drawList = ImGui.GetWindowDrawList();
var gradientHeight = 60f; var gradientHeight = 60f;
@@ -513,7 +512,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
{ {
if (changelogTab) if (changelogTab)
{ {
_selectedTab = 0;
DrawChangelog(); DrawChangelog();
} }
} }
@@ -524,7 +522,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
{ {
if (creditsTab) if (creditsTab)
{ {
_selectedTab = 1;
DrawCredits(); DrawCredits();
} }
} }
@@ -558,7 +555,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
} }
} }
private void DrawCreditCategory(CreditCategory category) private static void DrawCreditCategory(CreditCategory category)
{ {
DrawFeatureSection(category.Category, UIColors.Get("LightlessBlue")); DrawFeatureSection(category.Category, UIColors.Get("LightlessBlue"));
@@ -745,7 +742,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
using var changelogStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.changelog.yaml"); using var changelogStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.changelog.yaml");
if (changelogStream != null) if (changelogStream != null)
{ {
using var reader = new StreamReader(changelogStream, Encoding.UTF8, true, 128); using var reader = new StreamReader(changelogStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, 128);
var yaml = reader.ReadToEnd(); var yaml = reader.ReadToEnd();
_changelog = deserializer.Deserialize<ChangelogFile>(yaml) ?? new(); _changelog = deserializer.Deserialize<ChangelogFile>(yaml) ?? new();
} }
@@ -754,7 +751,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
using var creditsStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.credits.yaml"); using var creditsStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.credits.yaml");
if (creditsStream != null) if (creditsStream != null)
{ {
using var reader = new StreamReader(creditsStream, Encoding.UTF8, true, 128); using var reader = new StreamReader(creditsStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, 128);
var yaml = reader.ReadToEnd(); var yaml = reader.ReadToEnd();
_credits = deserializer.Deserialize<CreditsFile>(yaml) ?? new(); _credits = deserializer.Deserialize<CreditsFile>(yaml) ?? new();
} }

View File

@@ -1,3 +1,4 @@
using Blake3;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.IO; using System.IO;
@@ -16,14 +17,88 @@ public static class Crypto
private static readonly ConcurrentDictionary<string, string> _hashListSHA256 = new(StringComparer.Ordinal); private static readonly ConcurrentDictionary<string, string> _hashListSHA256 = new(StringComparer.Ordinal);
private static readonly SHA256CryptoServiceProvider _sha256CryptoProvider = new(); private static readonly SHA256CryptoServiceProvider _sha256CryptoProvider = new();
public static string GetFileHash(this string filePath) // BLAKE3 hash caches
private static readonly Dictionary<(string, ushort), string> _hashListPlayersBlake3 = [];
private static readonly Dictionary<string, string> _hashListBlake3 = new(StringComparer.Ordinal);
/// <summary>
/// Supports Blake3 or SHA1 for file transfers, no SHA256 supported on it
/// </summary>
public enum HashAlgo
{ {
using SHA1 sha1 = SHA1.Create(); Blake3,
using FileStream stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); Sha1
return BitConverter.ToString(sha1.ComputeHash(stream)).Replace("-", "", StringComparison.Ordinal);
} }
public static async Task<string> GetFileHashAsync(string filePath, CancellationToken cancellationToken = default) /// <summary>
/// Detects which algo is being used for the file
/// </summary>
/// <param name="hashHex">Hashed string</param>
/// <returns>HashAlgo</returns>
public static HashAlgo DetectAlgo(string hashHex)
{
if (hashHex.Length == 40)
return HashAlgo.Sha1;
return HashAlgo.Blake3;
}
#region File Hashing
/// <summary>
/// Compute file hash with given algorithm, supports BLAKE3 and Sha1 for file hashing
/// </summary>
/// <param name="filePath">Filepath for the hashing</param>
/// <param name="algo">BLAKE3 or Sha1</param>
/// <returns>Hashed file hash</returns>
/// <exception cref="ArgumentOutOfRangeException">Not a valid HashAlgo or Filepath</exception>
public static string ComputeFileHash(string filePath, HashAlgo algo)
{
return algo switch
{
HashAlgo.Blake3 => ComputeFileHashBlake3(filePath),
HashAlgo.Sha1 => ComputeFileHashSha1(filePath),
_ => throw new ArgumentOutOfRangeException(nameof(algo), algo, null)
};
}
/// <summary>
/// Compute file hash asynchronously with given algorithm, supports BLAKE3 and SHA1 for file hashing
/// </summary>
/// <param name="filePath">Filepath for the hashing</param>
/// <param name="algo">BLAKE3 or Sha1</param>
/// <returns>Hashed file hash</returns>
/// <exception cref="ArgumentOutOfRangeException">Not a valid HashAlgo or Filepath</exception>
public static async Task<string> ComputeFileHashAsync(string filePath, HashAlgo algo, CancellationToken cancellationToken = default)
{
return algo switch
{
HashAlgo.Blake3 => await ComputeFileHashBlake3Async(filePath, cancellationToken).ConfigureAwait(false),
HashAlgo.Sha1 => await ComputeFileHashSha1Async(filePath, cancellationToken).ConfigureAwait(false),
_ => throw new ArgumentOutOfRangeException(nameof(algo), algo, message: null)
};
}
/// <summary>
/// Computes an file hash with SHA1
/// </summary>
/// <param name="filePath">Filepath that has to be computed</param>
/// <returns>Hashed file in hex string</returns>
private static string ComputeFileHashSha1(string filePath)
{
using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
using var sha1 = SHA1.Create();
var hash = sha1.ComputeHash(stream);
return Convert.ToHexString(hash);
}
/// <summary>
/// Computes an file hash with SHA1 asynchronously
/// </summary>
/// <param name="filePath">Filepath that has to be computed</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Hashed file in hex string hashed in SHA1</returns>
private static async Task<string> ComputeFileHashSha1Async(string filePath, CancellationToken cancellationToken)
{ {
var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, bufferSize: _bufferSize, options: FileOptions.Asynchronous); var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, bufferSize: _bufferSize, options: FileOptions.Asynchronous);
await using (stream.ConfigureAwait(false)) await using (stream.ConfigureAwait(false))
@@ -32,22 +107,121 @@ public static class Crypto
var buffer = new byte[8192]; var buffer = new byte[8192];
int bytesRead; int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0) while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
{ {
sha1.TransformBlock(buffer, 0, bytesRead, outputBuffer: null, 0); sha1.TransformBlock(buffer, 0, bytesRead, outputBuffer: null, 0);
} }
sha1.TransformFinalBlock([], 0, 0); sha1.TransformFinalBlock([], 0, 0);
return Convert.ToHexString(sha1.Hash!); return Convert.ToHexString(sha1.Hash!);
} }
} }
public static string GetHash256(this (string, ushort) playerToHash) /// <summary>
/// Computes an file hash with Blake3
/// </summary>
/// <param name="filePath">Filepath that has to be computed</param>
/// <returns>Hashed file in hex string hashed in Blake3</returns>
private static string ComputeFileHashBlake3(string filePath)
{ {
return _hashListPlayersSHA256.GetOrAdd(playerToHash, key => ComputeHashSHA256(key.Item1 + key.Item2.ToString())); using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
using var hasher = Hasher.New();
var buffer = new byte[_bufferSize];
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
hasher.Update(buffer.AsSpan(0, bytesRead));
}
var hash = hasher.Finalize();
return hash.ToString();
} }
/// <summary>
/// Computes an file hash with Blake3 asynchronously
/// </summary>
/// <param name="filePath">Filepath that has to be computed</param>
/// <returns>Hashed file in hex string hashed in Blake3</returns>
private static async Task<string> ComputeFileHashBlake3Async(string filePath, CancellationToken cancellationToken)
{
var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, bufferSize: _bufferSize, options: FileOptions.Asynchronous);
await using (stream.ConfigureAwait(false))
{
using var hasher = Hasher.New();
var buffer = new byte[8192];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
{
hasher.Update(buffer.AsSpan(0, bytesRead));
}
var hash = hasher.Finalize();
return hash.ToString();
}
}
#endregion
#region String hashing
public static string GetBlake3Hash(this (string, ushort) playerToHash)
{
if (_hashListPlayersBlake3.TryGetValue(playerToHash, out var hash))
return hash;
var toHash = playerToHash.Item1 + playerToHash.Item2.ToString();
hash = ComputeBlake3Hex(toHash);
_hashListPlayersBlake3[playerToHash] = hash;
return hash;
}
/// <summary>
/// Computes or gets an Blake3 hash(ed) string.
/// </summary>
/// <param name="stringToHash">String that needs to be hashsed</param>
/// <returns>Hashed string</returns>
public static string GetBlake3Hash(this string stringToHash)
{
return GetOrComputeBlake3(stringToHash);
}
private static string GetOrComputeBlake3(string stringToCompute)
{
if (_hashListBlake3.TryGetValue(stringToCompute, out var hash))
return hash;
hash = ComputeBlake3Hex(stringToCompute);
_hashListBlake3[stringToCompute] = hash;
return hash;
}
private static string ComputeBlake3Hex(string input)
{
var bytes = Encoding.UTF8.GetBytes(input);
var hash = Hasher.Hash(bytes);
return Convert.ToHexString(hash.AsSpan());
}
public static string GetHash256(this (string, ushort) playerToHash)
{
if (_hashListPlayersSHA256.TryGetValue(playerToHash, out var hash))
return hash;
return _hashListPlayersSHA256[playerToHash] =
Convert.ToHexString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(playerToHash.Item1 + playerToHash.Item2.ToString())));
}
/// <summary>
/// Computes or gets an SHA256 hash(ed) string.
/// </summary>
/// <param name="stringToHash">String that needs to be hashsed</param>
/// <returns>Hashed string</returns>
public static string GetHash256(this string stringToHash) public static string GetHash256(this string stringToHash)
{ {
return _hashListSHA256.GetOrAdd(stringToHash, ComputeHashSHA256); return _hashListSHA256.GetOrAdd(stringToHash, ComputeHashSHA256);
@@ -55,8 +229,13 @@ public static class Crypto
private static string ComputeHashSHA256(string stringToCompute) private static string ComputeHashSHA256(string stringToCompute)
{ {
using var sha = SHA256.Create(); if (_hashListSHA256.TryGetValue(stringToCompute, out var hash))
return BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(stringToCompute))).Replace("-", "", StringComparison.Ordinal); return hash;
}
return _hashListSHA256[stringToCompute] =
Convert.ToHexString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToCompute)));
}
#endregion
#pragma warning restore SYSLIB0021 // Type or member is obsolete #pragma warning restore SYSLIB0021 // Type or member is obsolete
} }

View File

@@ -1,6 +1,5 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace LightlessSync.Utils namespace LightlessSync.Utils
@@ -157,7 +156,7 @@ namespace LightlessSync.Utils
return mountOptions; return mountOptions;
} }
catch (Exception ex) catch (Exception)
{ {
return string.Empty; return string.Empty;
} }
@@ -234,40 +233,6 @@ namespace LightlessSync.Utils
return clusterSize; return clusterSize;
} }
string realPath = fi.FullName;
if (isWine && realPath.StartsWith("Z:\\", StringComparison.OrdinalIgnoreCase))
{
realPath = "/" + realPath.Substring(3).Replace('\\', '/');
}
var psi = new ProcessStartInfo
{
FileName = "/bin/bash",
Arguments = $"-c \"stat -f -c %s '{realPath.Replace("'", "'\\''")}'\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = "/"
};
using var proc = Process.Start(psi);
string stdout = proc?.StandardOutput.ReadToEnd().Trim() ?? "";
string _stderr = proc?.StandardError.ReadToEnd() ?? "";
try { proc?.WaitForExit(); }
catch (Exception ex) { logger?.LogTrace(ex, "stat WaitForExit failed under Wine; ignoring"); }
if (!(!int.TryParse(stdout, out int block) || block <= 0))
{
_blockSizeCache[root] = block;
logger?.LogTrace("Filesystem block size via stat for {root}: {block}", root, block);
return block;
}
logger?.LogTrace("stat did not return valid block size for {file}, output: {out}", fi.FullName, stdout);
_blockSizeCache[root] = _defaultBlockSize;
return _defaultBlockSize; return _defaultBlockSize;
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -497,10 +497,9 @@ public static class SeStringUtils
continue; continue;
var hasColor = fragment.Color.HasValue; var hasColor = fragment.Color.HasValue;
Vector4 color = default;
if (hasColor) if (hasColor)
{ {
color = fragment.Color!.Value; Vector4 color = fragment.Color!.Value;
builder.PushColorRgba(color); builder.PushColorRgba(color);
} }
@@ -673,7 +672,7 @@ public static class SeStringUtils
protected abstract byte ChunkType { get; } protected abstract byte ChunkType { get; }
} }
private class ColorPayload : AbstractColorPayload private sealed class ColorPayload : AbstractColorPayload
{ {
protected override byte ChunkType => 0x13; protected override byte ChunkType => 0x13;
@@ -687,12 +686,12 @@ public static class SeStringUtils
public ColorPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { } public ColorPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { }
} }
private class ColorEndPayload : AbstractColorEndPayload private sealed class ColorEndPayload : AbstractColorEndPayload
{ {
protected override byte ChunkType => 0x13; protected override byte ChunkType => 0x13;
} }
private class GlowPayload : AbstractColorPayload private sealed class GlowPayload : AbstractColorPayload
{ {
protected override byte ChunkType => 0x14; protected override byte ChunkType => 0x14;
@@ -706,7 +705,7 @@ public static class SeStringUtils
public GlowPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { } public GlowPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { }
} }
private class GlowEndPayload : AbstractColorEndPayload private sealed class GlowEndPayload : AbstractColorEndPayload
{ {
protected override byte ChunkType => 0x14; protected override byte ChunkType => 0x14;
} }

View File

@@ -319,8 +319,9 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
bytesRead = await readTask.ConfigureAwait(false); bytesRead = await readTask.ConfigureAwait(false);
} }
catch (OperationCanceledException) catch (OperationCanceledException ex)
{ {
Logger.LogWarning(ex, "Request got cancelled : {url}", requestUrl);
throw; throw;
} }

View File

@@ -190,7 +190,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Logger.LogInformation("Not recreating Connection, paused"); Logger.LogInformation("Not recreating Connection, paused");
_connectionDto = null; _connectionDto = null;
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false); await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel(); if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return; return;
} }
@@ -204,7 +207,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Mediator.Publish(new NotificationMessage("Multiple Identical Characters detected", "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect to Lightless.", Mediator.Publish(new NotificationMessage("Multiple Identical Characters detected", "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect to Lightless.",
NotificationType.Error)); NotificationType.Error));
await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false); await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel(); if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return; return;
} }
@@ -213,7 +219,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Logger.LogWarning("No secret key set for current character"); Logger.LogWarning("No secret key set for current character");
_connectionDto = null; _connectionDto = null;
await StopConnectionAsync(ServerState.NoSecretKey).ConfigureAwait(false); await StopConnectionAsync(ServerState.NoSecretKey).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel(); if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return; return;
} }
} }
@@ -227,7 +236,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Mediator.Publish(new NotificationMessage("Multiple Identical Characters detected", "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect to Lightless.", Mediator.Publish(new NotificationMessage("Multiple Identical Characters detected", "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect to Lightless.",
NotificationType.Error)); NotificationType.Error));
await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false); await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel(); if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return; return;
} }
@@ -236,7 +248,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Logger.LogWarning("No UID/OAuth set for current character"); Logger.LogWarning("No UID/OAuth set for current character");
_connectionDto = null; _connectionDto = null;
await StopConnectionAsync(ServerState.OAuthMisconfigured).ConfigureAwait(false); await StopConnectionAsync(ServerState.OAuthMisconfigured).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel(); if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return; return;
} }
@@ -245,7 +260,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Logger.LogWarning("OAuth2 login token could not be updated"); Logger.LogWarning("OAuth2 login token could not be updated");
_connectionDto = null; _connectionDto = null;
await StopConnectionAsync(ServerState.OAuthLoginTokenStale).ConfigureAwait(false); await StopConnectionAsync(ServerState.OAuthLoginTokenStale).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel(); if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return; return;
} }
} }
@@ -256,7 +274,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Informational, Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Informational,
$"Starting Connection to {_serverManager.CurrentServer.ServerName}"))); $"Starting Connection to {_serverManager.CurrentServer.ServerName}")));
_connectionCancellationTokenSource?.Cancel(); if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
_connectionCancellationTokenSource?.Dispose(); _connectionCancellationTokenSource?.Dispose();
_connectionCancellationTokenSource = new CancellationTokenSource(); _connectionCancellationTokenSource = new CancellationTokenSource();
var token = _connectionCancellationTokenSource.Token; var token = _connectionCancellationTokenSource.Token;
@@ -730,7 +751,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
$"Stopping existing connection to {_serverManager.CurrentServer.ServerName}"))); $"Stopping existing connection to {_serverManager.CurrentServer.ServerName}")));
_initialized = false; _initialized = false;
_healthCheckTokenSource?.Cancel(); if (_healthCheckTokenSource != null)
{
await _healthCheckTokenSource.CancelAsync().ConfigureAwait(false);
}
Mediator.Publish(new DisconnectedMessage()); Mediator.Publish(new DisconnectedMessage());
_lightlessHub = null; _lightlessHub = null;
_connectionDto = null; _connectionDto = null;

View File

@@ -2,6 +2,12 @@
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net9.0-windows7.0": { "net9.0-windows7.0": {
"Blake3": {
"type": "Direct",
"requested": "[2.0.0, )",
"resolved": "2.0.0",
"contentHash": "v447kojeuNYSY5dvtVGG2bv1+M3vOWJXcrYWwXho/2uUpuwK6qPeu5WSMlqLm4VRJu96kysVO11La0zN3dLAuQ=="
},
"DalamudPackager": { "DalamudPackager": {
"type": "Direct", "type": "Direct",
"requested": "[13.1.0, )", "requested": "[13.1.0, )",