1.12.1 #43
@@ -41,9 +41,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Get version
|
- name: Get version
|
||||||
id: package_version
|
id: package_version
|
||||||
uses: KageKirin/get-csproj-version@v0
|
run: |
|
||||||
with:
|
version=$(grep -oPm1 "(?<=<Version>)[^<]+" LightlessSync/LightlessSync.csproj)
|
||||||
file: LightlessSync/LightlessSync.csproj
|
echo "version=$version" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Display version
|
- name: Display version
|
||||||
run: |
|
run: |
|
||||||
@@ -153,8 +153,11 @@ jobs:
|
|||||||
}' \
|
}' \
|
||||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases"
|
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases"
|
||||||
)
|
)
|
||||||
|
echo "API response: $response"
|
||||||
release_id=$(echo "$response" | jq -r .id)
|
release_id=$(echo "$response" | jq -r .id)
|
||||||
echo "release_id=$release_id" >> "$GITHUB_OUTPUT"
|
echo "release_id=$release_id"
|
||||||
|
echo "release_id=$release_id" >> $GITHUB_OUTPUT || echo "::set-output name=release_id::$release_id"
|
||||||
|
echo "RELEASE_ID=$release_id" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Check asset exists
|
- name: Check asset exists
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
Submodule LightlessAPI updated: 167508d27b...6c542c0ccc
@@ -1,4 +1,4 @@
|
|||||||
using K4os.Compression.LZ4.Legacy;
|
using K4os.Compression.LZ4.Legacy;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
@@ -19,7 +19,8 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
private readonly string _csvPath;
|
private readonly string _csvPath;
|
||||||
private readonly ConcurrentDictionary<string, List<FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
|
||||||
|
private readonly ConcurrentDictionary<string, FileCacheEntity> _fileCachesByPrefixedPath = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1);
|
||||||
private readonly Lock _fileWriteLock = new();
|
private readonly Lock _fileWriteLock = new();
|
||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
@@ -37,6 +38,57 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
private string CsvBakPath => _csvPath + ".bak";
|
private string CsvBakPath => _csvPath + ".bak";
|
||||||
|
|
||||||
|
private static string NormalizeSeparators(string path)
|
||||||
|
{
|
||||||
|
return path.Replace("/", "\\", StringComparison.Ordinal)
|
||||||
|
.Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizePrefixedPathKey(string prefixedPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(prefixedPath))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NormalizeSeparators(prefixedPath).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NormalizeToPrefixedPath(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path)) return string.Empty;
|
||||||
|
|
||||||
|
var normalized = NormalizeSeparators(path);
|
||||||
|
|
||||||
|
if (normalized.StartsWith(CachePrefix, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
normalized.StartsWith(PenumbraPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return NormalizePrefixedPathKey(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
var penumbraDir = _ipcManager.Penumbra.ModDirectory;
|
||||||
|
if (!string.IsNullOrEmpty(penumbraDir))
|
||||||
|
{
|
||||||
|
var normalizedPenumbra = NormalizeSeparators(penumbraDir);
|
||||||
|
var replacement = normalizedPenumbra.EndsWith("\\", StringComparison.Ordinal)
|
||||||
|
? PenumbraPrefix + "\\"
|
||||||
|
: PenumbraPrefix;
|
||||||
|
normalized = normalized.Replace(normalizedPenumbra, replacement, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheFolder = _configService.Current.CacheFolder;
|
||||||
|
if (!string.IsNullOrEmpty(cacheFolder))
|
||||||
|
{
|
||||||
|
var normalizedCache = NormalizeSeparators(cacheFolder);
|
||||||
|
var replacement = normalizedCache.EndsWith("\\", StringComparison.Ordinal)
|
||||||
|
? CachePrefix + "\\"
|
||||||
|
: CachePrefix;
|
||||||
|
normalized = normalized.Replace(normalizedCache, replacement, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NormalizePrefixedPathKey(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
public FileCacheEntity? CreateCacheEntry(string path)
|
public FileCacheEntity? CreateCacheEntry(string path)
|
||||||
{
|
{
|
||||||
FileInfo fi = new(path);
|
FileInfo fi = new(path);
|
||||||
@@ -61,20 +113,26 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
return CreateFileCacheEntity(fi, prefixedPath);
|
return CreateFileCacheEntity(fi, prefixedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v).ToList();
|
public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null)).ToList();
|
||||||
|
|
||||||
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 = [];
|
List<FileCacheEntity> output = [];
|
||||||
if (_fileCaches.TryGetValue(hash, out var fileCacheEntities))
|
if (_fileCaches.TryGetValue(hash, out var fileCacheEntities))
|
||||||
{
|
{
|
||||||
foreach (var fileCache in fileCacheEntities.Where(c => !ignoreCacheEntries || !c.IsCacheEntry).ToList())
|
foreach (var fileCache in fileCacheEntities.Values.Where(c => !ignoreCacheEntries || !c.IsCacheEntry).ToList())
|
||||||
{
|
{
|
||||||
if (!validate) output.Add(fileCache);
|
if (!validate)
|
||||||
|
{
|
||||||
|
output.Add(fileCache);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var validated = GetValidatedFileCache(fileCache);
|
var validated = GetValidatedFileCache(fileCache);
|
||||||
if (validated != null) output.Add(validated);
|
if (validated != null)
|
||||||
|
{
|
||||||
|
output.Add(validated);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,7 +144,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
{
|
{
|
||||||
_lightlessMediator.Publish(new HaltScanMessage(nameof(ValidateLocalIntegrity)));
|
_lightlessMediator.Publish(new HaltScanMessage(nameof(ValidateLocalIntegrity)));
|
||||||
_logger.LogInformation("Validating local storage");
|
_logger.LogInformation("Validating local storage");
|
||||||
var cacheEntries = _fileCaches.SelectMany(v => v.Value).Where(v => v.IsCacheEntry).ToList();
|
var cacheEntries = _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null)).Where(v => v.IsCacheEntry).ToList();
|
||||||
List<FileCacheEntity> brokenEntities = [];
|
List<FileCacheEntity> brokenEntities = [];
|
||||||
int i = 0;
|
int i = 0;
|
||||||
foreach (var fileCache in cacheEntries)
|
foreach (var fileCache in cacheEntries)
|
||||||
@@ -151,29 +209,40 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
public FileCacheEntity? GetFileCacheByHash(string hash)
|
public FileCacheEntity? GetFileCacheByHash(string hash)
|
||||||
{
|
{
|
||||||
if (_fileCaches.TryGetValue(hash, out var hashes))
|
if (_fileCaches.TryGetValue(hash, out var entries))
|
||||||
{
|
{
|
||||||
var item = hashes.OrderBy(p => p.PrefixedFilePath.Contains(PenumbraPrefix, StringComparison.Ordinal) ? 0 : 1).FirstOrDefault();
|
var item = entries.Values
|
||||||
if (item != null) return GetValidatedFileCache(item);
|
.OrderBy(p => p.PrefixedFilePath.Contains(PenumbraPrefix, StringComparison.Ordinal) ? 0 : 1)
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (item != null)
|
||||||
|
{
|
||||||
|
return GetValidatedFileCache(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private FileCacheEntity? GetFileCacheByPath(string path)
|
private FileCacheEntity? GetFileCacheByPath(string path)
|
||||||
{
|
{
|
||||||
var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant()
|
var normalizedPrefixedPath = NormalizeToPrefixedPath(path);
|
||||||
.Replace(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase);
|
if (string.IsNullOrEmpty(normalizedPrefixedPath))
|
||||||
var entry = _fileCaches.SelectMany(v => v.Value).FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (entry == null)
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Found no entries for {path}", cleanedPath);
|
return null;
|
||||||
return CreateFileEntry(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var validatedCacheEntry = GetValidatedFileCache(entry);
|
if (_fileCachesByPrefixedPath.TryGetValue(normalizedPrefixedPath, out var entry))
|
||||||
|
{
|
||||||
|
return GetValidatedFileCache(entry);
|
||||||
|
}
|
||||||
|
|
||||||
return validatedCacheEntry;
|
_logger.LogDebug("Found no entries for {path}", normalizedPrefixedPath);
|
||||||
|
|
||||||
|
if (normalizedPrefixedPath.Contains(CachePrefix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return CreateCacheEntry(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateFileEntry(path) ?? CreateCacheEntry(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, FileCacheEntity?> GetFileCachesByPaths(string[] paths)
|
public Dictionary<string, FileCacheEntity?> GetFileCachesByPaths(string[] paths)
|
||||||
@@ -182,73 +251,55 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var allEntities = _fileCaches.SelectMany(f => f.Value).ToArray();
|
var result = new Dictionary<string, FileCacheEntity?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var seenNormalized = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var cacheDict = new ConcurrentDictionary<string, FileCacheEntity>(
|
foreach (var originalPath in paths)
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
Parallel.ForEach(allEntities, entity =>
|
|
||||||
{
|
{
|
||||||
if (entity != null && entity.PrefixedFilePath != null)
|
if (string.IsNullOrEmpty(originalPath))
|
||||||
{
|
{
|
||||||
cacheDict[entity.PrefixedFilePath] = entity;
|
result[originalPath] = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = NormalizeToPrefixedPath(originalPath);
|
||||||
|
if (seenNormalized.Add(normalized))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(normalized))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Normalized path {cleaned}", normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(normalized))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Duplicate normalized path detected: {cleaned}", normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_fileCachesByPrefixedPath.TryGetValue(normalized, out var entity))
|
||||||
|
{
|
||||||
|
result[originalPath] = GetValidatedFileCache(entity);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileCacheEntity? created = null;
|
||||||
|
|
||||||
|
if (normalized.Contains(CachePrefix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
created = CreateCacheEntry(originalPath);
|
||||||
|
}
|
||||||
|
else if (normalized.Contains(PenumbraPrefix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
created = CreateFileEntry(originalPath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Null FileCacheEntity or PrefixedFilePath encountered in cache population: {entity}", entity);
|
created = CreateFileEntry(originalPath) ?? CreateCacheEntry(originalPath);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
var cleanedPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
result[originalPath] = created;
|
||||||
var seenCleaned = new ConcurrentDictionary<string, byte>(StringComparer.OrdinalIgnoreCase);
|
}
|
||||||
|
|
||||||
Parallel.ForEach(paths, p =>
|
return result;
|
||||||
{
|
|
||||||
var cleaned = p.Replace("/", "\\", StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace(
|
|
||||||
_ipcManager.Penumbra.ModDirectory!,
|
|
||||||
_ipcManager.Penumbra.ModDirectory!.EndsWith('\\')
|
|
||||||
? PenumbraPrefix + '\\' : PenumbraPrefix,
|
|
||||||
StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace(
|
|
||||||
_configService.Current.CacheFolder,
|
|
||||||
_configService.Current.CacheFolder.EndsWith('\\')
|
|
||||||
? CachePrefix + '\\' : CachePrefix,
|
|
||||||
StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace("\\\\", "\\", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
if (seenCleaned.TryAdd(cleaned, 0))
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Adding to cleanedPaths: {cleaned}", cleaned);
|
|
||||||
cleanedPaths[p] = cleaned;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Duplicate found: {cleaned}", cleaned);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var result = new ConcurrentDictionary<string, FileCacheEntity?>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
Parallel.ForEach(cleanedPaths, entry =>
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Checking if in cache: {path}", entry.Value);
|
|
||||||
|
|
||||||
if (cacheDict.TryGetValue(entry.Value, out var entity))
|
|
||||||
{
|
|
||||||
var validatedCache = GetValidatedFileCache(entity);
|
|
||||||
result[entry.Key] = validatedCache;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!entry.Value.Contains(CachePrefix, StringComparison.Ordinal))
|
|
||||||
result[entry.Key] = CreateFileEntry(entry.Key);
|
|
||||||
else
|
|
||||||
result[entry.Key] = CreateCacheEntry(entry.Key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Dictionary<string, FileCacheEntity?>(result, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -258,17 +309,24 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
public void RemoveHashedFile(string hash, string prefixedFilePath)
|
public void RemoveHashedFile(string hash, string prefixedFilePath)
|
||||||
{
|
{
|
||||||
|
var normalizedPath = NormalizePrefixedPathKey(prefixedFilePath);
|
||||||
|
|
||||||
if (_fileCaches.TryGetValue(hash, out var caches))
|
if (_fileCaches.TryGetValue(hash, out var caches))
|
||||||
{
|
{
|
||||||
_logger.LogTrace("Removing from DB: {hash} => {path}", hash, prefixedFilePath);
|
_logger.LogTrace("Removing from DB: {hash} => {path}", hash, prefixedFilePath);
|
||||||
var removedCount = caches?.RemoveAll(c => string.Equals(c.PrefixedFilePath, prefixedFilePath, StringComparison.Ordinal));
|
|
||||||
_logger.LogTrace("Removed from DB: {count} file(s) with hash {hash} and file cache {path}", removedCount, hash, prefixedFilePath);
|
|
||||||
|
|
||||||
if (caches?.Count == 0)
|
if (caches.TryRemove(normalizedPath, out var removedEntity))
|
||||||
{
|
{
|
||||||
_fileCaches.Remove(hash, out var entity);
|
_logger.LogTrace("Removed from DB: {hash} => {path}", hash, removedEntity.PrefixedFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caches.IsEmpty)
|
||||||
|
{
|
||||||
|
_fileCaches.TryRemove(hash, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_fileCachesByPrefixedPath.TryRemove(normalizedPath, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true)
|
public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true)
|
||||||
@@ -309,7 +367,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
lock (_fileWriteLock)
|
lock (_fileWriteLock)
|
||||||
{
|
{
|
||||||
StringBuilder sb = new();
|
StringBuilder sb = new();
|
||||||
foreach (var entry in _fileCaches.SelectMany(k => k.Value).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
foreach (var entry in _fileCaches.Values.SelectMany(k => k.Values).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
sb.AppendLine(entry.CsvEntry);
|
sb.AppendLine(entry.CsvEntry);
|
||||||
}
|
}
|
||||||
@@ -354,16 +412,11 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
private void AddHashedFile(FileCacheEntity fileCache)
|
private void AddHashedFile(FileCacheEntity fileCache)
|
||||||
{
|
{
|
||||||
if (!_fileCaches.TryGetValue(fileCache.Hash, out var entries) || entries is null)
|
var normalizedPath = NormalizePrefixedPathKey(fileCache.PrefixedFilePath);
|
||||||
{
|
var entries = _fileCaches.GetOrAdd(fileCache.Hash, _ => new ConcurrentDictionary<string, FileCacheEntity>(StringComparer.OrdinalIgnoreCase));
|
||||||
_fileCaches[fileCache.Hash] = entries = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entries.Exists(u => string.Equals(u.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.OrdinalIgnoreCase)))
|
entries[normalizedPath] = fileCache;
|
||||||
{
|
_fileCachesByPrefixedPath[normalizedPath] = fileCache;
|
||||||
//_logger.LogTrace("Adding to DB: {hash} => {path}", fileCache.Hash, fileCache.PrefixedFilePath);
|
|
||||||
entries.Add(fileCache);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null)
|
private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Dalamud.Game.Text;
|
||||||
|
using LightlessSync.UtilsEnum.Enum;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.UI;
|
using LightlessSync.UI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -33,6 +35,9 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool OpenGposeImportOnGposeStart { get; set; } = false;
|
public bool OpenGposeImportOnGposeStart { get; set; } = false;
|
||||||
public bool OpenPopupOnAdd { get; set; } = true;
|
public bool OpenPopupOnAdd { get; set; } = true;
|
||||||
public int ParallelDownloads { get; set; } = 10;
|
public int ParallelDownloads { get; set; } = 10;
|
||||||
|
public int ParallelUploads { get; set; } = 8;
|
||||||
|
public bool EnablePairProcessingLimiter { get; set; } = true;
|
||||||
|
public int MaxConcurrentPairApplications { get; set; } = 3;
|
||||||
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
||||||
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
||||||
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||||
@@ -70,7 +75,16 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool overrideFcTagColor { get; set; } = false;
|
public bool overrideFcTagColor { get; set; } = false;
|
||||||
public bool useColoredUIDs { get; set; } = true;
|
public bool useColoredUIDs { get; set; } = true;
|
||||||
public bool BroadcastEnabled { get; set; } = false;
|
public bool BroadcastEnabled { get; set; } = false;
|
||||||
|
public short LightfinderLabelOffsetX { get; set; } = 0;
|
||||||
|
public short LightfinderLabelOffsetY { get; set; } = 0;
|
||||||
|
public bool LightfinderLabelUseIcon { get; set; } = false;
|
||||||
|
public bool LightfinderLabelShowOwn { get; set; } = true;
|
||||||
|
public bool LightfinderLabelShowPaired { get; set; } = true;
|
||||||
|
public string LightfinderLabelIconGlyph { get; set; } = SeIconCharExtensions.ToIconString(SeIconChar.Hyadelyn);
|
||||||
|
public float LightfinderLabelScale { get; set; } = 1.0f;
|
||||||
|
public bool LightfinderAutoAlign { get; set; } = true;
|
||||||
|
public LabelAlignment LabelAlignment { get; set; } = LabelAlignment.Left;
|
||||||
public DateTime BroadcastTtl { get; set; } = DateTime.MinValue;
|
public DateTime BroadcastTtl { get; set; } = DateTime.MinValue;
|
||||||
public bool SyncshellFinderEnabled { get; set; } = false;
|
public bool SyncshellFinderEnabled { get; set; } = false;
|
||||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
|
|
||||||
|
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
public class ServerTagConfig : ILightlessConfiguration
|
||||||
|
{
|
||||||
|
public Dictionary<string, ServerTagStorage> ServerTagStorage { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace LightlessSync.LightlessConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ServerTagStorage
|
||||||
|
{
|
||||||
|
public HashSet<string> OpenPairTags { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<string, List<string>> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace LightlessSync.LightlessConfiguration;
|
||||||
|
|
||||||
|
public class ServerTagConfigService : ConfigurationServiceBase<ServerTagConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "servertags.json";
|
||||||
|
|
||||||
|
public ServerTagConfigService(string configDir) : base(configDir)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors></Authors>
|
<Authors></Authors>
|
||||||
<Company></Company>
|
<Company></Company>
|
||||||
<Version>1.12.0</Version>
|
<Version>1.12.1</Version>
|
||||||
<Description></Description>
|
<Description></Description>
|
||||||
<Copyright></Copyright>
|
<Copyright></Copyright>
|
||||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
@@ -21,6 +21,7 @@ public class PairHandlerFactory
|
|||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||||
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
private readonly ServerConfigurationManager _serverConfigManager;
|
private readonly ServerConfigurationManager _serverConfigManager;
|
||||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ public class PairHandlerFactory
|
|||||||
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
||||||
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
||||||
FileCacheManager fileCacheManager, LightlessMediator lightlessMediator, PlayerPerformanceService playerPerformanceService,
|
FileCacheManager fileCacheManager, LightlessMediator lightlessMediator, PlayerPerformanceService playerPerformanceService,
|
||||||
|
PairProcessingLimiter pairProcessingLimiter,
|
||||||
ServerConfigurationManager serverConfigManager)
|
ServerConfigurationManager serverConfigManager)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
@@ -40,6 +42,7 @@ public class PairHandlerFactory
|
|||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
_playerPerformanceService = playerPerformanceService;
|
_playerPerformanceService = playerPerformanceService;
|
||||||
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
_serverConfigManager = serverConfigManager;
|
_serverConfigManager = serverConfigManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +50,6 @@ public class PairHandlerFactory
|
|||||||
{
|
{
|
||||||
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _gameObjectHandlerFactory,
|
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _gameObjectHandlerFactory,
|
||||||
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
||||||
_fileCacheManager, _lightlessMediator, _playerPerformanceService, _serverConfigManager);
|
_fileCacheManager, _lightlessMediator, _playerPerformanceService, _pairProcessingLimiter, _serverConfigManager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
using LightlessSync.PlayerData.Factories;
|
using LightlessSync.PlayerData.Factories;
|
||||||
@@ -28,6 +28,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly IHostApplicationLifetime _lifetime;
|
private readonly IHostApplicationLifetime _lifetime;
|
||||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||||
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
private readonly ServerConfigurationManager _serverConfigManager;
|
private readonly ServerConfigurationManager _serverConfigManager;
|
||||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||||
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
||||||
@@ -50,6 +51,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
||||||
FileCacheManager fileDbManager, LightlessMediator mediator,
|
FileCacheManager fileDbManager, LightlessMediator mediator,
|
||||||
PlayerPerformanceService playerPerformanceService,
|
PlayerPerformanceService playerPerformanceService,
|
||||||
|
PairProcessingLimiter pairProcessingLimiter,
|
||||||
ServerConfigurationManager serverConfigManager) : base(logger, mediator)
|
ServerConfigurationManager serverConfigManager) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
Pair = pair;
|
Pair = pair;
|
||||||
@@ -61,6 +63,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
_lifetime = lifetime;
|
_lifetime = lifetime;
|
||||||
_fileDbManager = fileDbManager;
|
_fileDbManager = fileDbManager;
|
||||||
_playerPerformanceService = playerPerformanceService;
|
_playerPerformanceService = playerPerformanceService;
|
||||||
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
_serverConfigManager = serverConfigManager;
|
_serverConfigManager = serverConfigManager;
|
||||||
_penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, Pair.UserData.UID).ConfigureAwait(false).GetAwaiter().GetResult();
|
_penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, Pair.UserData.UID).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||||
|
|
||||||
@@ -420,6 +423,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
||||||
bool updateModdedPaths, bool updateManip, CancellationToken downloadToken)
|
bool updateModdedPaths, bool updateManip, CancellationToken downloadToken)
|
||||||
{
|
{
|
||||||
|
await using var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false);
|
||||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
|
Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
|
||||||
|
|
||||||
if (updateModdedPaths)
|
if (updateModdedPaths)
|
||||||
@@ -737,6 +741,11 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[BASE-{appBase}] Modded path calculation cancelled", applicationBase);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "[BASE-{appBase}] Something went wrong during calculation replacements", applicationBase);
|
Logger.LogError(ex, "[BASE-{appBase}] Something went wrong during calculation replacements", applicationBase);
|
||||||
@@ -763,4 +772,4 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
_dataReceivedInDowntime = null;
|
_dataReceivedInDowntime = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Comparer;
|
using LightlessSync.API.Data.Comparer;
|
||||||
using LightlessSync.API.Data.Extensions;
|
using LightlessSync.API.Data.Extensions;
|
||||||
@@ -7,10 +7,14 @@ using LightlessSync.API.Dto.User;
|
|||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.PlayerData.Factories;
|
using LightlessSync.PlayerData.Factories;
|
||||||
|
using LightlessSync.Services;
|
||||||
|
|
||||||
using LightlessSync.Services.Events;
|
using LightlessSync.Services.Events;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Pairs;
|
namespace LightlessSync.PlayerData.Pairs;
|
||||||
|
|
||||||
@@ -24,14 +28,19 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
private Lazy<List<Pair>> _directPairsInternal;
|
private Lazy<List<Pair>> _directPairsInternal;
|
||||||
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> _groupPairsInternal;
|
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> _groupPairsInternal;
|
||||||
private Lazy<Dictionary<Pair, List<GroupFullInfoDto>>> _pairsWithGroupsInternal;
|
private Lazy<Dictionary<Pair, List<GroupFullInfoDto>>> _pairsWithGroupsInternal;
|
||||||
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
|
private readonly ConcurrentQueue<(Pair Pair, OnlineUserIdentDto? Ident)> _pairCreationQueue = new();
|
||||||
|
private CancellationTokenSource _pairCreationCts = new();
|
||||||
|
private int _pairCreationProcessorRunning;
|
||||||
|
|
||||||
public PairManager(ILogger<PairManager> logger, PairFactory pairFactory,
|
public PairManager(ILogger<PairManager> logger, PairFactory pairFactory,
|
||||||
LightlessConfigService configurationService, LightlessMediator mediator,
|
LightlessConfigService configurationService, LightlessMediator mediator,
|
||||||
IContextMenu dalamudContextMenu) : base(logger, mediator)
|
IContextMenu dalamudContextMenu, PairProcessingLimiter pairProcessingLimiter) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_pairFactory = pairFactory;
|
_pairFactory = pairFactory;
|
||||||
_configurationService = configurationService;
|
_configurationService = configurationService;
|
||||||
_dalamudContextMenu = dalamudContextMenu;
|
_dalamudContextMenu = dalamudContextMenu;
|
||||||
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
Mediator.Subscribe<DisconnectedMessage>(this, (_) => ClearPairs());
|
Mediator.Subscribe<DisconnectedMessage>(this, (_) => ClearPairs());
|
||||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => ReapplyPairData());
|
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => ReapplyPairData());
|
||||||
_directPairsInternal = DirectPairsLazy();
|
_directPairsInternal = DirectPairsLazy();
|
||||||
@@ -112,6 +121,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
public void ClearPairs()
|
public void ClearPairs()
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Clearing all Pairs");
|
Logger.LogDebug("Clearing all Pairs");
|
||||||
|
ResetPairCreationQueue();
|
||||||
DisposePairs();
|
DisposePairs();
|
||||||
_allClientPairs.Clear();
|
_allClientPairs.Clear();
|
||||||
_allGroups.Clear();
|
_allGroups.Clear();
|
||||||
@@ -161,7 +171,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5)));
|
Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pair.CreateCachedPlayer(dto);
|
QueuePairCreation(pair, dto);
|
||||||
|
|
||||||
RecreateLazy();
|
RecreateLazy();
|
||||||
}
|
}
|
||||||
@@ -332,6 +342,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
ResetPairCreationQueue();
|
||||||
_dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu;
|
_dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu;
|
||||||
|
|
||||||
DisposePairs();
|
DisposePairs();
|
||||||
@@ -390,6 +401,84 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void QueuePairCreation(Pair pair, OnlineUserIdentDto? dto)
|
||||||
|
{
|
||||||
|
if (pair.HasCachedPlayer)
|
||||||
|
{
|
||||||
|
RecreateLazy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pairCreationQueue.Enqueue((pair, dto));
|
||||||
|
StartPairCreationProcessor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartPairCreationProcessor()
|
||||||
|
{
|
||||||
|
if (_pairCreationCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Interlocked.CompareExchange(ref _pairCreationProcessorRunning, 1, 0) == 0)
|
||||||
|
{
|
||||||
|
_ = Task.Run(ProcessPairCreationQueueAsync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessPairCreationQueueAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!_pairCreationCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (!_pairCreationQueue.TryDequeue(out var work))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var lease = await _pairProcessingLimiter.AcquireAsync(_pairCreationCts.Token).ConfigureAwait(false);
|
||||||
|
if (!work.Pair.HasCachedPlayer)
|
||||||
|
{
|
||||||
|
work.Pair.CreateCachedPlayer(work.Ident);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error creating cached player for {uid}", work.Pair.UserData.UID);
|
||||||
|
}
|
||||||
|
|
||||||
|
RecreateLazy();
|
||||||
|
await Task.Yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _pairCreationProcessorRunning, 0);
|
||||||
|
if (!_pairCreationQueue.IsEmpty && !_pairCreationCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
StartPairCreationProcessor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetPairCreationQueue()
|
||||||
|
{
|
||||||
|
_pairCreationCts.Cancel();
|
||||||
|
while (_pairCreationQueue.TryDequeue(out _))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
_pairCreationCts.Dispose();
|
||||||
|
_pairCreationCts = new CancellationTokenSource();
|
||||||
|
Interlocked.Exchange(ref _pairCreationProcessorRunning, 0);
|
||||||
|
}
|
||||||
|
|
||||||
private void ReapplyPairData()
|
private void ReapplyPairData()
|
||||||
{
|
{
|
||||||
foreach (var pair in _allClientPairs.Select(k => k.Value))
|
foreach (var pair in _allClientPairs.Select(k => k.Value))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
@@ -101,6 +101,8 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
forced |= _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash;
|
forced |= _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash;
|
||||||
|
|
||||||
if (_fileUploadTask == null || (_fileUploadTask?.IsCompleted ?? false) || forced)
|
if (_fileUploadTask == null || (_fileUploadTask?.IsCompleted ?? false) || forced)
|
||||||
@@ -127,6 +129,15 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
|||||||
_pushDataSemaphore.Release();
|
_pushDataSemaphore.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (_runtimeCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("PushCharacterData cancelled");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Failed to push character data");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +106,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<GameObjectHandlerFactory>();
|
collection.AddSingleton<GameObjectHandlerFactory>();
|
||||||
collection.AddSingleton<FileDownloadManagerFactory>();
|
collection.AddSingleton<FileDownloadManagerFactory>();
|
||||||
collection.AddSingleton<PairHandlerFactory>();
|
collection.AddSingleton<PairHandlerFactory>();
|
||||||
|
collection.AddSingleton<PairProcessingLimiter>();
|
||||||
collection.AddSingleton<PairFactory>();
|
collection.AddSingleton<PairFactory>();
|
||||||
collection.AddSingleton<XivDataAnalyzer>();
|
collection.AddSingleton<XivDataAnalyzer>();
|
||||||
collection.AddSingleton<CharacterAnalyzer>();
|
collection.AddSingleton<CharacterAnalyzer>();
|
||||||
@@ -144,7 +145,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService<ILogger<DtrEntry>>(), dtrBar, s.GetRequiredService<LightlessConfigService>(),
|
collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService<ILogger<DtrEntry>>(), dtrBar, s.GetRequiredService<LightlessConfigService>(),
|
||||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>()));
|
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>()));
|
||||||
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
|
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
|
||||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu));
|
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu, s.GetRequiredService<PairProcessingLimiter>()));
|
||||||
collection.AddSingleton<RedrawManager>();
|
collection.AddSingleton<RedrawManager>();
|
||||||
collection.AddSingleton<BroadcastService>();
|
collection.AddSingleton<BroadcastService>();
|
||||||
collection.AddSingleton(addonLifecycle);
|
collection.AddSingleton(addonLifecycle);
|
||||||
@@ -207,7 +208,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<ConfigurationMigrator>();
|
collection.AddSingleton<ConfigurationMigrator>();
|
||||||
collection.AddSingleton<ConfigurationSaveService>();
|
collection.AddSingleton<ConfigurationSaveService>();
|
||||||
collection.AddSingleton<HubFactory>();
|
collection.AddSingleton<HubFactory>();
|
||||||
collection.AddSingleton<NameplateHandler>();
|
|
||||||
collection.AddSingleton(s => new BroadcastScannerService( s.GetRequiredService<ILogger<BroadcastScannerService>>(), clientState, objectTable, framework, s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<NameplateHandler>(), s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessConfigService>()));
|
collection.AddSingleton(s => new BroadcastScannerService( s.GetRequiredService<ILogger<BroadcastScannerService>>(), clientState, objectTable, framework, s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<NameplateHandler>(), s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessConfigService>()));
|
||||||
|
|
||||||
|
|
||||||
@@ -252,6 +252,8 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
s.GetRequiredService<LightlessMediator>()));
|
s.GetRequiredService<LightlessMediator>()));
|
||||||
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), namePlateGui, clientState,
|
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), namePlateGui, clientState,
|
||||||
s.GetRequiredService<PairManager>(), s.GetRequiredService<LightlessMediator>()));
|
s.GetRequiredService<PairManager>(), s.GetRequiredService<LightlessMediator>()));
|
||||||
|
collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService<ILogger<NameplateHandler>>(), addonLifecycle, gameGui, s.GetRequiredService<DalamudUtilService>(),
|
||||||
|
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), clientState, s.GetRequiredService<PairManager>()));
|
||||||
|
|
||||||
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<LightlessMediator>());
|
collection.AddHostedService(p => p.GetRequiredService<LightlessMediator>());
|
||||||
@@ -277,4 +279,4 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
_host.StopAsync().GetAwaiter().GetResult();
|
_host.StopAsync().GetAwaiter().GetResult();
|
||||||
_host.Dispose();
|
_host.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,8 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
"\t /light toggle on|off - Connects or disconnects to Lightless respectively" + Environment.NewLine +
|
"\t /light toggle on|off - Connects or disconnects to Lightless respectively" + Environment.NewLine +
|
||||||
"\t /light gpose - Opens the Lightless Character Data Hub window" + Environment.NewLine +
|
"\t /light gpose - Opens the Lightless Character Data Hub window" + Environment.NewLine +
|
||||||
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
||||||
"\t /light settings - Opens the Lightless Settings window"
|
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
|
||||||
|
"\t /light lightfinder - Opens the Lightfinder window"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,5 +123,9 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
{
|
{
|
||||||
_mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
_mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
||||||
}
|
}
|
||||||
|
else if (string.Equals(splitArgs[0], "lightfinder", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_mediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
private readonly PairRequestService _pairRequestService;
|
private readonly PairRequestService _pairRequestService;
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly IObjectTable _objectTable;
|
private readonly IObjectTable _objectTable;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
|
|
||||||
public ContextMenuService(
|
public ContextMenuService(
|
||||||
IContextMenu contextMenu,
|
IContextMenu contextMenu,
|
||||||
@@ -45,6 +46,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_objectTable = objectTable;
|
_objectTable = objectTable;
|
||||||
|
_configService = configService;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
_pairRequestService = pairRequestService;
|
_pairRequestService = pairRequestService;
|
||||||
_clientState = clientState;
|
_clientState = clientState;
|
||||||
@@ -108,6 +110,9 @@ internal class ContextMenuService : IHostedService
|
|||||||
var world = GetWorld(target.TargetHomeWorld.RowId);
|
var world = GetWorld(target.TargetHomeWorld.RowId);
|
||||||
if (!IsWorldValid(world))
|
if (!IsWorldValid(world))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (!_configService.Current.EnableRightClickMenus)
|
||||||
|
return;
|
||||||
|
|
||||||
args.AddMenuItem(new MenuItem
|
args.AddMenuItem(new MenuItem
|
||||||
{
|
{
|
||||||
@@ -153,6 +158,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
_logger.LogError(ex, "Error sending pair request.");
|
_logger.LogError(ex, "Error sending pair request.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private HashSet<ulong> VisibleUserIds => [.. _pairManager.GetOnlineUserPairs()
|
private HashSet<ulong> VisibleUserIds => [.. _pairManager.GetOnlineUserPairs()
|
||||||
.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)];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Dto;
|
using LightlessSync.API.Dto;
|
||||||
using LightlessSync.API.Dto.CharaData;
|
using LightlessSync.API.Dto.CharaData;
|
||||||
@@ -77,6 +77,7 @@ public record OpenCensusPopupMessage() : MessageBase;
|
|||||||
public record OpenSyncshellAdminPanel(GroupFullInfoDto GroupInfo) : MessageBase;
|
public record OpenSyncshellAdminPanel(GroupFullInfoDto GroupInfo) : MessageBase;
|
||||||
public record OpenPermissionWindow(Pair Pair) : MessageBase;
|
public record OpenPermissionWindow(Pair Pair) : MessageBase;
|
||||||
public record DownloadLimitChangedMessage() : SameThreadMessage;
|
public record DownloadLimitChangedMessage() : SameThreadMessage;
|
||||||
|
public record PairProcessingLimitChangedMessage : SameThreadMessage;
|
||||||
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
|
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
|
||||||
public record TargetPairMessage(Pair Pair) : MessageBase;
|
public record TargetPairMessage(Pair Pair) : MessageBase;
|
||||||
public record CombatStartMessage : MessageBase;
|
public record CombatStartMessage : MessageBase;
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
using Dalamud.Game.Addon.Lifecycle;
|
using Dalamud.Game.Addon.Lifecycle;
|
||||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.UI;
|
using LightlessSync.UI;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
|
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.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
@@ -18,7 +25,10 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
private readonly ILogger<NameplateHandler> _logger;
|
private readonly ILogger<NameplateHandler> _logger;
|
||||||
private readonly IAddonLifecycle _addonLifecycle;
|
private readonly IAddonLifecycle _addonLifecycle;
|
||||||
private readonly IGameGui _gameGui;
|
private readonly IGameGui _gameGui;
|
||||||
|
private readonly IClientState _clientState;
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
private readonly LightlessMediator _mediator;
|
private readonly LightlessMediator _mediator;
|
||||||
public LightlessMediator Mediator => _mediator;
|
public LightlessMediator Mediator => _mediator;
|
||||||
|
|
||||||
@@ -26,18 +36,31 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
private bool _needsLabelRefresh = false;
|
private bool _needsLabelRefresh = false;
|
||||||
private AddonNamePlate* mpNameplateAddon = null;
|
private AddonNamePlate* mpNameplateAddon = null;
|
||||||
private readonly AtkTextNode*[] mTextNodes = new AtkTextNode*[AddonNamePlate.NumNamePlateObjects];
|
private readonly AtkTextNode*[] mTextNodes = new AtkTextNode*[AddonNamePlate.NumNamePlateObjects];
|
||||||
|
private readonly int[] _cachedNameplateTextWidths = new int[AddonNamePlate.NumNamePlateObjects];
|
||||||
|
private readonly int[] _cachedNameplateTextHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
||||||
|
private readonly int[] _cachedNameplateContainerHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
||||||
|
private readonly int[] _cachedNameplateTextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
|
||||||
|
|
||||||
internal const uint mNameplateNodeIDBase = 0x7D99D500;
|
internal const uint mNameplateNodeIDBase = 0x7D99D500;
|
||||||
|
private const string DefaultLabelText = "LightFinder";
|
||||||
|
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
|
||||||
|
private const int ContainerOffsetX = 50;
|
||||||
|
private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
|
||||||
|
|
||||||
private volatile HashSet<string> _activeBroadcastingCids = new();
|
private volatile HashSet<string> _activeBroadcastingCids = [];
|
||||||
|
|
||||||
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessMediator mediator)
|
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairManager pairManager)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_addonLifecycle = addonLifecycle;
|
_addonLifecycle = addonLifecycle;
|
||||||
_gameGui = gameGui;
|
_gameGui = gameGui;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_configService = configService;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
|
_clientState = clientState;
|
||||||
|
_pairManager = pairManager;
|
||||||
|
|
||||||
|
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Init()
|
internal void Init()
|
||||||
@@ -96,6 +119,10 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
if (mpNameplateAddon != pNameplateAddon)
|
if (mpNameplateAddon != pNameplateAddon)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < mTextNodes.Length; ++i) mTextNodes[i] = null;
|
for (int i = 0; i < mTextNodes.Length; ++i) mTextNodes[i] = null;
|
||||||
|
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
||||||
|
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
||||||
|
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
||||||
|
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||||
mpNameplateAddon = pNameplateAddon;
|
mpNameplateAddon = pNameplateAddon;
|
||||||
if (mpNameplateAddon != null) CreateNameplateNodes();
|
if (mpNameplateAddon != null) CreateNameplateNodes();
|
||||||
}
|
}
|
||||||
@@ -156,6 +183,11 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
||||||
|
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
||||||
|
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
||||||
|
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HideAllNameplateNodes()
|
private void HideAllNameplateNodes()
|
||||||
@@ -177,6 +209,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
|
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
|
||||||
{
|
{
|
||||||
var objectInfo = ui3DModule->NamePlateObjectInfoPointers[i].Value;
|
var objectInfo = ui3DModule->NamePlateObjectInfoPointers[i].Value;
|
||||||
|
|
||||||
if (objectInfo == null || objectInfo->GameObject == null)
|
if (objectInfo == null || objectInfo->GameObject == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -190,30 +223,202 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
|
|
||||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject);
|
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject);
|
||||||
|
|
||||||
//_logger.LogInformation($"checking cid: {cid}", cid);
|
|
||||||
|
|
||||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
||||||
{
|
{
|
||||||
pNode->AtkResNode.ToggleVisibility(false);
|
pNode->AtkResNode.ToggleVisibility(false);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
pNode->AtkResNode.ToggleVisibility(true);
|
if (!_configService.Current.LightfinderLabelShowOwn && (objectInfo->GameObject->GetGameObjectId() == _clientState.LocalPlayer.GameObjectId))
|
||||||
|
{
|
||||||
|
pNode->AtkResNode.ToggleVisibility(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_configService.Current.LightfinderLabelShowPaired && VisibleUserIds.Any(u => u == objectInfo->GameObject->GetGameObjectId()))
|
||||||
|
{
|
||||||
|
pNode->AtkResNode.ToggleVisibility(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var nameplateObject = mpNameplateAddon->NamePlateObjectArray[nameplateIndex];
|
var nameplateObject = mpNameplateAddon->NamePlateObjectArray[nameplateIndex];
|
||||||
nameplateObject.RootComponentNode->Component->UldManager.UpdateDrawNodeList();
|
nameplateObject.RootComponentNode->Component->UldManager.UpdateDrawNodeList();
|
||||||
|
|
||||||
|
var pNameplateIconNode = nameplateObject.MarkerIcon;
|
||||||
|
var pNameplateResNode = nameplateObject.NameContainer;
|
||||||
|
var pNameplateTextNode = nameplateObject.NameText;
|
||||||
|
bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible());
|
||||||
|
pNode->AtkResNode.ToggleVisibility(IsVisible);
|
||||||
|
|
||||||
var nameContainer = nameplateObject.NameContainer;
|
var nameContainer = nameplateObject.NameContainer;
|
||||||
var nameText = nameplateObject.NameText;
|
var nameText = nameplateObject.NameText;
|
||||||
|
|
||||||
|
if (nameContainer == null || nameText == null)
|
||||||
|
{
|
||||||
|
pNode->AtkResNode.ToggleVisibility(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var labelColor = UIColors.Get("LightlessPurple");
|
var labelColor = UIColors.Get("LightlessPurple");
|
||||||
var edgeColor = UIColors.Get("FullBlack");
|
var edgeColor = UIColors.Get("FullBlack");
|
||||||
|
var config = _configService.Current;
|
||||||
|
|
||||||
var labelY = nameContainer->Height - nameplateObject.TextH - (int)(24 * nameText->AtkResNode.ScaleY);
|
var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
|
||||||
|
var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
||||||
|
var effectiveScale = baseScale * scaleMultiplier;
|
||||||
|
var labelContent = config.LightfinderLabelUseIcon
|
||||||
|
? NormalizeIconGlyph(config.LightfinderLabelIconGlyph)
|
||||||
|
: DefaultLabelText;
|
||||||
|
|
||||||
pNode->AtkResNode.SetPositionShort(58, (short)labelY);
|
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
||||||
|
pNode->AtkResNode.SetScale(effectiveScale, effectiveScale);
|
||||||
|
var nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||||
|
if (nodeWidth <= 0)
|
||||||
|
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||||
|
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
||||||
|
var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f;
|
||||||
|
var computedFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
||||||
|
pNode->FontSize = (byte)System.Math.Clamp(computedFontSize, 1, 255);
|
||||||
|
AlignmentType alignment;
|
||||||
|
|
||||||
|
var textScaleY = nameText->AtkResNode.ScaleY;
|
||||||
|
if (textScaleY <= 0f)
|
||||||
|
textScaleY = 1f;
|
||||||
|
|
||||||
|
var blockHeight = System.Math.Abs((int)nameplateObject.TextH);
|
||||||
|
if (blockHeight > 0)
|
||||||
|
{
|
||||||
|
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
blockHeight = _cachedNameplateTextHeights[nameplateIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockHeight <= 0)
|
||||||
|
{
|
||||||
|
blockHeight = GetScaledTextHeight(nameText);
|
||||||
|
if (blockHeight <= 0)
|
||||||
|
blockHeight = nodeHeight;
|
||||||
|
|
||||||
|
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
var containerHeight = (int)nameContainer->Height;
|
||||||
|
if (containerHeight > 0)
|
||||||
|
{
|
||||||
|
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
containerHeight = _cachedNameplateContainerHeights[nameplateIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containerHeight <= 0)
|
||||||
|
{
|
||||||
|
containerHeight = blockHeight + (int)System.Math.Round(8 * textScaleY);
|
||||||
|
if (containerHeight <= blockHeight)
|
||||||
|
containerHeight = blockHeight + 1;
|
||||||
|
|
||||||
|
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockTop = containerHeight - blockHeight;
|
||||||
|
if (blockTop < 0)
|
||||||
|
blockTop = 0;
|
||||||
|
var verticalPadding = (int)System.Math.Round(4 * effectiveScale);
|
||||||
|
|
||||||
|
var positionY = blockTop - verticalPadding - nodeHeight;
|
||||||
|
|
||||||
|
var textWidth = System.Math.Abs((int)nameplateObject.TextW);
|
||||||
|
if (textWidth <= 0)
|
||||||
|
{
|
||||||
|
textWidth = GetScaledTextWidth(nameText);
|
||||||
|
if (textWidth <= 0)
|
||||||
|
textWidth = nodeWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textWidth > 0)
|
||||||
|
{
|
||||||
|
_cachedNameplateTextWidths[nameplateIndex] = textWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
||||||
|
var hasValidOffset = true;
|
||||||
|
|
||||||
|
if (System.Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0)
|
||||||
|
{
|
||||||
|
_cachedNameplateTextOffsets[nameplateIndex] = textOffset;
|
||||||
|
}
|
||||||
|
else if (_cachedNameplateTextOffsets[nameplateIndex] != int.MinValue)
|
||||||
|
{
|
||||||
|
textOffset = _cachedNameplateTextOffsets[nameplateIndex];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hasValidOffset = false;
|
||||||
|
}
|
||||||
|
int positionX;
|
||||||
|
|
||||||
|
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
|
||||||
|
{
|
||||||
|
var nameplateWidth = (int)nameContainer->Width;
|
||||||
|
|
||||||
|
if (!config.LightfinderLabelUseIcon)
|
||||||
|
{
|
||||||
|
pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize;
|
||||||
|
pNode->AtkResNode.Width = 0;
|
||||||
|
pNode->SetText(labelContent);
|
||||||
|
|
||||||
|
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||||
|
if (nodeWidth <= 0)
|
||||||
|
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||||
|
|
||||||
|
if (nodeWidth > nameplateWidth)
|
||||||
|
nodeWidth = nameplateWidth;
|
||||||
|
|
||||||
|
pNode->AtkResNode.Width = (ushort)nodeWidth;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pNode->TextFlags |= TextFlags.AutoAdjustNodeSize;
|
||||||
|
pNode->AtkResNode.Width = 0;
|
||||||
|
pNode->SetText(labelContent);
|
||||||
|
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
int leftPos = nameplateWidth / 8;
|
||||||
|
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
|
||||||
|
int centrePos = (nameplateWidth - nodeWidth) / 2;
|
||||||
|
int staticMargin = 24;
|
||||||
|
int calcMargin = (int)(nameplateWidth * 0.08f);
|
||||||
|
|
||||||
|
switch (config.LabelAlignment)
|
||||||
|
{
|
||||||
|
case LabelAlignment.Left:
|
||||||
|
positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos;
|
||||||
|
alignment = AlignmentType.BottomLeft;
|
||||||
|
break;
|
||||||
|
case LabelAlignment.Right:
|
||||||
|
positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin;
|
||||||
|
alignment = AlignmentType.BottomRight;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin;
|
||||||
|
alignment = AlignmentType.Bottom;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
positionX = 58 + config.LightfinderLabelOffsetX;
|
||||||
|
alignment = AlignmentType.Bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
positionY += config.LightfinderLabelOffsetY;
|
||||||
|
|
||||||
|
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
||||||
pNode->AtkResNode.SetUseDepthBasedPriority(true);
|
pNode->AtkResNode.SetUseDepthBasedPriority(true);
|
||||||
pNode->AtkResNode.SetScale(0.5f, 0.5f);
|
|
||||||
|
|
||||||
pNode->AtkResNode.Color.A = 255;
|
pNode->AtkResNode.Color.A = 255;
|
||||||
|
|
||||||
@@ -227,18 +432,98 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
pNode->EdgeColor.B = (byte)(edgeColor.Z * 255);
|
pNode->EdgeColor.B = (byte)(edgeColor.Z * 255);
|
||||||
pNode->EdgeColor.A = (byte)(edgeColor.W * 255);
|
pNode->EdgeColor.A = (byte)(edgeColor.W * 255);
|
||||||
|
|
||||||
pNode->FontSize = 24;
|
|
||||||
pNode->AlignmentType = AlignmentType.Center;
|
if(!config.LightfinderLabelUseIcon)
|
||||||
pNode->FontType = FontType.MiedingerMed;
|
{
|
||||||
pNode->LineSpacing = 24;
|
pNode->AlignmentType = AlignmentType.Bottom;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pNode->AlignmentType = alignment;
|
||||||
|
}
|
||||||
|
pNode->AtkResNode.SetPositionShort(
|
||||||
|
(short)System.Math.Clamp(positionX, short.MinValue, short.MaxValue),
|
||||||
|
(short)System.Math.Clamp(positionY, short.MinValue, short.MaxValue)
|
||||||
|
);
|
||||||
|
var computedLineSpacing = (int)System.Math.Round(24 * scaleMultiplier);
|
||||||
|
pNode->LineSpacing = (byte)System.Math.Clamp(computedLineSpacing, 0, byte.MaxValue);
|
||||||
pNode->CharSpacing = 1;
|
pNode->CharSpacing = 1;
|
||||||
|
pNode->TextFlags = config.LightfinderLabelUseIcon
|
||||||
pNode->TextFlags = TextFlags.Edge | TextFlags.Glare;
|
? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize
|
||||||
|
: TextFlags.Edge | TextFlags.Glare;
|
||||||
pNode->SetText("Lightfinder");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static unsafe int GetScaledTextHeight(AtkTextNode* node)
|
||||||
|
{
|
||||||
|
if (node == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var resNode = &node->AtkResNode;
|
||||||
|
var rawHeight = (int)resNode->GetHeight();
|
||||||
|
if (rawHeight <= 0 && node->LineSpacing > 0)
|
||||||
|
rawHeight = node->LineSpacing;
|
||||||
|
if (rawHeight <= 0)
|
||||||
|
rawHeight = AtkNodeHelpers.DefaultTextNodeHeight;
|
||||||
|
|
||||||
|
var scale = resNode->ScaleY;
|
||||||
|
if (scale <= 0f)
|
||||||
|
scale = 1f;
|
||||||
|
|
||||||
|
var computed = (int)System.Math.Round(rawHeight * scale);
|
||||||
|
return System.Math.Max(1, computed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe int GetScaledTextWidth(AtkTextNode* node)
|
||||||
|
{
|
||||||
|
if (node == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var resNode = &node->AtkResNode;
|
||||||
|
var rawWidth = (int)resNode->GetWidth();
|
||||||
|
if (rawWidth <= 0)
|
||||||
|
rawWidth = AtkNodeHelpers.DefaultTextNodeWidth;
|
||||||
|
|
||||||
|
var scale = resNode->ScaleX;
|
||||||
|
if (scale <= 0f)
|
||||||
|
scale = 1f;
|
||||||
|
|
||||||
|
var computed = (int)System.Math.Round(rawWidth * scale);
|
||||||
|
return System.Math.Max(1, computed);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string NormalizeIconGlyph(string? rawInput)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(rawInput))
|
||||||
|
return DefaultIconGlyph;
|
||||||
|
|
||||||
|
var trimmed = rawInput.Trim();
|
||||||
|
|
||||||
|
if (Enum.TryParse<SeIconChar>(trimmed, true, out var iconEnum))
|
||||||
|
return SeIconCharExtensions.ToIconString(iconEnum);
|
||||||
|
|
||||||
|
var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? trimmed[2..]
|
||||||
|
: trimmed;
|
||||||
|
|
||||||
|
if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue))
|
||||||
|
return char.ConvertFromUtf32(hexValue);
|
||||||
|
|
||||||
|
var enumerator = trimmed.EnumerateRunes();
|
||||||
|
if (enumerator.MoveNext())
|
||||||
|
return enumerator.Current.ToString();
|
||||||
|
|
||||||
|
return DefaultIconGlyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string ToIconEditorString(string? rawInput)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeIconGlyph(rawInput);
|
||||||
|
var runeEnumerator = normalized.EnumerateRunes();
|
||||||
|
return runeEnumerator.MoveNext()
|
||||||
|
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
|
||||||
|
: DefaultIconGlyph;
|
||||||
|
}
|
||||||
private void HideNameplateTextNode(int i)
|
private void HideNameplateTextNode(int i)
|
||||||
{
|
{
|
||||||
var pNode = mTextNodes[i];
|
var pNode = mTextNodes[i];
|
||||||
@@ -267,6 +552,9 @@ 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 => [.. _pairManager.GetOnlineUserPairs()
|
||||||
|
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||||
|
.Select(u => (ulong)u.PlayerCharacterId)];
|
||||||
|
|
||||||
public void FlagRefresh()
|
public void FlagRefresh()
|
||||||
{
|
{
|
||||||
@@ -298,4 +586,12 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
|
|
||||||
FlagRefresh();
|
FlagRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ClearNameplateCaches()
|
||||||
|
{
|
||||||
|
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
||||||
|
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
||||||
|
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
||||||
|
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
|||||||
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
|
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
|
||||||
_namePlateGui.RequestRedraw();
|
_namePlateGui.RequestRedraw();
|
||||||
Mediator.Subscribe<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw());
|
Mediator.Subscribe<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||||
|
|||||||
220
LightlessSync/Services/PairProcessingLimiter.cs
Normal file
220
LightlessSync/Services/PairProcessingLimiter.cs
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
|
public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private const int HardLimit = 32;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
|
private readonly object _limitLock = new();
|
||||||
|
private readonly SemaphoreSlim _semaphore;
|
||||||
|
private int _currentLimit;
|
||||||
|
private int _pendingReductions;
|
||||||
|
private int _waiting;
|
||||||
|
private int _inFlight;
|
||||||
|
|
||||||
|
public PairProcessingLimiter(ILogger<PairProcessingLimiter> logger, LightlessMediator mediator, LightlessConfigService configService)
|
||||||
|
: base(logger, mediator)
|
||||||
|
{
|
||||||
|
_configService = configService;
|
||||||
|
_currentLimit = CalculateLimit();
|
||||||
|
var initialCount = _configService.Current.EnablePairProcessingLimiter ? _currentLimit : HardLimit;
|
||||||
|
_semaphore = new SemaphoreSlim(initialCount, HardLimit);
|
||||||
|
|
||||||
|
Mediator.Subscribe<PairProcessingLimitChangedMessage>(this, _ => UpdateSemaphoreLimit());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<IAsyncDisposable> AcquireAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return WaitInternalAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PairProcessingLimiterSnapshot GetSnapshot()
|
||||||
|
{
|
||||||
|
lock (_limitLock)
|
||||||
|
{
|
||||||
|
var enabled = IsEnabled;
|
||||||
|
var limit = enabled ? _currentLimit : CalculateLimit();
|
||||||
|
var waiting = Math.Max(0, Volatile.Read(ref _waiting));
|
||||||
|
var inFlight = Math.Max(0, Volatile.Read(ref _inFlight));
|
||||||
|
return new PairProcessingLimiterSnapshot(enabled, limit, inFlight, waiting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsEnabled => _configService.Current.EnablePairProcessingLimiter;
|
||||||
|
|
||||||
|
private async ValueTask<IAsyncDisposable> WaitInternalAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
if (!IsEnabled)
|
||||||
|
{
|
||||||
|
return NoopReleaser.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _waiting);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _semaphore.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Interlocked.Decrement(ref _waiting);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Decrement(ref _waiting);
|
||||||
|
|
||||||
|
if (!IsEnabled)
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
return NoopReleaser.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _inFlight);
|
||||||
|
return new Releaser(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSemaphoreLimit()
|
||||||
|
{
|
||||||
|
lock (_limitLock)
|
||||||
|
{
|
||||||
|
var enabled = IsEnabled;
|
||||||
|
var desiredLimit = CalculateLimit();
|
||||||
|
|
||||||
|
if (!enabled)
|
||||||
|
{
|
||||||
|
var releaseAmount = HardLimit - _semaphore.CurrentCount;
|
||||||
|
if (releaseAmount > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_semaphore.Release(releaseAmount);
|
||||||
|
}
|
||||||
|
catch (SemaphoreFullException)
|
||||||
|
{
|
||||||
|
// ignore, already at max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentLimit = desiredLimit;
|
||||||
|
_pendingReductions = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (desiredLimit == _currentLimit)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (desiredLimit > _currentLimit)
|
||||||
|
{
|
||||||
|
var increment = desiredLimit - _currentLimit;
|
||||||
|
var allowed = Math.Min(increment, HardLimit - _semaphore.CurrentCount);
|
||||||
|
if (allowed > 0)
|
||||||
|
{
|
||||||
|
_semaphore.Release(allowed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var decrement = _currentLimit - desiredLimit;
|
||||||
|
var removed = 0;
|
||||||
|
while (removed < decrement && _semaphore.Wait(0))
|
||||||
|
{
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remaining = decrement - removed;
|
||||||
|
if (remaining > 0)
|
||||||
|
{
|
||||||
|
_pendingReductions += remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentLimit = desiredLimit;
|
||||||
|
Logger.LogDebug("Pair processing concurrency updated to {limit} (pending reductions: {pending})", _currentLimit, _pendingReductions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int CalculateLimit()
|
||||||
|
{
|
||||||
|
var configured = _configService.Current.MaxConcurrentPairApplications;
|
||||||
|
return Math.Clamp(configured, 1, HardLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReleaseOne()
|
||||||
|
{
|
||||||
|
var inFlight = Interlocked.Decrement(ref _inFlight);
|
||||||
|
if (inFlight < 0)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _inFlight, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsEnabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_limitLock)
|
||||||
|
{
|
||||||
|
if (_pendingReductions > 0)
|
||||||
|
{
|
||||||
|
_pendingReductions--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
if (!disposing)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_semaphore.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Releaser : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private PairProcessingLimiter? _owner;
|
||||||
|
|
||||||
|
public Releaser(PairProcessingLimiter owner)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
var owner = Interlocked.Exchange(ref _owner, null);
|
||||||
|
owner?.ReleaseOne();
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NoopReleaser : IAsyncDisposable
|
||||||
|
{
|
||||||
|
public static readonly NoopReleaser Instance = new();
|
||||||
|
|
||||||
|
private NoopReleaser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct PairProcessingLimiterSnapshot(bool IsEnabled, int Limit, int InFlight, int Waiting)
|
||||||
|
{
|
||||||
|
public int Remaining => Math.Max(0, Limit - InFlight);
|
||||||
|
}
|
||||||
@@ -504,7 +504,7 @@ public class ServerConfigurationManager
|
|||||||
|
|
||||||
internal void RenameTag(Dictionary<string, List<string>> tags, HashSet<string> storage, string oldName, string newName)
|
internal void RenameTag(Dictionary<string, List<string>> tags, HashSet<string> storage, string oldName, string newName)
|
||||||
{
|
{
|
||||||
if (newName.Length > _maxCharactersFolder)
|
if (newName.Length < _maxCharactersFolder)
|
||||||
{
|
{
|
||||||
storage.Remove(oldName);
|
storage.Remove(oldName);
|
||||||
storage.Add(newName);
|
storage.Add(newName);
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ namespace LightlessSync.UI
|
|||||||
MinimumSize = new(600, 465),
|
MinimumSize = new(600, 465),
|
||||||
MaximumSize = new(750, 525)
|
MaximumSize = new(750, 525)
|
||||||
};
|
};
|
||||||
|
|
||||||
mediator.Subscribe<RefreshUiMessage>(this, async _ => await RefreshSyncshells().ConfigureAwait(false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RebuildSyncshellDropdownOptions()
|
private void RebuildSyncshellDropdownOptions()
|
||||||
@@ -121,7 +119,7 @@ namespace LightlessSync.UI
|
|||||||
public override void OnOpen()
|
public override void OnOpen()
|
||||||
{
|
{
|
||||||
_userUid = _apiController.UID;
|
_userUid = _apiController.UID;
|
||||||
_ = RefreshSyncshellsInternal();
|
_ = RefreshSyncshells();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
@@ -268,6 +266,7 @@ namespace LightlessSync.UI
|
|||||||
if (_allSyncshells == null)
|
if (_allSyncshells == null)
|
||||||
{
|
{
|
||||||
ImGui.Text("Loading Syncshells...");
|
ImGui.Text("Loading Syncshells...");
|
||||||
|
_ = RefreshSyncshells();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,6 +318,7 @@ namespace LightlessSync.UI
|
|||||||
{
|
{
|
||||||
_configService.Current.SelectedFinderSyncshell = gid;
|
_configService.Current.SelectedFinderSyncshell = gid;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
|
_ = RefreshSyncshells();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!available && ImGui.IsItemHovered())
|
if (!available && ImGui.IsItemHovered())
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using System;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
@@ -376,7 +377,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
private void DrawTransfers()
|
private void DrawTransfers()
|
||||||
{
|
{
|
||||||
var currentUploads = _fileTransferManager.CurrentUploads.ToList();
|
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot();
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Upload);
|
_uiSharedService.IconText(FontAwesomeIcon.Upload);
|
||||||
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
||||||
@@ -386,10 +387,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
var totalUploads = currentUploads.Count;
|
var totalUploads = currentUploads.Count;
|
||||||
|
|
||||||
var doneUploads = currentUploads.Count(c => c.IsTransferred);
|
var doneUploads = currentUploads.Count(c => c.IsTransferred);
|
||||||
|
var activeUploads = currentUploads.Count(c => !c.IsTransferred);
|
||||||
|
var uploadSlotLimit = Math.Clamp(_configService.Current.ParallelUploads, 1, 8);
|
||||||
var totalUploaded = currentUploads.Sum(c => c.Transferred);
|
var totalUploaded = currentUploads.Sum(c => c.Transferred);
|
||||||
var totalToUpload = currentUploads.Sum(c => c.Total);
|
var totalToUpload = currentUploads.Sum(c => c.Total);
|
||||||
|
|
||||||
ImGui.TextUnformatted($"{doneUploads}/{totalUploads}");
|
ImGui.TextUnformatted($"{doneUploads}/{totalUploads} (slots {activeUploads}/{uploadSlotLimit})");
|
||||||
var uploadText = $"({UiSharedService.ByteToString(totalUploaded)}/{UiSharedService.ByteToString(totalToUpload)})";
|
var uploadText = $"({UiSharedService.ByteToString(totalUploaded)}/{UiSharedService.ByteToString(totalToUpload)})";
|
||||||
var textSize = ImGui.CalcTextSize(uploadText);
|
var textSize = ImGui.CalcTextSize(uploadText);
|
||||||
ImGui.SameLine(_windowContentWidth - textSize.X);
|
ImGui.SameLine(_windowContentWidth - textSize.X);
|
||||||
@@ -488,7 +491,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
float contentWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
float contentWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
||||||
float uidStartX = (contentWidth - uidTextSize.X) / 2f;
|
float uidStartX = (contentWidth - uidTextSize.X) / 2f;
|
||||||
float cursorY = ImGui.GetCursorPosY();
|
float cursorY = ImGui.GetCursorPosY();
|
||||||
|
|
||||||
if (_configService.Current.BroadcastEnabled && _apiController.IsConnected)
|
if (_configService.Current.BroadcastEnabled && _apiController.IsConnected)
|
||||||
{
|
{
|
||||||
@@ -619,7 +622,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPosY(cursorY + 15f);
|
ImGui.SetCursorPosY(cursorY + 15f);
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
|
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
|
||||||
|
|
||||||
string warningMessage = "";
|
string warningMessage = "";
|
||||||
if (isOverTriHold)
|
if (isOverTriHold)
|
||||||
{
|
{
|
||||||
@@ -671,7 +674,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
UiSharedService.AttachToolTip("Click to copy");
|
UiSharedService.AttachToolTip("Click to copy");
|
||||||
if (uidFooterClicked)
|
if (uidFooterClicked)
|
||||||
{
|
{
|
||||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
|
ImGui.SetClipboardText(_apiController.UID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -825,7 +828,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
.Where(u => FilterGroupUsers(u.Value, group)));
|
.Where(u => FilterGroupUsers(u.Value, group)));
|
||||||
|
|
||||||
filteredGroupPairs = filteredPairs
|
filteredGroupPairs = filteredPairs
|
||||||
.Where(u => FilterGroupUsers( u.Value, group) && FilterOnlineOrPausedSelf(u.Key))
|
.Where(u => FilterGroupUsers(u.Value, group) && FilterOnlineOrPausedSelf(u.Key))
|
||||||
.OrderByDescending(u => u.Key.IsOnline)
|
.OrderByDescending(u => u.Key.IsOnline)
|
||||||
.ThenBy(u =>
|
.ThenBy(u =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ public class DrawUserPair
|
|||||||
using (ImRaii.PushColor(ImGuiCol.Text, roleColor))
|
using (ImRaii.PushColor(ImGuiCol.Text, roleColor))
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted(_pair.UserData.IsAdmin
|
ImGui.TextUnformatted(_pair.UserData.IsAdmin
|
||||||
? "Official Lightless Admin"
|
? "Official Lightless Developer"
|
||||||
: "Official Lightless Moderator");
|
: "Official Lightless Moderator");
|
||||||
}
|
}
|
||||||
ImGui.EndTooltip();
|
ImGui.EndTooltip();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using System;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
@@ -19,14 +20,16 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly FileUploadManager _fileTransferManager;
|
private readonly FileUploadManager _fileTransferManager;
|
||||||
private readonly UiSharedService _uiShared;
|
private readonly UiSharedService _uiShared;
|
||||||
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
|
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
|
||||||
|
|
||||||
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
|
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
|
||||||
FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService)
|
PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService)
|
||||||
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
|
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
|
||||||
{
|
{
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
_fileTransferManager = fileTransferManager;
|
_fileTransferManager = fileTransferManager;
|
||||||
_uiShared = uiShared;
|
_uiShared = uiShared;
|
||||||
|
|
||||||
@@ -73,11 +76,25 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (_configService.Current.ShowTransferWindow)
|
if (_configService.Current.ShowTransferWindow)
|
||||||
{
|
{
|
||||||
|
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();
|
||||||
|
if (limiterSnapshot.IsEnabled)
|
||||||
|
{
|
||||||
|
var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey;
|
||||||
|
var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}";
|
||||||
|
queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)";
|
||||||
|
UiSharedService.DrawOutlinedFont(queueText, queueColor, new Vector4(0, 0, 0, 255), 1);
|
||||||
|
ImGui.NewLine();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UiSharedService.DrawOutlinedFont("Pair apply limiter disabled", ImGuiColors.DalamudGrey, new Vector4(0, 0, 0, 255), 1);
|
||||||
|
ImGui.NewLine();
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_fileTransferManager.CurrentUploads.Any())
|
if (_fileTransferManager.IsUploading)
|
||||||
{
|
{
|
||||||
var currentUploads = _fileTransferManager.CurrentUploads.ToList();
|
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot();
|
||||||
var totalUploads = currentUploads.Count;
|
var totalUploads = currentUploads.Count;
|
||||||
|
|
||||||
var doneUploads = currentUploads.Count(c => c.IsTransferred);
|
var doneUploads = currentUploads.Count(c => c.IsTransferred);
|
||||||
@@ -214,7 +231,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (_uiShared.EditTrackerPosition) return true;
|
if (_uiShared.EditTrackerPosition) return true;
|
||||||
if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return false;
|
if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return false;
|
||||||
if (!_currentDownloads.Any() && !_fileTransferManager.CurrentUploads.Any() && !_uploadingPlayers.Any()) return false;
|
if (!_currentDownloads.Any() && !_fileTransferManager.IsUploading && !_uploadingPlayers.Any()) return false;
|
||||||
if (!IsOpen) return false;
|
if (!IsOpen) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
@@ -10,6 +11,7 @@ using LightlessSync.API.Routes;
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.LightlessConfiguration.Configurations;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
@@ -17,15 +19,18 @@ using LightlessSync.Services;
|
|||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
|
using LightlessSync.UtilsEnum.Enum;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using LightlessSync.WebAPI.Files;
|
using LightlessSync.WebAPI.Files;
|
||||||
using LightlessSync.WebAPI.Files.Models;
|
using LightlessSync.WebAPI.Files.Models;
|
||||||
using LightlessSync.WebAPI.SignalR.Utils;
|
using LightlessSync.WebAPI.SignalR.Utils;
|
||||||
using Microsoft.AspNetCore.Http.Connections;
|
using Microsoft.AspNetCore.Http.Connections;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
@@ -50,10 +55,12 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
private readonly PerformanceCollectorService _performanceCollector;
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||||
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly UiSharedService _uiShared;
|
private readonly UiSharedService _uiShared;
|
||||||
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
|
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
|
||||||
private readonly NameplateService _nameplateService;
|
private readonly NameplateService _nameplateService;
|
||||||
|
private readonly NameplateHandler _nameplateHandler;
|
||||||
private (int, int, FileCacheEntity) _currentProgress;
|
private (int, int, FileCacheEntity) _currentProgress;
|
||||||
private bool _deleteAccountPopupModalShown = false;
|
private bool _deleteAccountPopupModalShown = false;
|
||||||
private bool _deleteFilesPopupModalShown = false;
|
private bool _deleteFilesPopupModalShown = false;
|
||||||
@@ -63,6 +70,23 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
private bool _readClearCache = false;
|
private bool _readClearCache = false;
|
||||||
private int _selectedEntry = -1;
|
private int _selectedEntry = -1;
|
||||||
private string _uidToAddForIgnore = string.Empty;
|
private string _uidToAddForIgnore = string.Empty;
|
||||||
|
private string _lightfinderIconInput = string.Empty;
|
||||||
|
private bool _lightfinderIconInputInitialized = false;
|
||||||
|
private int _lightfinderIconPresetIndex = -1;
|
||||||
|
private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[]
|
||||||
|
{
|
||||||
|
("Link Marker", SeIconChar.LinkMarker),
|
||||||
|
("Hyadelyn", SeIconChar.Hyadelyn),
|
||||||
|
("Gil", SeIconChar.Gil),
|
||||||
|
("Quest Sync", SeIconChar.QuestSync),
|
||||||
|
("Glamoured", SeIconChar.Glamoured),
|
||||||
|
("Glamoured (Dyed)", SeIconChar.GlamouredDyed),
|
||||||
|
("Auto-Translate Open", SeIconChar.AutoTranslateOpen),
|
||||||
|
("Auto-Translate Close", SeIconChar.AutoTranslateClose),
|
||||||
|
("Boxed Star", SeIconChar.BoxedStar),
|
||||||
|
("Boxed Plus", SeIconChar.BoxedPlus)
|
||||||
|
};
|
||||||
|
|
||||||
private CancellationTokenSource? _validationCts;
|
private CancellationTokenSource? _validationCts;
|
||||||
private Task<List<FileCacheEntity>>? _validationTask;
|
private Task<List<FileCacheEntity>>? _validationTask;
|
||||||
private bool _wasOpen = false;
|
private bool _wasOpen = false;
|
||||||
@@ -72,6 +96,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
PairManager pairManager,
|
PairManager pairManager,
|
||||||
ServerConfigurationManager serverConfigurationManager,
|
ServerConfigurationManager serverConfigurationManager,
|
||||||
PlayerPerformanceConfigService playerPerformanceConfigService,
|
PlayerPerformanceConfigService playerPerformanceConfigService,
|
||||||
|
PairProcessingLimiter pairProcessingLimiter,
|
||||||
LightlessMediator mediator, PerformanceCollectorService performanceCollector,
|
LightlessMediator mediator, PerformanceCollectorService performanceCollector,
|
||||||
FileUploadManager fileTransferManager,
|
FileUploadManager fileTransferManager,
|
||||||
FileTransferOrchestrator fileTransferOrchestrator,
|
FileTransferOrchestrator fileTransferOrchestrator,
|
||||||
@@ -79,12 +104,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
FileCompactor fileCompactor, ApiController apiController,
|
FileCompactor fileCompactor, ApiController apiController,
|
||||||
IpcManager ipcManager, CacheMonitor cacheMonitor,
|
IpcManager ipcManager, CacheMonitor cacheMonitor,
|
||||||
DalamudUtilService dalamudUtilService, HttpClient httpClient,
|
DalamudUtilService dalamudUtilService, HttpClient httpClient,
|
||||||
NameplateService nameplateService) : base(logger, mediator, "Lightless Sync Settings", performanceCollector)
|
NameplateService nameplateService,
|
||||||
|
NameplateHandler nameplateHandler) : base(logger, mediator, "Lightless Sync Settings", performanceCollector)
|
||||||
{
|
{
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||||
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
_performanceCollector = performanceCollector;
|
_performanceCollector = performanceCollector;
|
||||||
_fileTransferManager = fileTransferManager;
|
_fileTransferManager = fileTransferManager;
|
||||||
_fileTransferOrchestrator = fileTransferOrchestrator;
|
_fileTransferOrchestrator = fileTransferOrchestrator;
|
||||||
@@ -97,6 +124,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
_uiShared = uiShared;
|
_uiShared = uiShared;
|
||||||
_nameplateService = nameplateService;
|
_nameplateService = nameplateService;
|
||||||
|
_nameplateHandler = nameplateHandler;
|
||||||
AllowClickthrough = false;
|
AllowClickthrough = false;
|
||||||
AllowPinning = true;
|
AllowPinning = true;
|
||||||
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
||||||
@@ -218,6 +246,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|
||||||
int maxParallelDownloads = _configService.Current.ParallelDownloads;
|
int maxParallelDownloads = _configService.Current.ParallelDownloads;
|
||||||
|
int maxParallelUploads = _configService.Current.ParallelUploads;
|
||||||
|
int maxPairApplications = _configService.Current.MaxConcurrentPairApplications;
|
||||||
|
bool limitPairApplications = _configService.Current.EnablePairProcessingLimiter;
|
||||||
bool useAlternativeUpload = _configService.Current.UseAlternativeFileUpload;
|
bool useAlternativeUpload = _configService.Current.UseAlternativeFileUpload;
|
||||||
int downloadSpeedLimit = _configService.Current.DownloadSpeedLimitInBytes;
|
int downloadSpeedLimit = _configService.Current.DownloadSpeedLimitInBytes;
|
||||||
|
|
||||||
@@ -254,7 +285,60 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_configService.Current.ParallelDownloads = maxParallelDownloads;
|
_configService.Current.ParallelDownloads = maxParallelDownloads;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
|
Mediator.Publish(new DownloadLimitChangedMessage());
|
||||||
}
|
}
|
||||||
|
_uiShared.DrawHelpText("Controls how many download slots can be active at once.");
|
||||||
|
|
||||||
|
if (ImGui.SliderInt("Maximum Parallel Uploads", ref maxParallelUploads, 1, 8))
|
||||||
|
{
|
||||||
|
_configService.Current.ParallelUploads = maxParallelUploads;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("Controls how many uploads can run at once.");
|
||||||
|
|
||||||
|
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
||||||
|
|
||||||
|
if (ImGui.Checkbox("Enable Pair Download Limiter", ref limitPairApplications))
|
||||||
|
{
|
||||||
|
_configService.Current.EnablePairProcessingLimiter = limitPairApplications;
|
||||||
|
_configService.Save();
|
||||||
|
Mediator.Publish(new PairProcessingLimitChangedMessage());
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("When enabled we stagger pair downloads to avoid large network and game lag caused by attempting to download everyone at once.");
|
||||||
|
|
||||||
|
var limiterDisabledScope = !limitPairApplications;
|
||||||
|
if (limiterDisabledScope)
|
||||||
|
{
|
||||||
|
ImGui.BeginDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.SliderInt("Maximum Concurrent Pair Downloads", ref maxPairApplications, 1, 6))
|
||||||
|
{
|
||||||
|
_configService.Current.MaxConcurrentPairApplications = maxPairApplications;
|
||||||
|
_configService.Save();
|
||||||
|
Mediator.Publish(new PairProcessingLimitChangedMessage());
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("How many pair downloads/applications can run simultaneously when the limit is on.");
|
||||||
|
|
||||||
|
if (limiterDisabledScope)
|
||||||
|
{
|
||||||
|
ImGui.EndDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();
|
||||||
|
if (limiterSnapshot.IsEnabled)
|
||||||
|
{
|
||||||
|
var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey;
|
||||||
|
var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}";
|
||||||
|
queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)";
|
||||||
|
ImGui.TextColored(queueColor, queueText);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "Pair apply limiter is disabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
||||||
|
|
||||||
if (ImGui.Checkbox("Use Alternative Upload Method", ref useAlternativeUpload))
|
if (ImGui.Checkbox("Use Alternative Upload Method", ref useAlternativeUpload))
|
||||||
{
|
{
|
||||||
@@ -409,25 +493,33 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (ApiController.ServerState is ServerState.Connected && ImGui.BeginTabItem("Transfers"))
|
if (ApiController.ServerState is ServerState.Connected && ImGui.BeginTabItem("Transfers"))
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted("Uploads");
|
var uploadsSnapshot = _fileTransferManager.GetCurrentUploadsSnapshot();
|
||||||
|
var activeUploads = uploadsSnapshot.Count(c => !c.IsTransferred);
|
||||||
|
var uploadSlotLimit = Math.Clamp(_configService.Current.ParallelUploads, 1, 8);
|
||||||
|
ImGui.TextUnformatted($"Uploads (slots {activeUploads}/{uploadSlotLimit})");
|
||||||
if (ImGui.BeginTable("UploadsTable", 3))
|
if (ImGui.BeginTable("UploadsTable", 3))
|
||||||
{
|
{
|
||||||
ImGui.TableSetupColumn("File");
|
ImGui.TableSetupColumn("File");
|
||||||
ImGui.TableSetupColumn("Uploaded");
|
ImGui.TableSetupColumn("Uploaded");
|
||||||
ImGui.TableSetupColumn("Size");
|
ImGui.TableSetupColumn("Size");
|
||||||
ImGui.TableHeadersRow();
|
ImGui.TableHeadersRow();
|
||||||
foreach (var transfer in _fileTransferManager.CurrentUploads.ToArray())
|
foreach (var transfer in uploadsSnapshot)
|
||||||
{
|
{
|
||||||
var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total));
|
var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total));
|
||||||
var col = ImRaii.PushColor(ImGuiCol.Text, color);
|
using var col = ImRaii.PushColor(ImGuiCol.Text, color);
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.TextUnformatted(transfer.Hash);
|
if (transfer is UploadFileTransfer uploadTransfer)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(uploadTransfer.LocalFile);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(transfer.Hash);
|
||||||
|
}
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Transferred));
|
ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Transferred));
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Total));
|
ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Total));
|
||||||
col.Dispose();
|
|
||||||
ImGui.TableNextRow();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.EndTable();
|
ImGui.EndTable();
|
||||||
@@ -940,7 +1032,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_configService.Current.EnableRightClickMenus = enableRightClickMenu;
|
_configService.Current.EnableRightClickMenus = enableRightClickMenu;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
}
|
}
|
||||||
_uiShared.DrawHelpText("This will add Lightless related right click menu entries in the game UI on paired players.");
|
_uiShared.DrawHelpText("This will add all Lightless related right click menu entries in the game UI.");
|
||||||
|
|
||||||
if (ImGui.Checkbox("Display status and visible pair count in Server Info Bar", ref enableDtrEntry))
|
if (ImGui.Checkbox("Display status and visible pair count in Server Info Bar", ref enableDtrEntry))
|
||||||
{
|
{
|
||||||
@@ -972,6 +1064,261 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
|
if (_uiShared.MediumTreeNode("Lightfinder", UIColors.Get("LightlessPurple")))
|
||||||
|
{
|
||||||
|
var autoAlign = _configService.Current.LightfinderAutoAlign;
|
||||||
|
var offsetX = (int)_configService.Current.LightfinderLabelOffsetX;
|
||||||
|
var offsetY = (int)_configService.Current.LightfinderLabelOffsetY;
|
||||||
|
var labelScale = _configService.Current.LightfinderLabelScale;
|
||||||
|
|
||||||
|
ImGui.TextUnformatted("Alignment");
|
||||||
|
ImGui.BeginDisabled(autoAlign);
|
||||||
|
if (ImGui.SliderInt("Label Offset X", ref offsetX, -200, 200))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelOffsetX = (short)offsetX;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelOffsetX = 0;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Right click to reset to default.");
|
||||||
|
ImGui.EndDisabled();
|
||||||
|
_uiShared.DrawHelpText("Moves the Lightfinder label horizontally on player nameplates.\nUnavailable when automatic alignment is enabled.");
|
||||||
|
|
||||||
|
|
||||||
|
if (ImGui.SliderInt("Label Offset Y", ref offsetY, -200, 200))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelOffsetY = (short)offsetY;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelOffsetY = 0;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Right click to reset to default.");
|
||||||
|
_uiShared.DrawHelpText("Moves the Lightfinder label vertically on player nameplates.");
|
||||||
|
|
||||||
|
if (ImGui.SliderFloat("Label Size", ref labelScale, 0.5f, 2.0f, "%.2fx"))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelScale = labelScale;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelScale = 1.0f;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Right click to reset to default.");
|
||||||
|
_uiShared.DrawHelpText("Adjusts the Lightfinder label size for both text and icon modes.");
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(8));
|
||||||
|
|
||||||
|
if (ImGui.Checkbox("Automatically align with nameplate", ref autoAlign))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderAutoAlign = autoAlign;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("Automatically position the label relative to the in-game nameplate. Turn off to rely entirely on manual offsets.");
|
||||||
|
|
||||||
|
if (autoAlign)
|
||||||
|
{
|
||||||
|
var alignmentOption = _configService.Current.LabelAlignment;
|
||||||
|
var alignmentLabel = alignmentOption switch
|
||||||
|
{
|
||||||
|
LabelAlignment.Left => "Left",
|
||||||
|
LabelAlignment.Right => "Right",
|
||||||
|
_ => "Center",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ImGui.BeginCombo("Horizontal Alignment", alignmentLabel))
|
||||||
|
{
|
||||||
|
foreach (LabelAlignment option in Enum.GetValues<LabelAlignment>())
|
||||||
|
{
|
||||||
|
var optionLabel = option switch
|
||||||
|
{
|
||||||
|
LabelAlignment.Left => "Left",
|
||||||
|
LabelAlignment.Right => "Right",
|
||||||
|
_ => "Center",
|
||||||
|
};
|
||||||
|
var selected = option == alignmentOption;
|
||||||
|
if (ImGui.Selectable(optionLabel, selected))
|
||||||
|
{
|
||||||
|
_configService.Current.LabelAlignment = option;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected)
|
||||||
|
ImGui.SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndCombo();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
|
||||||
|
|
||||||
|
ImGui.TextUnformatted("Visibility");
|
||||||
|
var showOwn = _configService.Current.LightfinderLabelShowOwn;
|
||||||
|
if (ImGui.Checkbox("Show your own Lightfinder label", ref showOwn))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelShowOwn = showOwn;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("Toggles your own Lightfinder label.");
|
||||||
|
|
||||||
|
var showPaired = _configService.Current.LightfinderLabelShowPaired;
|
||||||
|
if (ImGui.Checkbox("Show paired player(s) Lightfinder label", ref showPaired))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelShowPaired = showPaired;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("Toggles paired player(s) Lightfinder label.");
|
||||||
|
|
||||||
|
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
|
||||||
|
|
||||||
|
ImGui.TextUnformatted("Label");
|
||||||
|
var useIcon = _configService.Current.LightfinderLabelUseIcon;
|
||||||
|
if (ImGui.Checkbox("Show icon instead of text", ref useIcon))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelUseIcon = useIcon;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
|
||||||
|
if (useIcon)
|
||||||
|
{
|
||||||
|
RefreshLightfinderIconState();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_lightfinderIconInputInitialized = false;
|
||||||
|
_lightfinderIconPresetIndex = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("Switch between the Lightfinder text label and an icon on nameplates.");
|
||||||
|
|
||||||
|
if (useIcon)
|
||||||
|
{
|
||||||
|
if (!_lightfinderIconInputInitialized)
|
||||||
|
{
|
||||||
|
RefreshLightfinderIconState();
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentPresetLabel = _lightfinderIconPresetIndex >= 0
|
||||||
|
? $"{GetLightfinderPresetGlyph(_lightfinderIconPresetIndex)} {LightfinderIconPresets[_lightfinderIconPresetIndex].Label}"
|
||||||
|
: "Custom";
|
||||||
|
|
||||||
|
if (ImGui.BeginCombo("Preset Icon", currentPresetLabel))
|
||||||
|
{
|
||||||
|
for (int i = 0; i < LightfinderIconPresets.Length; i++)
|
||||||
|
{
|
||||||
|
var optionGlyph = GetLightfinderPresetGlyph(i);
|
||||||
|
var preview = $"{optionGlyph} {LightfinderIconPresets[i].Label}";
|
||||||
|
var selected = i == _lightfinderIconPresetIndex;
|
||||||
|
if (ImGui.Selectable(preview, selected))
|
||||||
|
{
|
||||||
|
ApplyLightfinderIcon(optionGlyph, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.Selectable("Custom", _lightfinderIconPresetIndex == -1))
|
||||||
|
{
|
||||||
|
_lightfinderIconPresetIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndCombo();
|
||||||
|
}
|
||||||
|
|
||||||
|
var editorBuffer = _lightfinderIconInput;
|
||||||
|
if (ImGui.InputText("Icon Glyph", ref editorBuffer, 16))
|
||||||
|
{
|
||||||
|
_lightfinderIconInput = editorBuffer;
|
||||||
|
_lightfinderIconPresetIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.Button("Apply Icon"))
|
||||||
|
{
|
||||||
|
var normalized = NameplateHandler.NormalizeIconGlyph(_lightfinderIconInput);
|
||||||
|
ApplyLightfinderIcon(normalized, _lightfinderIconPresetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Reset Icon"))
|
||||||
|
{
|
||||||
|
var defaultGlyph = NameplateHandler.NormalizeIconGlyph(null);
|
||||||
|
var defaultIndex = -1;
|
||||||
|
for (int i = 0; i < LightfinderIconPresets.Length; i++)
|
||||||
|
{
|
||||||
|
if (string.Equals(GetLightfinderPresetGlyph(i), defaultGlyph, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
defaultIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultIndex < 0)
|
||||||
|
{
|
||||||
|
defaultIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyLightfinderIcon(GetLightfinderPresetGlyph(defaultIndex), defaultIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
var previewGlyph = NameplateHandler.NormalizeIconGlyph(_lightfinderIconInput);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.Text($"Preview: {previewGlyph}");
|
||||||
|
_uiShared.DrawHelpText("Enter a hex code (e.g. E0BB), pick a preset, or paste an icon character directly.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_lightfinderIconInputInitialized = false;
|
||||||
|
_lightfinderIconPresetIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
||||||
|
ImGui.TreePop();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
|
||||||
if (_uiShared.MediumTreeNode("Colors", UIColors.Get("LightlessPurple")))
|
if (_uiShared.MediumTreeNode("Colors", UIColors.Get("LightlessPurple")))
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted("UI Theme Colors");
|
ImGui.TextUnformatted("UI Theme Colors");
|
||||||
@@ -2216,6 +2563,39 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
return (true, failedConversions.Count != 0, sb.ToString());
|
return (true, failedConversions.Count != 0, sb.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetLightfinderPresetGlyph(int index)
|
||||||
|
{
|
||||||
|
return NameplateHandler.NormalizeIconGlyph(SeIconCharExtensions.ToIconString(LightfinderIconPresets[index].Icon));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshLightfinderIconState()
|
||||||
|
{
|
||||||
|
var normalized = NameplateHandler.NormalizeIconGlyph(_configService.Current.LightfinderLabelIconGlyph);
|
||||||
|
_lightfinderIconInput = NameplateHandler.ToIconEditorString(normalized);
|
||||||
|
_lightfinderIconInputInitialized = true;
|
||||||
|
|
||||||
|
_lightfinderIconPresetIndex = -1;
|
||||||
|
for (int i = 0; i < LightfinderIconPresets.Length; i++)
|
||||||
|
{
|
||||||
|
if (string.Equals(GetLightfinderPresetGlyph(i), normalized, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_lightfinderIconPresetIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyLightfinderIcon(string normalizedGlyph, int presetIndex)
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelIconGlyph = normalizedGlyph;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
_lightfinderIconInput = NameplateHandler.ToIconEditorString(normalizedGlyph);
|
||||||
|
_lightfinderIconPresetIndex = presetIndex;
|
||||||
|
_lightfinderIconInputInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawSettingsContent()
|
private void DrawSettingsContent()
|
||||||
{
|
{
|
||||||
if (_apiController.ServerState is ServerState.Connected)
|
if (_apiController.ServerState is ServerState.Connected)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ 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 readonly HashSet<string> _recentlyJoined = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
private GroupJoinDto? _joinDto;
|
private GroupJoinDto? _joinDto;
|
||||||
private GroupJoinInfoDto? _joinInfo;
|
private GroupJoinInfoDto? _joinInfo;
|
||||||
@@ -120,6 +121,17 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
foreach (var shell in _nearbySyncshells)
|
foreach (var shell in _nearbySyncshells)
|
||||||
{
|
{
|
||||||
|
// 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)
|
||||||
|
continue; // no active broadcasts
|
||||||
|
|
||||||
|
var (Name, Address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID);
|
||||||
|
if (string.IsNullOrEmpty(Name))
|
||||||
|
continue; // broadcaster not found in area, skipping
|
||||||
|
|
||||||
ImGui.TableNextRow();
|
ImGui.TableNextRow();
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
|
|
||||||
@@ -127,19 +139,8 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
ImGui.TextUnformatted(displayName);
|
ImGui.TextUnformatted(displayName);
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
var broadcasterName = "Unknown";
|
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(Address);
|
||||||
var broadcast = _broadcastScannerService.GetActiveSyncshellBroadcasts()
|
var broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{Name} ({worldName})" : Name;
|
||||||
.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal));
|
|
||||||
|
|
||||||
if (broadcast != null)
|
|
||||||
{
|
|
||||||
var (Name, Address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID);
|
|
||||||
if (!string.IsNullOrEmpty(Name))
|
|
||||||
{
|
|
||||||
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(Address);
|
|
||||||
broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{Name} ({worldName})" : Name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui.TextUnformatted(broadcasterName);
|
ImGui.TextUnformatted(broadcasterName);
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
@@ -149,7 +150,10 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f));
|
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f));
|
||||||
|
|
||||||
if (!_currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal)))
|
var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal));
|
||||||
|
var isRecentlyJoined = _recentlyJoined.Contains(shell.GID);
|
||||||
|
|
||||||
|
if (!isAlreadyMember && !isRecentlyJoined)
|
||||||
{
|
{
|
||||||
if (ImGui.Button(label))
|
if (ImGui.Button(label))
|
||||||
{
|
{
|
||||||
@@ -224,9 +228,11 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX);
|
finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX);
|
||||||
|
|
||||||
_ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions));
|
_ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions));
|
||||||
|
|
||||||
|
_recentlyJoined.Add(_joinDto.Group.GID);
|
||||||
|
|
||||||
_joinDto = null;
|
_joinDto = null;
|
||||||
_joinInfo = null;
|
_joinInfo = null;
|
||||||
_ = RefreshSyncshellsAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,6 +267,8 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
|
var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
|
||||||
_currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)];
|
_currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)];
|
||||||
|
|
||||||
|
_recentlyJoined.RemoveWhere(gid => _currentSyncshells.Any(s => string.Equals(s.GID, gid, StringComparison.Ordinal)));
|
||||||
|
|
||||||
if (syncshellBroadcasts.Count == 0)
|
if (syncshellBroadcasts.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -284,11 +292,6 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
if (updatedList != null)
|
if (updatedList != null)
|
||||||
{
|
{
|
||||||
var newGids = updatedList.Select(s => s.Group.GID).ToHashSet(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
if (currentGids.SetEquals(newGids))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var previousGid = GetSelectedGid();
|
var previousGid = GetSelectedGid();
|
||||||
|
|
||||||
_nearbySyncshells.Clear();
|
_nearbySyncshells.Clear();
|
||||||
|
|||||||
15
LightlessSync/Utils/UtilsEnum/LabelAlignment.cs
Normal file
15
LightlessSync/Utils/UtilsEnum/LabelAlignment.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LightlessSync.UtilsEnum.Enum
|
||||||
|
{
|
||||||
|
public enum LabelAlignment
|
||||||
|
{
|
||||||
|
Left,
|
||||||
|
Center,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using K4os.Compression.LZ4.Legacy;
|
using K4os.Compression.LZ4.Legacy;
|
||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Dto.Files;
|
using LightlessSync.API.Dto.Files;
|
||||||
@@ -8,6 +8,7 @@ using LightlessSync.PlayerData.Handlers;
|
|||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.WebAPI.Files.Models;
|
using LightlessSync.WebAPI.Files.Models;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
private readonly FileCompactor _fileCompactor;
|
private readonly FileCompactor _fileCompactor;
|
||||||
private readonly FileCacheManager _fileDbManager;
|
private readonly FileCacheManager _fileDbManager;
|
||||||
private readonly FileTransferOrchestrator _orchestrator;
|
private readonly FileTransferOrchestrator _orchestrator;
|
||||||
private readonly List<ThrottledStream> _activeDownloadStreams;
|
private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams;
|
||||||
|
|
||||||
public FileDownloadManager(ILogger<FileDownloadManager> logger, LightlessMediator mediator,
|
public FileDownloadManager(ILogger<FileDownloadManager> logger, LightlessMediator mediator,
|
||||||
FileTransferOrchestrator orchestrator,
|
FileTransferOrchestrator orchestrator,
|
||||||
@@ -29,14 +30,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
_orchestrator = orchestrator;
|
_orchestrator = orchestrator;
|
||||||
_fileDbManager = fileCacheManager;
|
_fileDbManager = fileCacheManager;
|
||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
_activeDownloadStreams = [];
|
_activeDownloadStreams = new();
|
||||||
|
|
||||||
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
|
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
if (!_activeDownloadStreams.Any()) return;
|
if (_activeDownloadStreams.IsEmpty) return;
|
||||||
var newLimit = _orchestrator.DownloadLimitPerSlot();
|
var newLimit = _orchestrator.DownloadLimitPerSlot();
|
||||||
Logger.LogTrace("Setting new Download Speed Limit to {newLimit}", newLimit);
|
Logger.LogTrace("Setting new Download Speed Limit to {newLimit}", newLimit);
|
||||||
foreach (var stream in _activeDownloadStreams)
|
foreach (var stream in _activeDownloadStreams.Keys)
|
||||||
{
|
{
|
||||||
stream.BandwidthLimit = newLimit;
|
stream.BandwidthLimit = newLimit;
|
||||||
}
|
}
|
||||||
@@ -47,7 +48,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public List<FileTransfer> ForbiddenTransfers => _orchestrator.ForbiddenTransfers;
|
public List<FileTransfer> ForbiddenTransfers => _orchestrator.ForbiddenTransfers;
|
||||||
|
|
||||||
public bool IsDownloading => !CurrentDownloads.Any();
|
public bool IsDownloading => CurrentDownloads.Any();
|
||||||
|
|
||||||
public static void MungeBuffer(Span<byte> buffer)
|
public static void MungeBuffer(Span<byte> buffer)
|
||||||
{
|
{
|
||||||
@@ -84,7 +85,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
ClearDownload();
|
ClearDownload();
|
||||||
foreach (var stream in _activeDownloadStreams.ToList())
|
foreach (var stream in _activeDownloadStreams.Keys.ToList())
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -95,6 +96,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
// do nothing
|
// do nothing
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_activeDownloadStreams.TryRemove(stream, out _);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
@@ -142,7 +147,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
await WaitForDownloadReady(fileTransfer, requestId, ct).ConfigureAwait(false);
|
await WaitForDownloadReady(fileTransfer, requestId, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
_downloadStatus[downloadGroup].DownloadStatus = DownloadStatus.Downloading;
|
if (_downloadStatus.TryGetValue(downloadGroup, out var downloadStatus))
|
||||||
|
{
|
||||||
|
downloadStatus.DownloadStatus = DownloadStatus.Downloading;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Download status missing for {group} when starting download", downloadGroup);
|
||||||
|
}
|
||||||
|
|
||||||
const int maxRetries = 3;
|
const int maxRetries = 3;
|
||||||
int retryCount = 0;
|
int retryCount = 0;
|
||||||
@@ -204,7 +216,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
stream = new(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), limit);
|
stream = new(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), limit);
|
||||||
|
|
||||||
_activeDownloadStreams.Add(stream);
|
_activeDownloadStreams.TryAdd(stream, 0);
|
||||||
|
|
||||||
while ((bytesRead = await stream.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
|
while ((bytesRead = await stream.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
|
||||||
{
|
{
|
||||||
@@ -245,7 +257,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (stream != null)
|
if (stream != null)
|
||||||
{
|
{
|
||||||
_activeDownloadStreams.Remove(stream);
|
_activeDownloadStreams.TryRemove(stream, out _);
|
||||||
await stream.DisposeAsync().ConfigureAwait(false);
|
await stream.DisposeAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,11 +265,28 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public async Task<List<DownloadFileTransfer>> InitiateDownloadList(GameObjectHandler gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct)
|
public async Task<List<DownloadFileTransfer>> InitiateDownloadList(GameObjectHandler gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Download start: {id}", gameObjectHandler.Name);
|
var objectName = gameObjectHandler?.Name ?? "Unknown";
|
||||||
|
Logger.LogDebug("Download start: {id}", objectName);
|
||||||
|
|
||||||
|
if (fileReplacement == null || fileReplacement.Count == 0)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("{dlName}: No file replacements provided", objectName);
|
||||||
|
CurrentDownloads = [];
|
||||||
|
return CurrentDownloads;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hashes = fileReplacement.Where(f => f != null && !string.IsNullOrWhiteSpace(f.Hash)).Select(f => f.Hash).Distinct(StringComparer.Ordinal).ToList();
|
||||||
|
|
||||||
|
if (hashes.Count == 0)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("{dlName}: No valid hashes to download", objectName);
|
||||||
|
CurrentDownloads = [];
|
||||||
|
return CurrentDownloads;
|
||||||
|
}
|
||||||
|
|
||||||
List<DownloadFileDto> downloadFileInfoFromService =
|
List<DownloadFileDto> downloadFileInfoFromService =
|
||||||
[
|
[
|
||||||
.. await FilesGetSizes(fileReplacement.Select(f => f.Hash).Distinct(StringComparer.Ordinal).ToList(), ct).ConfigureAwait(false),
|
.. await FilesGetSizes(hashes, ct).ConfigureAwait(false),
|
||||||
];
|
];
|
||||||
|
|
||||||
Logger.LogDebug("Files with size 0 or less: {files}", string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash)));
|
Logger.LogDebug("Files with size 0 or less: {files}", string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash)));
|
||||||
@@ -315,15 +344,23 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
FileInfo fi = new(blockFile);
|
FileInfo fi = new(blockFile);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForSlot;
|
if (!_downloadStatus.TryGetValue(fileGroup.Key, out var downloadStatus))
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Download status missing for {group}, aborting", fileGroup.Key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadStatus.DownloadStatus = DownloadStatus.WaitingForSlot;
|
||||||
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
|
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
|
||||||
_downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForQueue;
|
downloadStatus.DownloadStatus = DownloadStatus.WaitingForQueue;
|
||||||
Progress<long> progress = new((bytesDownloaded) =>
|
Progress<long> progress = new((bytesDownloaded) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!_downloadStatus.TryGetValue(fileGroup.Key, out FileDownloadStatus? value)) return;
|
if (_downloadStatus.TryGetValue(fileGroup.Key, out FileDownloadStatus? value))
|
||||||
value.TransferredBytes += bytesDownloaded;
|
{
|
||||||
|
value.TransferredBytes += bytesDownloaded;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -353,6 +390,12 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
status.TransferredFiles = 1;
|
status.TransferredFiles = 1;
|
||||||
status.DownloadStatus = DownloadStatus.Decompressing;
|
status.DownloadStatus = DownloadStatus.Decompressing;
|
||||||
}
|
}
|
||||||
|
if (!File.Exists(blockFile))
|
||||||
|
{
|
||||||
|
Logger.LogWarning("{dlName}: Block file missing before extraction, skipping", fi.Name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fileBlockStream = File.OpenRead(blockFile);
|
fileBlockStream = File.OpenRead(blockFile);
|
||||||
while (fileBlockStream.Position < fileBlockStream.Length)
|
while (fileBlockStream.Position < fileBlockStream.Length)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Dto.Files;
|
using LightlessSync.API.Dto.Files;
|
||||||
using LightlessSync.API.Routes;
|
using LightlessSync.API.Routes;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
@@ -10,6 +10,8 @@ using LightlessSync.WebAPI.Files.Models;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace LightlessSync.WebAPI.Files;
|
namespace LightlessSync.WebAPI.Files;
|
||||||
|
|
||||||
@@ -19,7 +21,9 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
private readonly LightlessConfigService _lightlessConfigService;
|
private readonly LightlessConfigService _lightlessConfigService;
|
||||||
private readonly FileTransferOrchestrator _orchestrator;
|
private readonly FileTransferOrchestrator _orchestrator;
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
private readonly Dictionary<string, DateTime> _verifiedUploadedHashes = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, DateTime> _verifiedUploadedHashes = new(StringComparer.Ordinal);
|
||||||
|
private readonly object _currentUploadsLock = new();
|
||||||
|
private readonly Dictionary<string, FileTransfer> _currentUploadsByHash = new(StringComparer.Ordinal);
|
||||||
private CancellationTokenSource? _uploadCancellationTokenSource = new();
|
private CancellationTokenSource? _uploadCancellationTokenSource = new();
|
||||||
|
|
||||||
public FileUploadManager(ILogger<FileUploadManager> logger, LightlessMediator mediator,
|
public FileUploadManager(ILogger<FileUploadManager> logger, LightlessMediator mediator,
|
||||||
@@ -40,17 +44,38 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<FileTransfer> CurrentUploads { get; } = [];
|
public List<FileTransfer> CurrentUploads { get; } = [];
|
||||||
public bool IsUploading => CurrentUploads.Count > 0;
|
public bool IsUploading
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
return CurrentUploads.Count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FileTransfer> GetCurrentUploadsSnapshot()
|
||||||
|
{
|
||||||
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
return CurrentUploads.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool CancelUpload()
|
public bool CancelUpload()
|
||||||
{
|
{
|
||||||
if (CurrentUploads.Any())
|
if (IsUploading)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Cancelling current upload");
|
Logger.LogDebug("Cancelling current upload");
|
||||||
_uploadCancellationTokenSource?.Cancel();
|
_uploadCancellationTokenSource?.Cancel();
|
||||||
_uploadCancellationTokenSource?.Dispose();
|
_uploadCancellationTokenSource?.Dispose();
|
||||||
_uploadCancellationTokenSource = null;
|
_uploadCancellationTokenSource = null;
|
||||||
CurrentUploads.Clear();
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
CurrentUploads.Clear();
|
||||||
|
_currentUploadsByHash.Clear();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,22 +108,44 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
return [.. filesToUpload.Where(f => f.IsForbidden).Select(f => f.Hash)];
|
return [.. filesToUpload.Where(f => f.IsForbidden).Select(f => f.Hash)];
|
||||||
}
|
}
|
||||||
|
|
||||||
Task uploadTask = Task.CompletedTask;
|
var cancellationToken = ct ?? CancellationToken.None;
|
||||||
|
var parallelUploads = Math.Clamp(_lightlessConfigService.Current.ParallelUploads, 1, 8);
|
||||||
|
using SemaphoreSlim uploadSlots = new(parallelUploads, parallelUploads);
|
||||||
|
List<Task> uploadTasks = new();
|
||||||
|
|
||||||
int i = 1;
|
int i = 1;
|
||||||
foreach (var file in filesToUpload)
|
foreach (var file in filesToUpload)
|
||||||
{
|
{
|
||||||
progress.Report($"Uploading file {i++}/{filesToUpload.Count}. Please wait until the upload is completed.");
|
progress.Report($"Uploading file {i++}/{filesToUpload.Count}. Please wait until the upload is completed.");
|
||||||
Logger.LogDebug("[{hash}] Compressing", file);
|
uploadTasks.Add(UploadSingleFileAsync(file, uploadSlots, cancellationToken));
|
||||||
var data = await _fileDbManager.GetCompressedFileData(file.Hash, ct ?? CancellationToken.None).ConfigureAwait(false);
|
|
||||||
Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, _fileDbManager.GetFileCacheByHash(data.Item1)!.ResolvedFilepath);
|
|
||||||
await uploadTask.ConfigureAwait(false);
|
|
||||||
uploadTask = UploadFile(data.Item2, file.Hash, postProgress: false, ct ?? CancellationToken.None);
|
|
||||||
(ct ?? CancellationToken.None).ThrowIfCancellationRequested();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await uploadTask.ConfigureAwait(false);
|
await Task.WhenAll(uploadTasks).ConfigureAwait(false);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
|
async Task UploadSingleFileAsync(UploadFileDto fileDto, SemaphoreSlim gate, CancellationToken token)
|
||||||
|
{
|
||||||
|
await gate.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
Logger.LogDebug("[{hash}] Compressing", fileDto.Hash);
|
||||||
|
var data = await _fileDbManager.GetCompressedFileData(fileDto.Hash, token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var cacheEntry = _fileDbManager.GetFileCacheByHash(data.Item1);
|
||||||
|
if (cacheEntry != null)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, cacheEntry.ResolvedFilepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await UploadFile(data.Item2, fileDto.Hash, postProgress: false, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
gate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CharacterData> UploadFiles(CharacterData data, List<UserData> visiblePlayers)
|
public async Task<CharacterData> UploadFiles(CharacterData data, List<UserData> visiblePlayers)
|
||||||
@@ -167,7 +214,11 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
_uploadCancellationTokenSource?.Cancel();
|
_uploadCancellationTokenSource?.Cancel();
|
||||||
_uploadCancellationTokenSource?.Dispose();
|
_uploadCancellationTokenSource?.Dispose();
|
||||||
_uploadCancellationTokenSource = null;
|
_uploadCancellationTokenSource = null;
|
||||||
CurrentUploads.Clear();
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
CurrentUploads.Clear();
|
||||||
|
_currentUploadsByHash.Clear();
|
||||||
|
}
|
||||||
_verifiedUploadedHashes.Clear();
|
_verifiedUploadedHashes.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +262,17 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CurrentUploads.Single(f => string.Equals(f.Hash, fileHash, StringComparison.Ordinal)).Transferred = prog.Uploaded;
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
if (_currentUploadsByHash.TryGetValue(fileHash, out var transfer))
|
||||||
|
{
|
||||||
|
transfer.Transferred = prog.Uploaded;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{hash}] Could not find upload transfer during progress update", fileHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -240,10 +301,16 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CurrentUploads.Add(new UploadFileTransfer(file)
|
var uploadTransfer = new UploadFileTransfer(file)
|
||||||
{
|
{
|
||||||
Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length,
|
Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
CurrentUploads.Add(uploadTransfer);
|
||||||
|
_currentUploadsByHash[file.Hash] = uploadTransfer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -264,33 +331,75 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
_verifiedUploadedHashes[file.Hash] = DateTime.UtcNow;
|
_verifiedUploadedHashes[file.Hash] = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalSize = CurrentUploads.Sum(c => c.Total);
|
long totalSize;
|
||||||
|
List<FileTransfer> pendingUploads;
|
||||||
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
totalSize = CurrentUploads.Sum(c => c.Total);
|
||||||
|
pendingUploads = CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var parallelUploads = Math.Clamp(_lightlessConfigService.Current.ParallelUploads, 1, 8);
|
||||||
|
using SemaphoreSlim uploadSlots = new(parallelUploads, parallelUploads);
|
||||||
Logger.LogDebug("Compressing and uploading files");
|
Logger.LogDebug("Compressing and uploading files");
|
||||||
Task uploadTask = Task.CompletedTask;
|
List<Task> uploadTasks = new();
|
||||||
foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList())
|
|
||||||
|
foreach (var transfer in pendingUploads)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("[{hash}] Compressing", file);
|
uploadTasks.Add(UploadPendingFileAsync(transfer, uploadSlots, uploadToken));
|
||||||
var data = await _fileDbManager.GetCompressedFileData(file.Hash, uploadToken).ConfigureAwait(false);
|
|
||||||
CurrentUploads.Single(e => string.Equals(e.Hash, data.Item1, StringComparison.Ordinal)).Total = data.Item2.Length;
|
|
||||||
Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, _fileDbManager.GetFileCacheByHash(data.Item1)!.ResolvedFilepath);
|
|
||||||
await uploadTask.ConfigureAwait(false);
|
|
||||||
uploadTask = UploadFile(data.Item2, file.Hash, true, uploadToken);
|
|
||||||
uploadToken.ThrowIfCancellationRequested();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CurrentUploads.Any())
|
await Task.WhenAll(uploadTasks).ConfigureAwait(false);
|
||||||
{
|
|
||||||
await uploadTask.ConfigureAwait(false);
|
|
||||||
|
|
||||||
var compressedSize = CurrentUploads.Sum(c => c.Total);
|
long compressedSize;
|
||||||
Logger.LogDebug("Upload complete, compressed {size} to {compressed}", UiSharedService.ByteToString(totalSize), UiSharedService.ByteToString(compressedSize));
|
HashSet<string> uploadedHashes;
|
||||||
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
compressedSize = CurrentUploads.Sum(c => c.Total);
|
||||||
|
uploadedHashes = CurrentUploads.Select(u => u.Hash).ToHashSet(StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Exists(u => string.Equals(u.Hash, c, StringComparison.Ordinal))))
|
Logger.LogDebug("Upload complete, compressed {size} to {compressed}", UiSharedService.ByteToString(totalSize), UiSharedService.ByteToString(compressedSize));
|
||||||
|
|
||||||
|
foreach (var file in unverifiedUploadHashes.Where(c => !uploadedHashes.Contains(c)))
|
||||||
{
|
{
|
||||||
_verifiedUploadedHashes[file] = DateTime.UtcNow;
|
_verifiedUploadedHashes[file] = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentUploads.Clear();
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
CurrentUploads.Clear();
|
||||||
|
_currentUploadsByHash.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task UploadPendingFileAsync(FileTransfer transfer, SemaphoreSlim gate, CancellationToken token)
|
||||||
|
{
|
||||||
|
await gate.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
Logger.LogDebug("[{hash}] Compressing", transfer.Hash);
|
||||||
|
var data = await _fileDbManager.GetCompressedFileData(transfer.Hash, token).ConfigureAwait(false);
|
||||||
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
if (_currentUploadsByHash.TryGetValue(data.Item1, out var trackedUpload))
|
||||||
|
{
|
||||||
|
trackedUpload.Total = data.Item2.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheEntry = _fileDbManager.GetFileCacheByHash(data.Item1);
|
||||||
|
if (cacheEntry != null)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, cacheEntry.ResolvedFilepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await UploadFile(data.Item2, transfer.Hash, true, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
gate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Submodule PenumbraAPI updated: 648b6fc2ce...dd14131793
Reference in New Issue
Block a user