Compare commits

..

85 Commits

Author SHA1 Message Date
defnotken
1364ac32bd Merge branch '1.12.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 1.12.0 2025-10-04 22:04:30 -05:00
defnotken
e4931ded9f 1.12.0 not 3 2025-10-04 22:04:19 -05:00
a3b5a5cb2e Merge pull request 'ui-redesign' (#41) from ui-redesign into 1.12.0
Reviewed-on: #41
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-10-04 22:01:37 +02:00
choco
0e274f04f1 apply nameplate colors to cross-world players without FC tags 2025-10-04 21:55:42 +02:00
defnotken
dfb2e948c5 Update LightlessAPI pointer 2025-10-04 14:29:56 -05:00
defnotken
f037e7587d update penumbra pointer 2025-10-04 14:19:18 -05:00
defnotken
5bf443cf0e Merge branch '1.12.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 1.12.0 2025-10-04 14:18:07 -05:00
defnotken
78455f7523 Whitelist Kdb 2025-10-04 14:16:47 -05:00
8dd13479fc few changes:
- sending a successful pair request through context menu while a pending one exists in client clears it
- adjustments to higlight coloring, preventing any text colors to blend with the highlight
- some text adjustments
- editing uid color in profile editor also previews the highlight
2025-10-05 03:28:02 +09:00
choco
87e6c0b3f6 prevent null reference when checking FC tag color override for players without a FC 2025-10-04 14:37:44 +02:00
25d39c0ad1 Merge pull request 'ui-redesign' (#40) from ui-redesign into 1.12.0
Reviewed-on: #40
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-10-03 23:41:39 +02:00
458aa5f933 bunch of changes
- incoming pair requests
- auto fill notes when paired
- vanity colored uid at the top
- notifications now resolve player names
- hide lightfinder icon when not connected
- fixed download snapshot crashing the ui, supposedly
2025-10-04 01:57:50 +09:00
choco
3d2650cc5f added FC tag color override option for player nameplates 2025-10-03 16:11:09 +02:00
choco
931009607b color key name change 2025-10-03 10:33:36 +02:00
choco
36bf17aee2 rename accent purple labels to primary purple 2025-10-03 10:03:09 +02:00
choco
012547056a better column alignment 2025-10-03 09:58:58 +02:00
choco
9242f23787 color reset button always shown, but disabled if no custom value 2025-10-03 09:55:54 +02:00
choco
e8a3d87ff0 renamed color variables 2025-10-03 09:53:46 +02:00
choco
4862921b03 table layout for color settings 2025-10-03 09:50:37 +02:00
defnotken
f2b7b0c4e3 Send Pair Request will only show on Target Player rather than menus. 2025-10-02 17:29:15 -05:00
defnotken
05872c285c more fixes
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 35s
2025-10-02 15:39:58 -05:00
defnotken
da59105bab Merge branch '1.12.0' into dev 2025-10-02 15:39:19 -05:00
defnotken
54ea1e9d4c cleanup workflows 2025-10-02 15:38:04 -05:00
defnotken
f6e1832d62 Bugfixes for lightfinder
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 52s
2025-10-01 19:26:03 -05:00
afc42d97d1 highlight dark uid's 2025-10-01 23:25:34 +09:00
CakeAndBanana
714aeef468 Moved ContextMenu from UI to Service. 2025-10-01 04:16:19 +02:00
CakeAndBanana
3a627f2d3e Added refetch of syncshells after comfirmation. 2025-10-01 04:11:01 +02:00
CakeAndBanana
bf3770025b Changed logger calls to debug/trace. 2025-10-01 03:20:31 +02:00
CakeAndBanana
49d138049e Added check if in PVP/Gpose and check if you are targetting your own. 2025-10-01 03:15:11 +02:00
CakeAndBanana
e91d163763 Disabled the pair request on already paired users 2025-10-01 02:47:11 +02:00
dea4ef4832 rich text, updated lightfinder description and bug fixes 2025-10-01 08:59:08 +09:00
76520878bf bump submodule 2025-10-01 03:29:56 +09:00
defnotken
068c8cb180 Merge + Version Bump
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 35s
2025-09-30 09:52:12 -05:00
defnotken
c34e379a6e Merge branch '1.12.0' into dev 2025-09-30 09:48:16 -05:00
de007e2b04 File Cache Checks
Reviewed-on: #35
2025-09-30 16:46:40 +02:00
e52d4c6165 Merge branch '1.12.0' into 1.11.13 2025-09-30 16:46:21 +02:00
34250a18b4 Merge pull request 'compactui settings button peepoo' (#38) from ui-redesign into 1.12.0
Reviewed-on: #38
2025-09-30 16:15:53 +02:00
390a0c2d61 Merge branch '1.12.0' into ui-redesign 2025-09-30 16:15:30 +02:00
choco
571decd33b removed unused cursor position 2025-09-30 16:14:53 +02:00
choco
36c1611486 compactui settings button peepoo 2025-09-30 16:09:57 +02:00
CakeAndBanana
597a0beb91 Added check if context user is in range or in object table. 2025-09-30 05:50:39 +02:00
CakeAndBanana
1fa2f063e8 Merge branch '1.12.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 1.12.0 2025-09-30 05:45:03 +02:00
CakeAndBanana
1fae47b171 Removal of Logging of worlds for testing on what region I should needed 2025-09-30 05:44:49 +02:00
e85b900754 some ui updates 2025-09-30 11:17:55 +09:00
CakeAndBanana
9696ac60f1 Added region bound. 2025-09-30 00:00:11 +02:00
CakeAndBanana
0b7b543dd7 Add Group check, reworked world check for instances. 2025-09-29 23:56:11 +02:00
CakeAndBanana
8a9d4b8daa Added more byte options 2025-09-29 23:19:02 +02:00
CakeAndBanana
7d6d500e6a Changes from Bites to Bytes. 2025-09-29 22:38:38 +02:00
CakeAndBanana
dc1fdd4a3e Removed the lightfinder needs to be turned on. 2025-09-29 19:52:19 +02:00
CakeAndBanana
f74cd01a32 Pair request button only shows when lightfinder is on 2025-09-29 19:44:36 +02:00
CakeAndBanana
8f7f07311f Merge branch '1.12.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 1.12.0 2025-09-29 18:37:47 +02:00
CakeAndBanana
7b415b4e47 Changed it to so debug shows while in debug mode. 2025-09-29 18:37:02 +02:00
f90b9c3f8e Merge pull request 'ui-redesign' (#37) from ui-redesign into 1.12.0
Reviewed-on: #37
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-09-28 20:41:47 +02:00
choco
0cc7181e98 added world name to syncshell broadcaster info 2025-09-28 19:41:19 +02:00
defnotken
914553d5ab plogon missing var
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 49s
2025-09-28 10:11:13 -05:00
choco
08e3c8678f added broadcaster name display in Syncshell finder table 2025-09-28 15:49:15 +02:00
choco
6af2013a03 Merge branch '1.12.0' into ui-redesign 2025-09-28 15:17:48 +02:00
choco
3831dd24f1 settings cleanup readded title settings 2025-09-28 15:16:45 +02:00
defnotken
73f130a95a remove redundant checkout
Some checks failed
Tag and Release Lightless / tag-and-release (push) Failing after 50s
2025-09-27 22:05:33 -05:00
defnotken
7c4269b011 Testing dev release workflow
Some checks failed
Tag and Release Lightless / tag-and-release (push) Has been cancelled
2025-09-27 21:58:24 -05:00
CakeAndBanana
c6f8d6843e Disabled button and added tooltip for already joined/owned syncshells in finder 2025-09-26 18:39:38 +02:00
fd9bd3975b colored uid client support 2025-09-27 01:38:05 +09:00
CakeAndBanana
3e15dd643e Added more comments 2025-09-26 05:17:16 +02:00
CakeAndBanana
f6784fdf28 Refactored drawfolders function of CompactUI 2025-09-26 05:14:39 +02:00
CakeAndBanana
c182d10a0a Moved the warning triangle downwards 2025-09-26 04:54:12 +02:00
8e7cdce310 Merge pull request 'Fixed some issues with folders not showing, adding empty folders to the list.' (#34) from fix-showing-empty-folders into 1.12.0
Reviewed-on: #34
2025-09-26 04:40:52 +02:00
CakeAndBanana
e3a3f16d14 Hiding of syncshells that already been joined in shell finder 2025-09-26 04:40:15 +02:00
CakeAndBanana
31b56ba58a Merge branch '1.12.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 1.12.0 2025-09-25 22:01:22 +02:00
9bc2ad24cd clearing syncshell from lightfinder users 2025-09-26 04:51:05 +09:00
864a2e6677 slight clean up and minor spelling mistake 2025-09-26 03:29:28 +09:00
CakeAndBanana
ab3ca78c7f Fixed typo in setup 2025-09-25 18:53:20 +02:00
CakeAndBanana
6bb379ebad Returning finder on joining syncshell, added functions for syncshell profiles that would be needed later 2025-09-25 17:56:30 +02:00
defnotken
b0b149d8bc submodule 2025-09-25 10:25:55 -05:00
defnotken
777e6b9d27 remove created at for now 2025-09-25 10:25:12 -05:00
CakeAndBanana
37c11e9d73 Added tasks and added await on get groups 2025-09-25 03:34:59 +02:00
choco
4060ba96f1 moved settings button from compact UI to top tab menu 2025-09-25 00:15:42 +02:00
e8f8512cdd updated layout and adjusted scanning 2025-09-25 06:06:19 +09:00
7569b15993 seperate scanning service not relying on nameplate updates & other improvements/fixes 2025-09-24 22:28:32 +09:00
d91f1a3356 and genius again 2025-09-24 07:19:16 +09:00
0c38b9397a i'm a genius 2 2025-09-24 07:18:08 +09:00
9d850f8fa6 quick fix 2025-09-24 06:57:01 +09:00
9eb2309018 lightfinder! 2025-09-24 05:53:22 +09:00
defnotken
bd2c3a4a80 add cache logging to 1.11.13 2025-09-18 22:03:40 -05:00
defnotken
fe898cdc2b Start 1.11.13 2025-09-17 19:58:25 -05:00
CakeAndBanana
38a360bfee Fixed some issues with folders not showing, adding empty folders to the list. 2025-09-16 21:06:15 +02:00
29 changed files with 227 additions and 1553 deletions

View File

@@ -41,9 +41,9 @@ jobs:
- name: Get version - name: Get version
id: package_version id: package_version
run: | uses: KageKirin/get-csproj-version@v0
version=$(grep -oPm1 "(?<=<Version>)[^<]+" LightlessSync/LightlessSync.csproj) with:
echo "version=$version" >> $GITHUB_OUTPUT file: LightlessSync/LightlessSync.csproj
- name: Display version - name: Display version
run: | run: |
@@ -121,11 +121,8 @@ 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" echo "release_id=$release_id" >> "$GITHUB_OUTPUT"
echo "release_id=$release_id" >> $GITHUB_OUTPUT || echo "::set-output name=release_id::$release_id"
echo "RELEASE_ID=$release_id" >> $GITHUB_ENV
- name: Create Release (dev) - name: Create Release (dev)
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
@@ -153,28 +150,15 @@ 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" echo "release_id=$release_id" >> "$GITHUB_OUTPUT"
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
run: |
if [ ! -f output/LightlessClient.zip ]; then
echo "output/LightlessClient.zip does not exist!"
exit 1
fi
- name: Upload Assets to release - name: Upload Assets to release
env:
RELEASE_ID: ${{ env.RELEASE_ID }}
run: | run: |
echo "Uploading to release ID: $RELEASE_ID"
curl --fail-with-body -s -X POST \ curl --fail-with-body -s -X POST \
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
-F "attachment=@output/LightlessClient.zip" \ -F "attachment=@output/LightlessClient.zip" \
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/$RELEASE_ID/assets" "https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/${{ steps.create_release.outputs.release_id }}/assets"
- name: Clone plugin hosting repo - name: Clone plugin hosting repo
run: | run: |

View File

@@ -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,8 +19,7 @@ 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, ConcurrentDictionary<string, FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, List<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;
@@ -38,57 +37,6 @@ 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);
@@ -113,26 +61,20 @@ public sealed class FileCacheManager : IHostedService
return CreateFileCacheEntity(fi, prefixedPath); return CreateFileCacheEntity(fi, prefixedPath);
} }
public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null)).ToList(); public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v).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.Values.Where(c => !ignoreCacheEntries || !c.IsCacheEntry).ToList()) foreach (var fileCache in fileCacheEntities.Where(c => !ignoreCacheEntries || !c.IsCacheEntry).ToList())
{ {
if (!validate) if (!validate) output.Add(fileCache);
{
output.Add(fileCache);
}
else else
{ {
var validated = GetValidatedFileCache(fileCache); var validated = GetValidatedFileCache(fileCache);
if (validated != null) if (validated != null) output.Add(validated);
{
output.Add(validated);
}
} }
} }
} }
@@ -144,7 +86,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.Values.SelectMany(v => v.Values.Where(e => e != null)).Where(v => v.IsCacheEntry).ToList(); var cacheEntries = _fileCaches.SelectMany(v => v.Value).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)
@@ -209,40 +151,29 @@ public sealed class FileCacheManager : IHostedService
public FileCacheEntity? GetFileCacheByHash(string hash) public FileCacheEntity? GetFileCacheByHash(string hash)
{ {
if (_fileCaches.TryGetValue(hash, out var entries)) if (_fileCaches.TryGetValue(hash, out var hashes))
{ {
var item = entries.Values var item = hashes.OrderBy(p => p.PrefixedFilePath.Contains(PenumbraPrefix, StringComparison.Ordinal) ? 0 : 1).FirstOrDefault();
.OrderBy(p => p.PrefixedFilePath.Contains(PenumbraPrefix, StringComparison.Ordinal) ? 0 : 1) if (item != null) return GetValidatedFileCache(item);
.FirstOrDefault();
if (item != null)
{
return GetValidatedFileCache(item);
}
} }
return null; return null;
} }
private FileCacheEntity? GetFileCacheByPath(string path) private FileCacheEntity? GetFileCacheByPath(string path)
{ {
var normalizedPrefixedPath = NormalizeToPrefixedPath(path); var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant()
if (string.IsNullOrEmpty(normalizedPrefixedPath)) .Replace(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase);
var entry = _fileCaches.SelectMany(v => v.Value).FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase));
if (entry == null)
{ {
return null; _logger.LogDebug("Found no entries for {path}", cleanedPath);
return CreateFileEntry(path);
} }
if (_fileCachesByPrefixedPath.TryGetValue(normalizedPrefixedPath, out var entry)) var validatedCacheEntry = GetValidatedFileCache(entry);
{
return GetValidatedFileCache(entry);
}
_logger.LogDebug("Found no entries for {path}", normalizedPrefixedPath); return validatedCacheEntry;
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)
@@ -251,55 +182,73 @@ public sealed class FileCacheManager : IHostedService
try try
{ {
var result = new Dictionary<string, FileCacheEntity?>(StringComparer.OrdinalIgnoreCase); var allEntities = _fileCaches.SelectMany(f => f.Value).ToArray();
var seenNormalized = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var originalPath in paths) var cacheDict = new ConcurrentDictionary<string, FileCacheEntity>(
StringComparer.OrdinalIgnoreCase);
Parallel.ForEach(allEntities, entity =>
{ {
if (string.IsNullOrEmpty(originalPath)) if (entity != null && entity.PrefixedFilePath != null)
{ {
result[originalPath] = null; cacheDict[entity.PrefixedFilePath] = entity;
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
{ {
created = CreateFileEntry(originalPath) ?? CreateCacheEntry(originalPath); _logger.LogWarning("Null FileCacheEntity or PrefixedFilePath encountered in cache population: {entity}", entity);
} }
});
result[originalPath] = created; var cleanedPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
} var seenCleaned = new ConcurrentDictionary<string, byte>(StringComparer.OrdinalIgnoreCase);
return result; Parallel.ForEach(paths, p =>
{
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
{ {
@@ -309,24 +258,17 @@ 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.TryRemove(normalizedPath, out var removedEntity)) if (caches?.Count == 0)
{ {
_logger.LogTrace("Removed from DB: {hash} => {path}", hash, removedEntity.PrefixedFilePath); _fileCaches.Remove(hash, out var entity);
}
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)
@@ -367,7 +309,7 @@ public sealed class FileCacheManager : IHostedService
lock (_fileWriteLock) lock (_fileWriteLock)
{ {
StringBuilder sb = new(); StringBuilder sb = new();
foreach (var entry in _fileCaches.Values.SelectMany(k => k.Values).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase)) foreach (var entry in _fileCaches.SelectMany(k => k.Value).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
{ {
sb.AppendLine(entry.CsvEntry); sb.AppendLine(entry.CsvEntry);
} }
@@ -412,11 +354,16 @@ public sealed class FileCacheManager : IHostedService
private void AddHashedFile(FileCacheEntity fileCache) private void AddHashedFile(FileCacheEntity fileCache)
{ {
var normalizedPath = NormalizePrefixedPathKey(fileCache.PrefixedFilePath); if (!_fileCaches.TryGetValue(fileCache.Hash, out var entries) || entries is null)
var entries = _fileCaches.GetOrAdd(fileCache.Hash, _ => new ConcurrentDictionary<string, FileCacheEntity>(StringComparer.OrdinalIgnoreCase)); {
_fileCaches[fileCache.Hash] = entries = [];
}
entries[normalizedPath] = fileCache; if (!entries.Exists(u => string.Equals(u.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.OrdinalIgnoreCase)))
_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)

View File

@@ -1,5 +1,3 @@
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;
@@ -35,9 +33,6 @@ 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;
@@ -75,16 +70,7 @@ 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;
} }

View File

@@ -1,9 +0,0 @@
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;
}

View File

@@ -1,9 +0,0 @@
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);
}

View File

@@ -1,14 +0,0 @@
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;
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<Authors></Authors> <Authors></Authors>
<Company></Company> <Company></Company>
<Version>1.12.1.1</Version> <Version>1.12.0</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>

View File

@@ -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,7 +21,6 @@ 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;
@@ -29,7 +28,6 @@ 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;
@@ -42,7 +40,6 @@ public class PairHandlerFactory
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_lightlessMediator = lightlessMediator; _lightlessMediator = lightlessMediator;
_playerPerformanceService = playerPerformanceService; _playerPerformanceService = playerPerformanceService;
_pairProcessingLimiter = pairProcessingLimiter;
_serverConfigManager = serverConfigManager; _serverConfigManager = serverConfigManager;
} }
@@ -50,6 +47,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, _pairProcessingLimiter, _serverConfigManager); _fileCacheManager, _lightlessMediator, _playerPerformanceService, _serverConfigManager);
} }
} }

View File

@@ -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,7 +28,6 @@ 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();
@@ -51,7 +50,6 @@ 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;
@@ -63,7 +61,6 @@ 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();
@@ -423,7 +420,6 @@ 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)
@@ -741,11 +737,6 @@ 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);
@@ -772,4 +763,4 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
_dataReceivedInDowntime = null; _dataReceivedInDowntime = null;
} }
} }
} }

View File

@@ -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,14 +7,10 @@ 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;
@@ -28,19 +24,14 @@ 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, PairProcessingLimiter pairProcessingLimiter) : base(logger, mediator) IContextMenu dalamudContextMenu) : 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();
@@ -121,7 +112,6 @@ 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();
@@ -171,7 +161,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)));
} }
QueuePairCreation(pair, dto); pair.CreateCachedPlayer(dto);
RecreateLazy(); RecreateLazy();
} }
@@ -342,7 +332,6 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
{ {
base.Dispose(disposing); base.Dispose(disposing);
ResetPairCreationQueue();
_dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu; _dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu;
DisposePairs(); DisposePairs();
@@ -401,84 +390,6 @@ 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))

View File

@@ -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,8 +101,6 @@ 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)
@@ -129,15 +127,6 @@ 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");
}
}); });
} }
} }

View File

@@ -106,7 +106,6 @@ 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>();
@@ -145,7 +144,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<PairProcessingLimiter>())); s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu));
collection.AddSingleton<RedrawManager>(); collection.AddSingleton<RedrawManager>();
collection.AddSingleton<BroadcastService>(); collection.AddSingleton<BroadcastService>();
collection.AddSingleton(addonLifecycle); collection.AddSingleton(addonLifecycle);
@@ -208,6 +207,7 @@ 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,8 +252,6 @@ 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>());
@@ -279,4 +277,4 @@ public sealed class Plugin : IDalamudPlugin
_host.StopAsync().GetAwaiter().GetResult(); _host.StopAsync().GetAwaiter().GetResult();
_host.Dispose(); _host.Dispose();
} }
} }

View File

@@ -42,8 +42,7 @@ 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" + Environment.NewLine + "\t /light settings - Opens the Lightless Settings window"
"\t /light lightfinder - Opens the Lightfinder window"
}); });
} }
@@ -123,9 +122,5 @@ 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)));
}
} }
} }

View File

@@ -24,7 +24,6 @@ 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,
@@ -46,7 +45,6 @@ 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;
@@ -110,9 +108,6 @@ 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
{ {
@@ -158,7 +153,6 @@ 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)];

View File

@@ -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,7 +77,6 @@ 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;

View File

@@ -1,22 +1,15 @@
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;
@@ -25,10 +18,7 @@ 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;
@@ -36,31 +26,18 @@ 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 = []; private volatile HashSet<string> _activeBroadcastingCids = new();
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairManager pairManager) public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessMediator mediator)
{ {
_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()
@@ -119,10 +96,6 @@ 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();
} }
@@ -183,11 +156,6 @@ 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()
@@ -209,7 +177,6 @@ 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;
@@ -223,202 +190,30 @@ 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;
} }
if (!_configService.Current.LightfinderLabelShowOwn && (objectInfo->GameObject->GetGameObjectId() == _clientState.LocalPlayer.GameObjectId)) pNode->AtkResNode.ToggleVisibility(true);
{
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 scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f); var labelY = nameContainer->Height - nameplateObject.TextH - (int)(24 * nameText->AtkResNode.ScaleY);
var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f;
var effectiveScale = baseScale * scaleMultiplier;
var labelContent = config.LightfinderLabelUseIcon
? NormalizeIconGlyph(config.LightfinderLabelIconGlyph)
: DefaultLabelText;
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed; pNode->AtkResNode.SetPositionShort(58, (short)labelY);
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;
@@ -432,98 +227,18 @@ 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;
if(!config.LightfinderLabelUseIcon) pNode->AlignmentType = AlignmentType.Center;
{ pNode->FontType = FontType.MiedingerMed;
pNode->AlignmentType = AlignmentType.Bottom; pNode->LineSpacing = 24;
}
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
? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize pNode->TextFlags = TextFlags.Edge | TextFlags.Glare;
: 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];
@@ -552,9 +267,6 @@ 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()
{ {
@@ -586,12 +298,4 @@ 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);
}
} }

View File

@@ -35,6 +35,7 @@ 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)

View File

@@ -1,220 +0,0 @@
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);
}

View File

@@ -49,6 +49,8 @@ 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()
@@ -119,7 +121,7 @@ namespace LightlessSync.UI
public override void OnOpen() public override void OnOpen()
{ {
_userUid = _apiController.UID; _userUid = _apiController.UID;
_ = RefreshSyncshells(); _ = RefreshSyncshellsInternal();
} }
protected override void DrawInternal() protected override void DrawInternal()
@@ -266,7 +268,6 @@ namespace LightlessSync.UI
if (_allSyncshells == null) if (_allSyncshells == null)
{ {
ImGui.Text("Loading Syncshells..."); ImGui.Text("Loading Syncshells...");
_ = RefreshSyncshells();
return; return;
} }
@@ -318,7 +319,6 @@ namespace LightlessSync.UI
{ {
_configService.Current.SelectedFinderSyncshell = gid; _configService.Current.SelectedFinderSyncshell = gid;
_configService.Save(); _configService.Save();
_ = RefreshSyncshells();
} }
if (!available && ImGui.IsItemHovered()) if (!available && ImGui.IsItemHovered())

View File

@@ -1,5 +1,4 @@
using System; using Dalamud.Bindings.ImGui;
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;
@@ -377,7 +376,7 @@ public class CompactUi : WindowMediatorSubscriberBase
private void DrawTransfers() private void DrawTransfers()
{ {
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot(); var currentUploads = _fileTransferManager.CurrentUploads.ToList();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(FontAwesomeIcon.Upload); _uiSharedService.IconText(FontAwesomeIcon.Upload);
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
@@ -387,12 +386,10 @@ 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} (slots {activeUploads}/{uploadSlotLimit})"); ImGui.TextUnformatted($"{doneUploads}/{totalUploads}");
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);
@@ -491,7 +488,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)
{ {
@@ -622,7 +619,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)
{ {
@@ -674,7 +671,7 @@ public class CompactUi : WindowMediatorSubscriberBase
UiSharedService.AttachToolTip("Click to copy"); UiSharedService.AttachToolTip("Click to copy");
if (uidFooterClicked) if (uidFooterClicked)
{ {
ImGui.SetClipboardText(_apiController.UID); _lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
} }
} }
} }
@@ -828,7 +825,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 =>
{ {

View File

@@ -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 Developer" ? "Official Lightless Admin"
: "Official Lightless Moderator"); : "Official Lightless Moderator");
} }
ImGui.EndTooltip(); ImGui.EndTooltip();

View File

@@ -1,5 +1,4 @@
using System; using Dalamud.Bindings.ImGui;
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;
@@ -20,16 +19,14 @@ 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,
PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService) 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;
@@ -76,25 +73,11 @@ 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.IsUploading) if (_fileTransferManager.CurrentUploads.Any())
{ {
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot(); var currentUploads = _fileTransferManager.CurrentUploads.ToList();
var totalUploads = currentUploads.Count; var totalUploads = currentUploads.Count;
var doneUploads = currentUploads.Count(c => c.IsTransferred); var doneUploads = currentUploads.Count(c => c.IsTransferred);
@@ -231,7 +214,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.IsUploading && !_uploadingPlayers.Any()) return false; if (!_currentDownloads.Any() && !_fileTransferManager.CurrentUploads.Any() && !_uploadingPlayers.Any()) return false;
if (!IsOpen) return false; if (!IsOpen) return false;
return true; return true;
} }

View File

@@ -1,5 +1,4 @@
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;
@@ -11,7 +10,6 @@ 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;
@@ -19,18 +17,15 @@ 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;
@@ -55,12 +50,10 @@ 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;
@@ -70,23 +63,6 @@ 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;
@@ -96,7 +72,6 @@ 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,
@@ -104,14 +79,12 @@ 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, NameplateService nameplateService) : base(logger, mediator, "Lightless Sync Settings", performanceCollector)
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;
@@ -124,7 +97,6 @@ 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);
@@ -246,9 +218,6 @@ 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;
@@ -285,60 +254,7 @@ 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))
{ {
@@ -493,33 +409,25 @@ public class SettingsUi : WindowMediatorSubscriberBase
{ {
if (ApiController.ServerState is ServerState.Connected && ImGui.BeginTabItem("Transfers")) if (ApiController.ServerState is ServerState.Connected && ImGui.BeginTabItem("Transfers"))
{ {
var uploadsSnapshot = _fileTransferManager.GetCurrentUploadsSnapshot(); ImGui.TextUnformatted("Uploads");
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 uploadsSnapshot) foreach (var transfer in _fileTransferManager.CurrentUploads.ToArray())
{ {
var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total)); var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total));
using var col = ImRaii.PushColor(ImGuiCol.Text, color); var col = ImRaii.PushColor(ImGuiCol.Text, color);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (transfer is UploadFileTransfer uploadTransfer) ImGui.TextUnformatted(transfer.Hash);
{
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();
@@ -1032,7 +940,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_configService.Current.EnableRightClickMenus = enableRightClickMenu; _configService.Current.EnableRightClickMenus = enableRightClickMenu;
_configService.Save(); _configService.Save();
} }
_uiShared.DrawHelpText("This will add all Lightless related right click menu entries in the game UI."); _uiShared.DrawHelpText("This will add Lightless related right click menu entries in the game UI on paired players.");
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))
{ {
@@ -1064,261 +972,6 @@ 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");
@@ -2563,39 +2216,6 @@ 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)

View File

@@ -29,7 +29,6 @@ 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;
@@ -121,17 +120,6 @@ 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();
@@ -139,8 +127,19 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
ImGui.TextUnformatted(displayName); ImGui.TextUnformatted(displayName);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(Address); var broadcasterName = "Unknown";
var broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{Name} ({worldName})" : Name; var broadcast = _broadcastScannerService.GetActiveSyncshellBroadcasts()
.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();
@@ -150,10 +149,7 @@ 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));
var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal)); if (!_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))
{ {
@@ -228,11 +224,9 @@ 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();
} }
} }
} }
@@ -267,8 +261,6 @@ 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)
{ {
@@ -292,6 +284,11 @@ 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();

View File

@@ -1,15 +0,0 @@
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,
}
}

View File

@@ -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,7 +8,6 @@ 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;
@@ -20,7 +19,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 ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams; private readonly List<ThrottledStream> _activeDownloadStreams;
public FileDownloadManager(ILogger<FileDownloadManager> logger, LightlessMediator mediator, public FileDownloadManager(ILogger<FileDownloadManager> logger, LightlessMediator mediator,
FileTransferOrchestrator orchestrator, FileTransferOrchestrator orchestrator,
@@ -30,14 +29,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
_orchestrator = orchestrator; _orchestrator = orchestrator;
_fileDbManager = fileCacheManager; _fileDbManager = fileCacheManager;
_fileCompactor = fileCompactor; _fileCompactor = fileCompactor;
_activeDownloadStreams = new(); _activeDownloadStreams = [];
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) => Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
{ {
if (_activeDownloadStreams.IsEmpty) return; if (!_activeDownloadStreams.Any()) 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.Keys) foreach (var stream in _activeDownloadStreams)
{ {
stream.BandwidthLimit = newLimit; stream.BandwidthLimit = newLimit;
} }
@@ -48,7 +47,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)
{ {
@@ -85,7 +84,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.Keys.ToList()) foreach (var stream in _activeDownloadStreams.ToList())
{ {
try try
{ {
@@ -96,10 +95,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
// do nothing // do nothing
// //
} }
finally
{
_activeDownloadStreams.TryRemove(stream, out _);
}
} }
base.Dispose(disposing); base.Dispose(disposing);
} }
@@ -147,14 +142,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
await WaitForDownloadReady(fileTransfer, requestId, ct).ConfigureAwait(false); await WaitForDownloadReady(fileTransfer, requestId, ct).ConfigureAwait(false);
if (_downloadStatus.TryGetValue(downloadGroup, out var downloadStatus)) _downloadStatus[downloadGroup].DownloadStatus = DownloadStatus.Downloading;
{
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;
@@ -216,7 +204,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.TryAdd(stream, 0); _activeDownloadStreams.Add(stream);
while ((bytesRead = await stream.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0) while ((bytesRead = await stream.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
{ {
@@ -257,7 +245,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
{ {
if (stream != null) if (stream != null)
{ {
_activeDownloadStreams.TryRemove(stream, out _); _activeDownloadStreams.Remove(stream);
await stream.DisposeAsync().ConfigureAwait(false); await stream.DisposeAsync().ConfigureAwait(false);
} }
} }
@@ -265,28 +253,11 @@ 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)
{ {
var objectName = gameObjectHandler?.Name ?? "Unknown"; Logger.LogDebug("Download start: {id}", gameObjectHandler.Name);
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(hashes, ct).ConfigureAwait(false), .. await FilesGetSizes(fileReplacement.Select(f => f.Hash).Distinct(StringComparer.Ordinal).ToList(), 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)));
@@ -344,23 +315,15 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
FileInfo fi = new(blockFile); FileInfo fi = new(blockFile);
try try
{ {
if (!_downloadStatus.TryGetValue(fileGroup.Key, out var downloadStatus)) _downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForSlot;
{
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.DownloadStatus = DownloadStatus.WaitingForQueue; _downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForQueue;
Progress<long> progress = new((bytesDownloaded) => Progress<long> progress = new((bytesDownloaded) =>
{ {
try try
{ {
if (_downloadStatus.TryGetValue(fileGroup.Key, out FileDownloadStatus? value)) if (!_downloadStatus.TryGetValue(fileGroup.Key, out FileDownloadStatus? value)) return;
{ value.TransferredBytes += bytesDownloaded;
value.TransferredBytes += bytesDownloaded;
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -390,12 +353,6 @@ 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)
{ {

View File

@@ -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,8 +10,6 @@ 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;
@@ -21,9 +19,7 @@ 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 ConcurrentDictionary<string, DateTime> _verifiedUploadedHashes = new(StringComparer.Ordinal); private readonly Dictionary<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,
@@ -44,38 +40,17 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
} }
public List<FileTransfer> CurrentUploads { get; } = []; public List<FileTransfer> CurrentUploads { get; } = [];
public bool IsUploading public bool IsUploading => CurrentUploads.Count > 0;
{
get
{
lock (_currentUploadsLock)
{
return CurrentUploads.Count > 0;
}
}
}
public List<FileTransfer> GetCurrentUploadsSnapshot()
{
lock (_currentUploadsLock)
{
return CurrentUploads.ToList();
}
}
public bool CancelUpload() public bool CancelUpload()
{ {
if (IsUploading) if (CurrentUploads.Any())
{ {
Logger.LogDebug("Cancelling current upload"); Logger.LogDebug("Cancelling current upload");
_uploadCancellationTokenSource?.Cancel(); _uploadCancellationTokenSource?.Cancel();
_uploadCancellationTokenSource?.Dispose(); _uploadCancellationTokenSource?.Dispose();
_uploadCancellationTokenSource = null; _uploadCancellationTokenSource = null;
lock (_currentUploadsLock) CurrentUploads.Clear();
{
CurrentUploads.Clear();
_currentUploadsByHash.Clear();
}
return true; return true;
} }
@@ -108,44 +83,22 @@ 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)];
} }
var cancellationToken = ct ?? CancellationToken.None; Task uploadTask = Task.CompletedTask;
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.");
uploadTasks.Add(UploadSingleFileAsync(file, uploadSlots, cancellationToken)); Logger.LogDebug("[{hash}] Compressing", file);
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 Task.WhenAll(uploadTasks).ConfigureAwait(false); await uploadTask.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)
@@ -214,11 +167,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
_uploadCancellationTokenSource?.Cancel(); _uploadCancellationTokenSource?.Cancel();
_uploadCancellationTokenSource?.Dispose(); _uploadCancellationTokenSource?.Dispose();
_uploadCancellationTokenSource = null; _uploadCancellationTokenSource = null;
lock (_currentUploadsLock) CurrentUploads.Clear();
{
CurrentUploads.Clear();
_currentUploadsByHash.Clear();
}
_verifiedUploadedHashes.Clear(); _verifiedUploadedHashes.Clear();
} }
@@ -262,17 +211,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
{ {
try try
{ {
lock (_currentUploadsLock) CurrentUploads.Single(f => string.Equals(f.Hash, fileHash, StringComparison.Ordinal)).Transferred = prog.Uploaded;
{
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)
{ {
@@ -301,16 +240,10 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
{ {
try try
{ {
var uploadTransfer = new UploadFileTransfer(file) CurrentUploads.Add(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)
{ {
@@ -331,75 +264,33 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
_verifiedUploadedHashes[file.Hash] = DateTime.UtcNow; _verifiedUploadedHashes[file.Hash] = DateTime.UtcNow;
} }
long totalSize; var totalSize = CurrentUploads.Sum(c => c.Total);
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");
List<Task> uploadTasks = new(); Task uploadTask = Task.CompletedTask;
foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList())
foreach (var transfer in pendingUploads)
{ {
uploadTasks.Add(UploadPendingFileAsync(transfer, uploadSlots, uploadToken)); Logger.LogDebug("[{hash}] Compressing", file);
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();
} }
await Task.WhenAll(uploadTasks).ConfigureAwait(false); if (CurrentUploads.Any())
long compressedSize;
HashSet<string> uploadedHashes;
lock (_currentUploadsLock)
{ {
compressedSize = CurrentUploads.Sum(c => c.Total); await uploadTask.ConfigureAwait(false);
uploadedHashes = CurrentUploads.Select(u => u.Hash).ToHashSet(StringComparer.Ordinal);
var compressedSize = CurrentUploads.Sum(c => c.Total);
Logger.LogDebug("Upload complete, compressed {size} to {compressed}", UiSharedService.ByteToString(totalSize), UiSharedService.ByteToString(compressedSize));
} }
Logger.LogDebug("Upload complete, compressed {size} to {compressed}", UiSharedService.ByteToString(totalSize), UiSharedService.ByteToString(compressedSize)); foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Exists(u => string.Equals(u.Hash, c, StringComparison.Ordinal))))
foreach (var file in unverifiedUploadHashes.Where(c => !uploadedHashes.Contains(c)))
{ {
_verifiedUploadedHashes[file] = DateTime.UtcNow; _verifiedUploadedHashes[file] = DateTime.UtcNow;
} }
lock (_currentUploadsLock) CurrentUploads.Clear();
{
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();
}
}
} }
} }