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
50 changed files with 1016 additions and 5969 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;
@@ -16,13 +16,10 @@ public sealed class FileCacheManager : IHostedService
public const string CachePrefix = "{cache}"; public const string CachePrefix = "{cache}";
public const string CsvSplit = "|"; public const string CsvSplit = "|";
public const string PenumbraPrefix = "{penumbra}"; public const string PenumbraPrefix = "{penumbra}";
private const int FileCacheVersion = 1;
private const string FileCacheVersionHeaderPrefix = "#lightless-file-cache-version:";
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;
@@ -40,119 +37,12 @@ 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 static bool TryBuildPrefixedPath(string path, string? baseDirectory, string prefix, out string prefixedPath, out int matchedLength)
{
prefixedPath = string.Empty;
matchedLength = 0;
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(baseDirectory))
{
return false;
}
var normalizedPath = NormalizeSeparators(path).ToLowerInvariant();
var normalizedBase = NormalizeSeparators(baseDirectory).TrimEnd('\\').ToLowerInvariant();
if (!normalizedPath.StartsWith(normalizedBase, StringComparison.Ordinal))
{
return false;
}
if (normalizedPath.Length > normalizedBase.Length)
{
if (normalizedPath[normalizedBase.Length] != '\\')
{
return false;
}
prefixedPath = prefix + normalizedPath.Substring(normalizedBase.Length);
}
else
{
prefixedPath = prefix;
}
prefixedPath = prefixedPath.Replace("\\\\", "\\", StringComparison.Ordinal);
matchedLength = normalizedBase.Length;
return true;
}
private static string BuildVersionHeader() => $"{FileCacheVersionHeaderPrefix}{FileCacheVersion}";
private static bool TryParseVersionHeader(string? line, out int version)
{
version = 0;
if (string.IsNullOrWhiteSpace(line))
{
return false;
}
if (!line.StartsWith(FileCacheVersionHeaderPrefix, StringComparison.OrdinalIgnoreCase))
{
return false;
}
var versionSpan = line.AsSpan(FileCacheVersionHeaderPrefix.Length);
return int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out version);
}
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);
}
string? chosenPrefixed = null;
var chosenLength = -1;
if (TryBuildPrefixedPath(normalized, _ipcManager.Penumbra.ModDirectory, PenumbraPrefix, out var penumbraPrefixed, out var penumbraMatch))
{
chosenPrefixed = penumbraPrefixed;
chosenLength = penumbraMatch;
}
if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch))
{
if (cacheMatch > chosenLength)
{
chosenPrefixed = cachePrefixed;
chosenLength = cacheMatch;
}
}
return NormalizePrefixedPathKey(chosenPrefixed ?? normalized);
}
public FileCacheEntity? CreateCacheEntry(string path) public FileCacheEntity? CreateCacheEntry(string path)
{ {
FileInfo fi = new(path); FileInfo fi = new(path);
if (!fi.Exists) return null; if (!fi.Exists) return null;
_logger.LogTrace("Creating cache entry for {path}", path); _logger.LogTrace("Creating cache entry for {path}", path);
var cacheFolder = _configService.Current.CacheFolder; return CreateFileEntity(_configService.Current.CacheFolder.ToLowerInvariant(), CachePrefix, fi);
if (string.IsNullOrEmpty(cacheFolder)) return null;
return CreateFileEntity(cacheFolder, CachePrefix, fi);
} }
public FileCacheEntity? CreateFileEntry(string path) public FileCacheEntity? CreateFileEntry(string path)
@@ -160,41 +50,31 @@ public sealed class FileCacheManager : IHostedService
FileInfo fi = new(path); FileInfo fi = new(path);
if (!fi.Exists) return null; if (!fi.Exists) return null;
_logger.LogTrace("Creating file entry for {path}", path); _logger.LogTrace("Creating file entry for {path}", path);
var modDirectory = _ipcManager.Penumbra.ModDirectory; return CreateFileEntity(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix, fi);
if (string.IsNullOrEmpty(modDirectory)) return null;
return CreateFileEntity(modDirectory, PenumbraPrefix, fi);
} }
private FileCacheEntity? CreateFileEntity(string directory, string prefix, FileInfo fi) private FileCacheEntity? CreateFileEntity(string directory, string prefix, FileInfo fi)
{ {
if (!TryBuildPrefixedPath(fi.FullName, directory, prefix, out var prefixedPath, out _)) var fullName = fi.FullName.ToLowerInvariant();
{ if (!fullName.Contains(directory, StringComparison.Ordinal)) return null;
return null; string prefixedPath = fullName.Replace(directory, prefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
}
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);
}
} }
} }
} }
@@ -206,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)
@@ -271,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)
@@ -313,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
{ {
@@ -371,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)
@@ -429,8 +309,7 @@ public sealed class FileCacheManager : IHostedService
lock (_fileWriteLock) lock (_fileWriteLock)
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb.AppendLine(BuildVersionHeader()); foreach (var entry in _fileCaches.SelectMany(k => k.Value).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
foreach (var entry in _fileCaches.Values.SelectMany(k => k.Values).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
{ {
sb.AppendLine(entry.CsvEntry); sb.AppendLine(entry.CsvEntry);
} }
@@ -452,53 +331,6 @@ public sealed class FileCacheManager : IHostedService
} }
} }
private void EnsureCsvHeaderLocked()
{
if (!File.Exists(_csvPath))
{
return;
}
string[] existingLines = File.ReadAllLines(_csvPath);
if (existingLines.Length > 0 && TryParseVersionHeader(existingLines[0], out var existingVersion) && existingVersion == FileCacheVersion)
{
return;
}
StringBuilder rebuilt = new();
rebuilt.AppendLine(BuildVersionHeader());
foreach (var line in existingLines)
{
if (TryParseVersionHeader(line, out _))
{
continue;
}
if (!string.IsNullOrEmpty(line))
{
rebuilt.AppendLine(line);
}
}
File.WriteAllText(_csvPath, rebuilt.ToString());
}
private void BackupUnsupportedCache(string suffix)
{
var sanitizedSuffix = string.IsNullOrWhiteSpace(suffix) ? "unsupported" : $"{suffix}.unsupported";
var backupPath = _csvPath + "." + sanitizedSuffix;
try
{
File.Move(_csvPath, backupPath, overwrite: true);
_logger.LogWarning("Backed up unsupported file cache to {path}", backupPath);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to back up unsupported file cache to {path}", backupPath);
}
}
internal FileCacheEntity MigrateFileHashToExtension(FileCacheEntity fileCache, string ext) internal FileCacheEntity MigrateFileHashToExtension(FileCacheEntity fileCache, string ext)
{ {
try try
@@ -522,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)
@@ -537,15 +374,7 @@ public sealed class FileCacheManager : IHostedService
AddHashedFile(entity); AddHashedFile(entity);
lock (_fileWriteLock) lock (_fileWriteLock)
{ {
if (!File.Exists(_csvPath)) File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
{
File.WriteAllLines(_csvPath, new[] { BuildVersionHeader(), entity.CsvEntry });
}
else
{
EnsureCsvHeaderLocked();
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
}
} }
var result = GetFileCacheByPath(fileInfo.FullName); var result = GetFileCacheByPath(fileInfo.FullName);
_logger.LogTrace("Creating cache entity for {name} success: {success}", fileInfo.FullName, (result != null)); _logger.LogTrace("Creating cache entity for {name} success: {success}", fileInfo.FullName, (result != null));
@@ -664,111 +493,49 @@ public sealed class FileCacheManager : IHostedService
_logger.LogWarning("Could not load entries from {path}, continuing with empty file cache", _csvPath); _logger.LogWarning("Could not load entries from {path}, continuing with empty file cache", _csvPath);
} }
bool rewriteRequired = false; _logger.LogInformation("Found {amount} files in {path}", entries.Length, _csvPath);
bool parseEntries = entries.Length > 0;
int startIndex = 0;
if (entries.Length > 0)
{
var headerLine = entries[0];
var hasHeader = !string.IsNullOrEmpty(headerLine) &&
headerLine.StartsWith(FileCacheVersionHeaderPrefix, StringComparison.OrdinalIgnoreCase);
if (hasHeader)
{
if (!TryParseVersionHeader(headerLine, out var parsedVersion))
{
_logger.LogWarning("Failed to parse file cache version header \"{header}\". Backing up existing cache.", headerLine);
BackupUnsupportedCache("invalid-version");
parseEntries = false;
rewriteRequired = true;
entries = Array.Empty<string>();
}
else if (parsedVersion != FileCacheVersion)
{
_logger.LogWarning("Unsupported file cache version {version} detected (expected {expected}). Backing up existing cache.", parsedVersion, FileCacheVersion);
BackupUnsupportedCache($"v{parsedVersion}");
parseEntries = false;
rewriteRequired = true;
entries = Array.Empty<string>();
}
else
{
startIndex = 1;
}
}
else if (entries.Length > 0)
{
_logger.LogInformation("File cache missing version header, scheduling rewrite.");
rewriteRequired = true;
}
}
var totalEntries = Math.Max(0, entries.Length - startIndex);
Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase); Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase);
foreach (var entry in entries)
if (parseEntries && totalEntries > 0)
{ {
_logger.LogInformation("Found {amount} files in {path}", totalEntries, _csvPath); var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None);
try
for (var index = startIndex; index < entries.Length; index++)
{ {
var entry = entries[index]; var hash = splittedEntry[0];
if (string.IsNullOrWhiteSpace(entry)) if (hash.Length != 40) throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
var path = splittedEntry[1];
var time = splittedEntry[2];
if (processedFiles.ContainsKey(path))
{ {
_logger.LogWarning("Already processed {file}, ignoring", path);
continue; continue;
} }
var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None); processedFiles.Add(path, value: true);
try
long size = -1;
long compressed = -1;
if (splittedEntry.Length > 3)
{ {
var hash = splittedEntry[0]; if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result))
if (hash.Length != 40)
throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
var path = splittedEntry[1];
var time = splittedEntry[2];
if (processedFiles.ContainsKey(path))
{ {
_logger.LogWarning("Already processed {file}, ignoring", path); size = result;
continue;
} }
if (long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
processedFiles.Add(path, value: true);
long size = -1;
long compressed = -1;
if (splittedEntry.Length > 3)
{ {
if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result)) compressed = resultCompressed;
{
size = result;
}
if (splittedEntry.Length > 4 &&
long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
{
compressed = resultCompressed;
}
} }
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to initialize entry {entry}, ignoring", entry);
} }
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
} }
catch (Exception ex)
if (processedFiles.Count != totalEntries)
{ {
rewriteRequired = true; _logger.LogWarning(ex, "Failed to initialize entry {entry}, ignoring", entry);
} }
} }
else if (!parseEntries && entries.Length > 0)
{
_logger.LogInformation("Skipping existing file cache entries due to incompatible version.");
}
if (rewriteRequired) if (processedFiles.Count != entries.Length)
{ {
WriteOutFullCsv(); WriteOutFullCsv();
} }

View File

@@ -1,7 +0,0 @@
namespace LightlessSync.LightlessConfiguration.Configurations;
public enum LightfinderDtrDisplayMode
{
NearbyBroadcasts = 0,
PendingPairRequests = 1,
}

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;
@@ -22,13 +20,6 @@ public class LightlessConfig : ILightlessConfiguration
public DtrEntry.Colors DtrColorsDefault { get; set; } = default; public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu); public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u); public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u);
public bool ShowLightfinderInDtr { get; set; } = false;
public bool UseLightfinderColorsInDtr { get; set; } = true;
public DtrEntry.Colors DtrColorsLightfinderEnabled { get; set; } = new(Foreground: 0xB590FFu, Glow: 0x4F406Eu);
public DtrEntry.Colors DtrColorsLightfinderDisabled { get; set; } = new(Foreground: 0xD44444u, Glow: 0x642222u);
public DtrEntry.Colors DtrColorsLightfinderCooldown { get; set; } = new(Foreground: 0xFFE97Au, Glow: 0x766C3Au);
public DtrEntry.Colors DtrColorsLightfinderUnavailable { get; set; } = new(Foreground: 0x000000u, Glow: 0x000000u);
public LightfinderDtrDisplayMode LightfinderDtrDisplayMode { get; set; } = LightfinderDtrDisplayMode.PendingPairRequests;
public bool UseLightlessRedesign { get; set; } = true; public bool UseLightlessRedesign { get; set; } = true;
public bool EnableRightClickMenus { get; set; } = true; public bool EnableRightClickMenus { get; set; } = true;
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both; public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
@@ -42,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;
@@ -63,7 +51,6 @@ public class LightlessConfig : ILightlessConfiguration
public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false; public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false;
public bool ShowTransferBars { get; set; } = true; public bool ShowTransferBars { get; set; } = true;
public bool ShowTransferWindow { get; set; } = false; public bool ShowTransferWindow { get; set; } = false;
public bool UseNotificationsForDownloads { get; set; } = true;
public bool ShowUploading { get; set; } = true; public bool ShowUploading { get; set; } = true;
public bool ShowUploadingBigText { get; set; } = true; public bool ShowUploadingBigText { get; set; } = true;
public bool ShowVisibleUsersSeparately { get; set; } = true; public bool ShowVisibleUsersSeparately { get; set; } = true;
@@ -77,67 +64,13 @@ public class LightlessConfig : ILightlessConfiguration
public bool AutoPopulateEmptyNotesFromCharaName { get; set; } = false; public bool AutoPopulateEmptyNotesFromCharaName { get; set; } = false;
public int Version { get; set; } = 1; public int Version { get; set; } = 1;
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both; public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both;
// Lightless Notification Configuration
public bool UseLightlessNotifications { get; set; } = true;
public bool ShowNotificationProgress { get; set; } = true;
public NotificationLocation LightlessInfoNotification { get; set; } = NotificationLocation.LightlessUi;
public NotificationLocation LightlessWarningNotification { get; set; } = NotificationLocation.LightlessUi;
public NotificationLocation LightlessErrorNotification { get; set; } = NotificationLocation.ChatAndLightlessUi;
public NotificationLocation LightlessPairRequestNotification { get; set; } = NotificationLocation.LightlessUi;
public NotificationLocation LightlessDownloadNotification { get; set; } = NotificationLocation.TextOverlay;
// Basic Settings
public float NotificationOpacity { get; set; } = 0.95f;
public int MaxSimultaneousNotifications { get; set; } = 5;
public bool AutoDismissOnAction { get; set; } = true;
public bool DismissNotificationOnClick { get; set; } = false;
public bool ShowNotificationTimestamp { get; set; } = false;
// Position & Layout
public int NotificationOffsetY { get; set; } = 50;
public int NotificationOffsetX { get; set; } = 0;
public float NotificationWidth { get; set; } = 350f;
public float NotificationSpacing { get; set; } = 8f;
// Animation & Effects
public float NotificationAnimationSpeed { get; set; } = 10f;
public float NotificationAccentBarWidth { get; set; } = 3f;
// Duration per Type
public int InfoNotificationDurationSeconds { get; set; } = 10;
public int WarningNotificationDurationSeconds { get; set; } = 15;
public int ErrorNotificationDurationSeconds { get; set; } = 20;
public int PairRequestDurationSeconds { get; set; } = 180;
public int DownloadNotificationDurationSeconds { get; set; } = 300;
public uint CustomInfoSoundId { get; set; } = 2; // Se2
public uint CustomWarningSoundId { get; set; } = 16; // Se15
public uint CustomErrorSoundId { get; set; } = 16; // Se15
public uint PairRequestSoundId { get; set; } = 5; // Se5
public uint DownloadSoundId { get; set; } = 15; // Se14
public bool DisableInfoSound { get; set; } = true;
public bool DisableWarningSound { get; set; } = true;
public bool DisableErrorSound { get; set; } = true;
public bool DisablePairRequestSound { get; set; } = true;
public bool DisableDownloadSound { get; set; } = true;
public bool UseFocusTarget { get; set; } = false; public bool UseFocusTarget { get; set; } = false;
public bool overrideFriendColor { get; set; } = false; public bool overrideFriendColor { get; set; } = false;
public bool overridePartyColor { get; set; } = false; public bool overridePartyColor { get; set; } = false;
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 bool LightfinderAutoEnableOnConnect { 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 bool LightfinderLabelShowHidden { get; set; } = false;
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,21 +0,0 @@
using System;
using System.Numerics;
namespace LightlessSync.LightlessConfiguration.Configurations;
[Serializable]
public class UiStyleOverride
{
public uint? Color { get; set; }
public float? Float { get; set; }
public Vector2Config? Vector2 { get; set; }
public bool IsEmpty => Color is null && Float is null && Vector2 is null;
}
[Serializable]
public record struct Vector2Config(float X, float Y)
{
public static implicit operator Vector2(Vector2Config value) => new(value.X, value.Y);
public static implicit operator Vector2Config(Vector2 value) => new(value.X, value.Y);
}

View File

@@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
namespace LightlessSync.LightlessConfiguration.Configurations;
[Serializable]
public class UiThemeConfig : ILightlessConfiguration
{
public Dictionary<string, UiStyleOverride> StyleOverrides { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public int Version { get; set; } = 1;
}

View File

@@ -1,21 +1,16 @@
namespace LightlessSync.LightlessConfiguration.Models; namespace LightlessSync.LightlessConfiguration.Models;
public enum NotificationLocation public enum NotificationLocation
{ {
Nowhere, Nowhere,
Chat, Chat,
Toast, Toast,
Both, Both
LightlessUi,
ChatAndLightlessUi,
TextOverlay,
} }
public enum NotificationType public enum NotificationType
{ {
Info, Info,
Warning, Warning,
Error, Error
PairRequest,
Download
} }

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

@@ -1,14 +0,0 @@
using LightlessSync.LightlessConfiguration.Configurations;
namespace LightlessSync.LightlessConfiguration;
public class UiThemeConfigService : ConfigurationServiceBase<UiThemeConfig>
{
public const string ConfigName = "ui-theme.json";
public UiThemeConfigService(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.2.0</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>();
@@ -114,7 +113,6 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<PluginWarningNotificationService>(); collection.AddSingleton<PluginWarningNotificationService>();
collection.AddSingleton<FileCompactor>(); collection.AddSingleton<FileCompactor>();
collection.AddSingleton<TagHandler>(); collection.AddSingleton<TagHandler>();
collection.AddSingleton(s => new Lazy<ApiController>(() => s.GetRequiredService<ApiController>()));
collection.AddSingleton<PairRequestService>(); collection.AddSingleton<PairRequestService>();
collection.AddSingleton<IdDisplayHandler>(); collection.AddSingleton<IdDisplayHandler>();
collection.AddSingleton<PlayerPerformanceService>(); collection.AddSingleton<PlayerPerformanceService>();
@@ -143,20 +141,10 @@ public sealed class Plugin : IDalamudPlugin
clientState, objectTable, framework, gameGui, condition, gameData, targetManager, gameConfig, clientState, objectTable, framework, gameGui, condition, gameData, targetManager, gameConfig,
s.GetRequiredService<BlockedCharacterHandler>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BlockedCharacterHandler>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(),
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<PlayerPerformanceConfigService>())); s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<PlayerPerformanceConfigService>()));
collection.AddSingleton((s) => new DtrEntry( collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService<ILogger<DtrEntry>>(), dtrBar, s.GetRequiredService<LightlessConfigService>(),
s.GetRequiredService<ILogger<DtrEntry>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>()));
dtrBar,
s.GetRequiredService<LightlessConfigService>(),
s.GetRequiredService<LightlessMediator>(),
s.GetRequiredService<PairManager>(),
s.GetRequiredService<PairRequestService>(),
s.GetRequiredService<ApiController>(),
s.GetRequiredService<ServerConfigurationManager>(),
s.GetRequiredService<BroadcastService>(),
s.GetRequiredService<BroadcastScannerService>(),
s.GetRequiredService<DalamudUtilService>()));
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);
@@ -183,14 +171,9 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<IpcCallerPenumbra>(), s.GetRequiredService<IpcCallerGlamourer>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<IpcCallerPenumbra>(), s.GetRequiredService<IpcCallerGlamourer>(),
s.GetRequiredService<IpcCallerCustomize>(), s.GetRequiredService<IpcCallerHeels>(), s.GetRequiredService<IpcCallerHonorific>(), s.GetRequiredService<IpcCallerCustomize>(), s.GetRequiredService<IpcCallerHeels>(), s.GetRequiredService<IpcCallerHonorific>(),
s.GetRequiredService<IpcCallerMoodles>(), s.GetRequiredService<IpcCallerPetNames>(), s.GetRequiredService<IpcCallerBrio>())); s.GetRequiredService<IpcCallerMoodles>(), s.GetRequiredService<IpcCallerPetNames>(), s.GetRequiredService<IpcCallerBrio>()));
collection.AddSingleton((s) => new NotificationService( collection.AddSingleton((s) => new NotificationService(s.GetRequiredService<ILogger<NotificationService>>(),
s.GetRequiredService<ILogger<NotificationService>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<DalamudUtilService>(),
s.GetRequiredService<LightlessConfigService>(), notificationManager, chatGui, s.GetRequiredService<LightlessConfigService>()));
s.GetRequiredService<DalamudUtilService>(),
notificationManager,
chatGui,
s.GetRequiredService<LightlessMediator>(),
s.GetRequiredService<PairRequestService>()));
collection.AddSingleton((s) => collection.AddSingleton((s) =>
{ {
var httpClient = new HttpClient(); var httpClient = new HttpClient();
@@ -198,12 +181,10 @@ public sealed class Plugin : IDalamudPlugin
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
return httpClient; return httpClient;
}); });
collection.AddSingleton((s) => new UiThemeConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => collection.AddSingleton((s) =>
{ {
var cfg = new LightlessConfigService(pluginInterface.ConfigDirectory.FullName); var cfg = new LightlessConfigService(pluginInterface.ConfigDirectory.FullName);
var theme = s.GetRequiredService<UiThemeConfigService>(); LightlessSync.UI.Style.MainStyle.Init(cfg);
LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
return cfg; return cfg;
}); });
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
@@ -215,7 +196,6 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<LightlessConfigService>()); collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<LightlessConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<UiThemeConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<ServerConfigService>()); collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<NotesConfigService>()); collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<PairTagConfigService>()); collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<PairTagConfigService>());
@@ -227,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>()));
@@ -254,12 +235,6 @@ public sealed class Plugin : IDalamudPlugin
collection.AddScoped<WindowMediatorSubscriberBase, BroadcastUI>((s) => new BroadcastUI(s.GetRequiredService<ILogger<BroadcastUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>())); collection.AddScoped<WindowMediatorSubscriberBase, BroadcastUI>((s) => new BroadcastUI(s.GetRequiredService<ILogger<BroadcastUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>()));
collection.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>((s) => new SyncshellFinderUI(s.GetRequiredService<ILogger<SyncshellFinderUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<DalamudUtilService>())); collection.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>((s) => new SyncshellFinderUI(s.GetRequiredService<ILogger<SyncshellFinderUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<DalamudUtilService>()));
collection.AddScoped<IPopupHandler, BanUserPopupHandler>(); collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
collection.AddScoped<WindowMediatorSubscriberBase, LightlessNotificationUI>((s) =>
new LightlessNotificationUI(
s.GetRequiredService<ILogger<LightlessNotificationUI>>(),
s.GetRequiredService<LightlessMediator>(),
s.GetRequiredService<PerformanceCollectorService>(),
s.GetRequiredService<LightlessConfigService>()));
collection.AddScoped<IPopupHandler, CensusPopupHandler>(); collection.AddScoped<IPopupHandler, CensusPopupHandler>();
collection.AddScoped<CacheCreationService>(); collection.AddScoped<CacheCreationService>();
collection.AddScoped<PlayerDataFactory>(); collection.AddScoped<PlayerDataFactory>();
@@ -267,9 +242,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddScoped((s) => new UiService(s.GetRequiredService<ILogger<UiService>>(), pluginInterface.UiBuilder, s.GetRequiredService<LightlessConfigService>(), collection.AddScoped((s) => new UiService(s.GetRequiredService<ILogger<UiService>>(), pluginInterface.UiBuilder, s.GetRequiredService<LightlessConfigService>(),
s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(), s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(),
s.GetRequiredService<UiFactory>(), s.GetRequiredService<UiFactory>(),
s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<LightlessMediator>()));
s.GetRequiredService<LightlessMediator>(),
s.GetRequiredService<NotificationService>()));
collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(), collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(),
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<ApiController>(),
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<LightlessConfigService>())); s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<LightlessConfigService>()));
@@ -279,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>());

View File

@@ -211,16 +211,6 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
UpdateSyncshellBroadcasts(); UpdateSyncshellBroadcasts();
} }
public int CountActiveBroadcasts(string? excludeHashedCid = null)
{
var now = DateTime.UtcNow;
var comparer = StringComparer.Ordinal;
return _broadcastCache.Count(entry =>
entry.Value.IsBroadcasting &&
entry.Value.ExpiryTime > now &&
(excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid)));
}
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing); base.Dispose(disposing);

View File

@@ -7,7 +7,6 @@ using LightlessSync.WebAPI;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Threading;
namespace LightlessSync.Services; namespace LightlessSync.Services;
public class BroadcastService : IHostedService, IMediatorSubscriber public class BroadcastService : IHostedService, IMediatorSubscriber
@@ -17,11 +16,9 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
private readonly LightlessMediator _mediator; private readonly LightlessMediator _mediator;
private readonly LightlessConfigService _config; private readonly LightlessConfigService _config;
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
private CancellationTokenSource? _lightfinderCancelTokens;
private Action? _connectedHandler;
public LightlessMediator Mediator => _mediator; public LightlessMediator Mediator => _mediator;
public bool IsLightFinderAvailable { get; private set; } = false; public bool IsLightFinderAvailable { get; private set; } = true;
public bool IsBroadcasting => _config.Current.BroadcastEnabled; public bool IsBroadcasting => _config.Current.BroadcastEnabled;
private bool _syncedOnStartup = false; private bool _syncedOnStartup = false;
@@ -60,125 +57,24 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
await action().ConfigureAwait(false); await action().ConfigureAwait(false);
} }
private async Task<string?> GetLocalHashedCidAsync(string context) public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false);
return cid.ToString().GetHash256();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to resolve CID for {Context}", context);
return null;
}
}
private void ApplyBroadcastDisabled(bool forcePublish = false)
{
bool wasEnabled = _config.Current.BroadcastEnabled;
bool hadExpiry = _config.Current.BroadcastTtl != DateTime.MinValue;
bool hadRemaining = _remainingTtl.HasValue;
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
if (wasEnabled || hadExpiry)
_config.Save();
_remainingTtl = null;
_waitingForTtlFetch = false;
_syncedOnStartup = false;
if (forcePublish || wasEnabled || hadRemaining)
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
}
private bool TryApplyBroadcastEnabled(TimeSpan? ttl, string context)
{
if (ttl is not { } validTtl || validTtl <= TimeSpan.Zero)
{
_logger.LogWarning("Lightfinder enable skipped ({Context}): invalid TTL ({TTL})", context, ttl);
return false;
}
bool wasEnabled = _config.Current.BroadcastEnabled;
TimeSpan? previousRemaining = _remainingTtl;
DateTime previousExpiry = _config.Current.BroadcastTtl;
var newExpiry = DateTime.UtcNow + validTtl;
_config.Current.BroadcastEnabled = true;
_config.Current.BroadcastTtl = newExpiry;
if (!wasEnabled || previousExpiry != newExpiry)
_config.Save();
_remainingTtl = validTtl;
_waitingForTtlFetch = false;
if (!wasEnabled || previousRemaining != validTtl)
_mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl));
_logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl);
return true;
}
private void HandleLightfinderUnavailable(string message, Exception? ex = null)
{
if (ex != null)
_logger.LogWarning(ex, message);
else
_logger.LogWarning(message);
IsLightFinderAvailable = false;
ApplyBroadcastDisabled(forcePublish: true);
}
private void OnDisconnected()
{
IsLightFinderAvailable = false;
ApplyBroadcastDisabled(forcePublish: true);
_logger.LogDebug("Cleared Lightfinder state due to disconnect.");
}
public Task StartAsync(CancellationToken cancellationToken)
{ {
_mediator.Subscribe<EnableBroadcastMessage>(this, OnEnableBroadcast); _mediator.Subscribe<EnableBroadcastMessage>(this, OnEnableBroadcast);
_mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged); _mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick); _mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
_mediator.Subscribe<DisconnectedMessage>(this, _ => OnDisconnected());
IsLightFinderAvailable = false; _apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken);
//_ = CheckLightfinderSupportAsync(cancellationToken);
_lightfinderCancelTokens?.Cancel();
_lightfinderCancelTokens?.Dispose();
_lightfinderCancelTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_connectedHandler = () => _ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token);
_apiController.OnConnected += _connectedHandler;
if (_apiController.IsConnected)
_ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token);
return Task.CompletedTask;
} }
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
_lightfinderCancelTokens?.Cancel();
_lightfinderCancelTokens?.Dispose();
_lightfinderCancelTokens = null;
if (_connectedHandler is not null)
{
_apiController.OnConnected -= _connectedHandler;
_connectedHandler = null;
}
_mediator.UnsubscribeAll(this); _mediator.UnsubscribeAll(this);
_apiController.OnConnected -= () => _ = CheckLightfinderSupportAsync(cancellationToken);
return Task.CompletedTask; return Task.CompletedTask;
} }
// need to rework this, this is cooked
private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken) private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken)
{ {
try try
@@ -189,54 +85,25 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
return; return;
var hashedCid = await GetLocalHashedCidAsync("Lightfinder state check").ConfigureAwait(false); var dummy = "0".PadLeft(64, '0');
if (string.IsNullOrEmpty(hashedCid))
return;
BroadcastStatusInfoDto? status = null; await _apiController.IsUserBroadcasting(dummy).ConfigureAwait(false);
try await _apiController.SetBroadcastStatus(dummy, true, null).ConfigureAwait(false);
{ await _apiController.GetBroadcastTtl(dummy).ConfigureAwait(false);
status = await _apiController.IsUserBroadcasting(hashedCid).ConfigureAwait(false); await _apiController.AreUsersBroadcasting([dummy]).ConfigureAwait(false);
}
catch (HubException ex) when (ex.Message.Contains("Method does not exist", StringComparison.OrdinalIgnoreCase))
{
HandleLightfinderUnavailable("Lightfinder unavailable on server (required method missing).", ex);
}
if (!IsLightFinderAvailable)
_logger.LogInformation("Lightfinder is available.");
IsLightFinderAvailable = true; IsLightFinderAvailable = true;
_logger.LogInformation("Lightfinder is available.");
}
catch (HubException ex) when (ex.Message.Contains("Method does not exist"))
{
_logger.LogWarning("Lightfinder unavailable: required method missing.");
IsLightFinderAvailable = false;
bool isBroadcasting = status?.IsBroadcasting == true; _config.Current.BroadcastEnabled = false;
TimeSpan? ttl = status?.TTL; _config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
if (isBroadcasting) _mediator.Publish(new BroadcastStatusChangedMessage(false, null));
{
if (ttl is not { } remaining || remaining <= TimeSpan.Zero)
ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
if (TryApplyBroadcastEnabled(ttl, "server handshake"))
{
_syncedOnStartup = true;
}
else
{
isBroadcasting = false;
}
}
if (!isBroadcasting)
{
ApplyBroadcastDisabled(forcePublish: true);
_logger.LogInformation("Lightfinder is available but no active broadcast was found.");
}
if (_config.Current.LightfinderAutoEnableOnConnect && !isBroadcasting)
{
_logger.LogInformation("Auto-enabling Lightfinder broadcast after reconnect.");
_mediator.Publish(new EnableBroadcastMessage(hashedCid, true));
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -244,7 +111,14 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
} }
catch (Exception ex) catch (Exception ex)
{ {
HandleLightfinderUnavailable("Lightfinder check failed.", ex); _logger.LogWarning(ex, "Lightfinder check failed.");
IsLightFinderAvailable = false;
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
} }
} }
@@ -265,38 +139,46 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
}; };
} }
await _apiController.SetBroadcastStatus(msg.Enabled, groupDto).ConfigureAwait(false); await _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false);
_logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid); _logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid);
if (!msg.Enabled) if (!msg.Enabled)
{ {
ApplyBroadcastDisabled(forcePublish: true); _config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}"))); Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}")));
return; return;
} }
_waitingForTtlFetch = true; _waitingForTtlFetch = true;
try TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
{
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
if (TryApplyBroadcastEnabled(ttl, "client request")) if (ttl is { } remaining && remaining > TimeSpan.Zero)
{
_logger.LogDebug("Fetched TTL from server: {TTL}", ttl);
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
}
else
{
ApplyBroadcastDisabled(forcePublish: true);
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
}
}
finally
{ {
_waitingForTtlFetch = false; _config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
_config.Current.BroadcastEnabled = true;
_config.Save();
_logger.LogDebug("Fetched TTL from server: {TTL}", remaining);
_mediator.Publish(new BroadcastStatusChangedMessage(true, remaining));
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
} }
else
{
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
}
_waitingForTtlFetch = false;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -337,24 +219,17 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
return result; return result;
} }
public async Task<TimeSpan?> GetBroadcastTtlAsync(string? cidForLog = null) public async Task<TimeSpan?> GetBroadcastTtlAsync(string cid)
{ {
TimeSpan? ttl = null; TimeSpan? ttl = null;
await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => { await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => {
try try
{ {
ttl = await _apiController.GetBroadcastTtl().ConfigureAwait(false); ttl = await _apiController.GetBroadcastTtl(cid).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
if (cidForLog is { Length: > 0 }) _logger.LogWarning(ex, "Failed to fetch broadcast TTL for {cid}", cid);
{
_logger.LogWarning(ex, "Failed to fetch broadcast TTL for {Cid}", cidForLog);
}
else
{
_logger.LogWarning(ex, "Failed to fetch broadcast TTL");
}
} }
}).ConfigureAwait(false); }).ConfigureAwait(false);
return ttl; return ttl;
@@ -406,12 +281,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
return; return;
} }
var hashedCid = await GetLocalHashedCidAsync(nameof(ToggleBroadcast)).ConfigureAwait(false); var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
if (string.IsNullOrEmpty(hashedCid))
{
_logger.LogWarning("ToggleBroadcast - unable to resolve CID.");
return;
}
try try
{ {
@@ -451,31 +321,31 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
await RequireConnectionAsync(nameof(OnTick), async () => { await RequireConnectionAsync(nameof(OnTick), async () => {
if (!_syncedOnStartup && _config.Current.BroadcastEnabled) if (!_syncedOnStartup && _config.Current.BroadcastEnabled)
{ {
_syncedOnStartup = true;
try try
{ {
var hashedCid = await GetLocalHashedCidAsync("startup TTL refresh").ConfigureAwait(false); string hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
if (string.IsNullOrEmpty(hashedCid))
{
_logger.LogDebug("Skipping TTL refresh; hashed CID unavailable.");
return;
}
TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false); TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
if (TryApplyBroadcastEnabled(ttl, "startup TTL refresh")) if (ttl is { }
remaining && remaining > TimeSpan.Zero)
{ {
_syncedOnStartup = true; _config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", ttl); _config.Current.BroadcastEnabled = true;
_config.Save();
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining);
} }
else else
{ {
_logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state."); _logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state.");
ApplyBroadcastDisabled(forcePublish: true); _config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Failed to refresh TTL in OnTick"); _logger.LogError(ex, "Failed to refresh TTL in OnTick");
_syncedOnStartup = false;
} }
} }
if (_config.Current.BroadcastEnabled) if (_config.Current.BroadcastEnabled)
@@ -492,7 +362,10 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
if (_remainingTtl == null) if (_remainingTtl == null)
{ {
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally."); _logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
ApplyBroadcastDisabled(forcePublish: true); _config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
} }
} }
else else

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 finder - 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], "finder", 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
{ {
@@ -147,7 +142,7 @@ internal class ContextMenuService : IHostedService
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address); var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
await _apiController.TryPairWithContentId(receiverCid).ConfigureAwait(false); await _apiController.TryPairWithContentId(receiverCid, senderCid).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(receiverCid)) if (!string.IsNullOrWhiteSpace(receiverCid))
{ {
_pairRequestService.RemoveRequest(receiverCid); _pairRequestService.RemoveRequest(receiverCid);
@@ -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;
@@ -17,7 +17,6 @@ namespace LightlessSync.Services.Mediator;
public record SwitchToIntroUiMessage : MessageBase; public record SwitchToIntroUiMessage : MessageBase;
public record SwitchToMainUiMessage : MessageBase; public record SwitchToMainUiMessage : MessageBase;
public record OpenSettingsUiMessage : MessageBase; public record OpenSettingsUiMessage : MessageBase;
public record OpenLightfinderSettingsMessage : MessageBase;
public record DalamudLoginMessage : MessageBase; public record DalamudLoginMessage : MessageBase;
public record DalamudLogoutMessage : MessageBase; public record DalamudLogoutMessage : MessageBase;
public record PriorityFrameworkUpdateMessage : SameThreadMessage; public record PriorityFrameworkUpdateMessage : SameThreadMessage;
@@ -54,8 +53,6 @@ public record NotificationMessage
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage; public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage; public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage; public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
public record LightlessNotificationMessage(LightlessSync.UI.Models.LightlessNotification Notification) : MessageBase;
public record LightlessNotificationDismissMessage(string NotificationId) : MessageBase;
public record CharacterDataAnalyzedMessage : MessageBase; public record CharacterDataAnalyzedMessage : MessageBase;
public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase; public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase;
public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase; public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase;
@@ -80,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,4 +1,5 @@
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using LightlessSync.UI.Style;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace LightlessSync.Services.Mediator; namespace LightlessSync.Services.Mediator;
@@ -33,6 +34,18 @@ public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
public override void PreDraw()
{
base.PreDraw();
MainStyle.PushStyle(); // internally checks ShouldUseTheme
}
public override void PostDraw()
{
MainStyle.PopStyle(); // always attempts to pop if pushed
base.PostDraw();
}
public override void Draw() public override void Draw()
{ {
_performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal); _performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal);

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;
@@ -221,217 +188,32 @@ public unsafe class NameplateHandler : IMediatorSubscriber
if (pNode == null) if (pNode == null)
continue; continue;
if (mpNameplateAddon == null)
continue;
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()) || _configService.Current.LightfinderLabelShowHidden;
pNode->AtkResNode.ToggleVisibility(IsVisible);
if (nameplateObject.RootComponentNode == null ||
nameplateObject.NameContainer == null ||
nameplateObject.NameText == null)
{
pNode->AtkResNode.ToggleVisibility(false);
continue;
}
var nameContainer = nameplateObject.NameContainer; var nameContainer = nameplateObject.NameContainer;
var nameText = nameplateObject.NameText; var nameText = nameplateObject.NameText;
if (nameContainer == null || nameText == null) var labelColor = UIColors.Get("LightlessPurple");
{ var edgeColor = UIColors.Get("FullBlack");
pNode->AtkResNode.ToggleVisibility(false);
continue;
}
var labelColor = UIColors.Get("Lightfinder"); var labelY = nameContainer->Height - nameplateObject.TextH - (int)(24 * nameText->AtkResNode.ScaleY);
var edgeColor = UIColors.Get("LightfinderEdge");
var config = _configService.Current;
var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f); pNode->AtkResNode.SetPositionShort(58, (short)labelY);
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.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.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
labelContent = DefaultLabelText;
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
pNode->SetText(labelContent);
if (!config.LightfinderLabelUseIcon)
{
pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize;
pNode->AtkResNode.Width = 0;
nodeWidth = (int)pNode->AtkResNode.GetWidth();
if (nodeWidth <= 0)
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
pNode->AtkResNode.Width = (ushort)nodeWidth;
}
else
{
pNode->TextFlags |= TextFlags.AutoAdjustNodeSize;
pNode->AtkResNode.Width = 0;
nodeWidth = pNode->AtkResNode.GetWidth();
}
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
{
var nameplateWidth = (int)nameContainer->Width;
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;
@@ -445,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];
@@ -565,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()
{ {
@@ -599,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,495 +1,62 @@
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models; using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.UI;
using LightlessSync.UI.Models;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using FFXIVClientStructs.FFXIV.Client.UI;
using NotificationType = LightlessSync.LightlessConfiguration.Models.NotificationType; using NotificationType = LightlessSync.LightlessConfiguration.Models.NotificationType;
namespace LightlessSync.Services; namespace LightlessSync.Services;
public class NotificationService : DisposableMediatorSubscriberBase, IHostedService public class NotificationService : DisposableMediatorSubscriberBase, IHostedService
{ {
private readonly ILogger<NotificationService> _logger;
private readonly LightlessConfigService _configService;
private readonly DalamudUtilService _dalamudUtilService; private readonly DalamudUtilService _dalamudUtilService;
private readonly INotificationManager _notificationManager; private readonly INotificationManager _notificationManager;
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly PairRequestService _pairRequestService; private readonly LightlessConfigService _configurationService;
private readonly HashSet<string> _shownPairRequestNotifications = new();
public NotificationService( public NotificationService(ILogger<NotificationService> logger, LightlessMediator mediator,
ILogger<NotificationService> logger,
LightlessConfigService configService,
DalamudUtilService dalamudUtilService, DalamudUtilService dalamudUtilService,
INotificationManager notificationManager, INotificationManager notificationManager,
IChatGui chatGui, IChatGui chatGui, LightlessConfigService configurationService) : base(logger, mediator)
LightlessMediator mediator,
PairRequestService pairRequestService) : base(logger, mediator)
{ {
_logger = logger;
_configService = configService;
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
_notificationManager = notificationManager; _notificationManager = notificationManager;
_chatGui = chatGui; _chatGui = chatGui;
_pairRequestService = pairRequestService; _configurationService = configurationService;
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage); Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated);
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken)
public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info,
TimeSpan? duration = null, List<LightlessNotificationAction>? actions = null, uint? soundEffectId = null)
{ {
var notification = CreateNotification(title, message, type, duration, actions, soundEffectId); return Task.CompletedTask;
if (_configService.Current.AutoDismissOnAction && notification.Actions.Any())
{
WrapActionsWithAutoDismiss(notification);
}
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
} }
private LightlessNotification CreateNotification(string title, string message, NotificationType type, private void PrintErrorChat(string? message)
TimeSpan? duration, List<LightlessNotificationAction>? actions, uint? soundEffectId)
{ {
return new LightlessNotification SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
{ _chatGui.PrintError(se.BuiltString);
Title = title,
Message = message,
Type = type,
Duration = duration ?? GetDefaultDurationForType(type),
Actions = actions ?? new List<LightlessNotificationAction>(),
SoundEffectId = GetSoundEffectId(type, soundEffectId),
ShowProgress = _configService.Current.ShowNotificationProgress,
CreatedAt = DateTime.UtcNow
};
} }
private void WrapActionsWithAutoDismiss(LightlessNotification notification) private void PrintInfoChat(string? message)
{ {
foreach (var action in notification.Actions) SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ").AddItalics(message ?? string.Empty);
{ _chatGui.Print(se.BuiltString);
var originalOnClick = action.OnClick;
action.OnClick = (n) =>
{
originalOnClick(n);
if (_configService.Current.AutoDismissOnAction)
{
DismissNotification(n);
}
};
}
} }
private void DismissNotification(LightlessNotification notification) private void PrintWarnChat(string? message)
{ {
notification.IsDismissed = true; SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
notification.IsAnimatingOut = true; _chatGui.Print(se.BuiltString);
} }
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
{
var notification = new LightlessNotification
{
Id = $"pair_request_{senderId}",
Title = "Pair Request Received",
Message = $"{senderName} wants to directly pair with you.",
Type = NotificationType.PairRequest,
Duration = TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
SoundEffectId = GetPairRequestSoundId(),
Actions = CreatePairRequestActions(onAccept, onDecline)
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
private uint? GetPairRequestSoundId() =>
!_configService.Current.DisablePairRequestSound ? _configService.Current.PairRequestSoundId : null;
private List<LightlessNotificationAction> CreatePairRequestActions(Action onAccept, Action onDecline)
{
return new List<LightlessNotificationAction>
{
new()
{
Id = "accept",
Label = "Accept",
Icon = FontAwesomeIcon.Check,
Color = UIColors.Get("LightlessGreen"),
IsPrimary = true,
OnClick = (n) =>
{
_logger.LogInformation("Pair request accepted");
onAccept();
DismissNotification(n);
}
},
new()
{
Id = "decline",
Label = "Decline",
Icon = FontAwesomeIcon.Times,
Color = UIColors.Get("DimRed"),
IsDestructive = true,
OnClick = (n) =>
{
_logger.LogInformation("Pair request declined");
onDecline();
DismissNotification(n);
}
}
};
}
public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null)
{
var notification = new LightlessNotification
{
Title = "Download Complete",
Message = FormatDownloadCompleteMessage(fileName, fileCount),
Type = NotificationType.Info,
Duration = TimeSpan.FromSeconds(8),
Actions = CreateDownloadCompleteActions(onOpenFolder),
SoundEffectId = NotificationSounds.DownloadComplete
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
private string FormatDownloadCompleteMessage(string fileName, int fileCount) =>
fileCount > 1
? $"Downloaded {fileCount} files successfully."
: $"Downloaded {fileName} successfully.";
private List<LightlessNotificationAction> CreateDownloadCompleteActions(Action? onOpenFolder)
{
var actions = new List<LightlessNotificationAction>();
if (onOpenFolder != null)
{
actions.Add(new LightlessNotificationAction
{
Id = "open_folder",
Label = "Open Folder",
Icon = FontAwesomeIcon.FolderOpen,
Color = UIColors.Get("LightlessBlue"),
OnClick = (n) =>
{
onOpenFolder();
DismissNotification(n);
}
});
}
return actions;
}
public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null,
Action? onViewLog = null)
{
var notification = new LightlessNotification
{
Title = title,
Message = FormatErrorMessage(message, exception),
Type = NotificationType.Error,
Duration = TimeSpan.FromSeconds(15),
Actions = CreateErrorActions(onRetry, onViewLog),
SoundEffectId = NotificationSounds.Error
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
private string FormatErrorMessage(string message, Exception? exception) =>
exception != null ? $"{message}\n\nError: {exception.Message}" : message;
private List<LightlessNotificationAction> CreateErrorActions(Action? onRetry, Action? onViewLog)
{
var actions = new List<LightlessNotificationAction>();
if (onRetry != null)
{
actions.Add(new LightlessNotificationAction
{
Id = "retry",
Label = "Retry",
Icon = FontAwesomeIcon.Redo,
Color = UIColors.Get("LightlessBlue"),
OnClick = (n) =>
{
onRetry();
DismissNotification(n);
}
});
}
if (onViewLog != null)
{
actions.Add(new LightlessNotificationAction
{
Id = "view_log",
Label = "View Log",
Icon = FontAwesomeIcon.FileAlt,
Color = UIColors.Get("LightlessYellow"),
OnClick = (n) => onViewLog()
});
}
return actions;
}
public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus,
int queueWaiting = 0)
{
var userDownloads = downloadStatus.Where(x => x.playerName != "Pair Queue").ToList();
var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.progress) : 0f;
var message = BuildPairDownloadMessage(userDownloads, queueWaiting);
var notification = new LightlessNotification
{
Id = "pair_download_progress",
Title = "Downloading Pair Data",
Message = message,
Type = NotificationType.Download,
Duration = TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
ShowProgress = true,
Progress = totalProgress
};
Mediator.Publish(new LightlessNotificationMessage(notification));
if (AreAllDownloadsCompleted(userDownloads))
{
DismissPairDownloadNotification();
}
}
private string BuildPairDownloadMessage(List<(string playerName, float progress, string status)> userDownloads,
int queueWaiting)
{
var messageParts = new List<string>();
if (queueWaiting > 0)
{
messageParts.Add($"Queue: {queueWaiting} waiting");
}
if (userDownloads.Count > 0)
{
var completedCount = userDownloads.Count(x => x.progress >= 1.0f);
messageParts.Add($"Progress: {completedCount}/{userDownloads.Count} completed");
}
var activeDownloadLines = BuildActiveDownloadLines(userDownloads);
if (!string.IsNullOrEmpty(activeDownloadLines))
{
messageParts.Add(activeDownloadLines);
}
return string.Join("\n", messageParts);
}
private string BuildActiveDownloadLines(List<(string playerName, float progress, string status)> userDownloads)
{
var activeDownloads = userDownloads
.Where(x => x.progress < 1.0f)
.Take(_configService.Current.MaxConcurrentPairApplications);
if (!activeDownloads.Any()) return string.Empty;
return string.Join("\n", activeDownloads.Select(x => $"• {x.playerName}: {FormatDownloadStatus(x)}"));
}
private string FormatDownloadStatus((string playerName, float progress, string status) download) =>
download.status switch
{
"downloading" => $"{download.progress:P0}",
"decompressing" => "decompressing",
"queued" => "queued",
"waiting" => "waiting for slot",
_ => download.status
};
private bool AreAllDownloadsCompleted(List<(string playerName, float progress, string status)> userDownloads) =>
userDownloads.Any() && userDownloads.All(x => x.progress >= 1.0f);
public void DismissPairDownloadNotification() =>
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
private TimeSpan GetDefaultDurationForType(NotificationType type) => type switch
{
NotificationType.Info => TimeSpan.FromSeconds(_configService.Current.InfoNotificationDurationSeconds),
NotificationType.Warning => TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds),
NotificationType.Error => TimeSpan.FromSeconds(_configService.Current.ErrorNotificationDurationSeconds),
NotificationType.PairRequest => TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
NotificationType.Download => TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
_ => TimeSpan.FromSeconds(10)
};
private uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId)
{
if (overrideSoundId.HasValue) return overrideSoundId;
if (IsSoundDisabledForType(type)) return null;
return GetConfiguredSoundForType(type);
}
private bool IsSoundDisabledForType(NotificationType type) => type switch
{
NotificationType.Info => _configService.Current.DisableInfoSound,
NotificationType.Warning => _configService.Current.DisableWarningSound,
NotificationType.Error => _configService.Current.DisableErrorSound,
NotificationType.Download => _configService.Current.DisableDownloadSound,
_ => false
};
private uint GetConfiguredSoundForType(NotificationType type) => type switch
{
NotificationType.Info => _configService.Current.CustomInfoSoundId,
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
NotificationType.Error => _configService.Current.CustomErrorSoundId,
NotificationType.Download => _configService.Current.DownloadSoundId,
_ => NotificationSounds.GetDefaultSound(type)
};
private void PlayNotificationSound(uint soundEffectId)
{
try
{
UIGlobals.PlayChatSoundEffect(soundEffectId);
_logger.LogDebug("Played notification sound effect {SoundId} via ChatGui", soundEffectId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to play notification sound effect {SoundId}", soundEffectId);
}
}
private void HandleNotificationMessage(NotificationMessage msg)
{
_logger.LogInformation("{msg}", msg.ToString());
if (!_dalamudUtilService.IsLoggedIn) return;
var location = GetNotificationLocation(msg.Type);
ShowNotificationLocationBased(msg, location);
}
private NotificationLocation GetNotificationLocation(NotificationType type) =>
_configService.Current.UseLightlessNotifications
? GetLightlessNotificationLocation(type)
: GetClassicNotificationLocation(type);
private NotificationLocation GetLightlessNotificationLocation(NotificationType type) => type switch
{
NotificationType.Info => _configService.Current.LightlessInfoNotification,
NotificationType.Warning => _configService.Current.LightlessWarningNotification,
NotificationType.Error => _configService.Current.LightlessErrorNotification,
NotificationType.PairRequest => _configService.Current.LightlessPairRequestNotification,
NotificationType.Download => _configService.Current.LightlessDownloadNotification,
_ => NotificationLocation.LightlessUi
};
private NotificationLocation GetClassicNotificationLocation(NotificationType type) => type switch
{
NotificationType.Info => _configService.Current.InfoNotification,
NotificationType.Warning => _configService.Current.WarningNotification,
NotificationType.Error => _configService.Current.ErrorNotification,
NotificationType.PairRequest => NotificationLocation.Toast,
NotificationType.Download => NotificationLocation.Toast,
_ => NotificationLocation.Nowhere
};
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
{
switch (location)
{
case NotificationLocation.Toast:
ShowToast(msg);
break;
case NotificationLocation.Chat:
ShowChat(msg);
break;
case NotificationLocation.Both:
ShowToast(msg);
ShowChat(msg);
break;
case NotificationLocation.LightlessUi:
ShowLightlessNotification(msg);
break;
case NotificationLocation.ChatAndLightlessUi:
ShowChat(msg);
ShowLightlessNotification(msg);
break;
case NotificationLocation.Nowhere:
break;
}
}
private void ShowLightlessNotification(NotificationMessage msg)
{
var duration = msg.TimeShownOnScreen ?? GetDefaultDurationForType(msg.Type);
ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, null);
}
private void ShowToast(NotificationMessage msg)
{
var dalamudType = ConvertToDalamudNotificationType(msg.Type);
_notificationManager.AddNotification(new Notification()
{
Content = msg.Message ?? string.Empty,
Title = msg.Title,
Type = dalamudType,
Minimized = false,
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
});
}
private Dalamud.Interface.ImGuiNotification.NotificationType
ConvertToDalamudNotificationType(NotificationType type) => type switch
{
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
};
private void ShowChat(NotificationMessage msg) private void ShowChat(NotificationMessage msg)
{ {
switch (msg.Type) switch (msg.Type)
@@ -508,54 +75,67 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
} }
} }
private void PrintErrorChat(string? message) private void ShowNotification(NotificationMessage msg)
{ {
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message); Logger.LogInformation("{msg}", msg.ToString());
_chatGui.PrintError(se.BuiltString);
}
private void PrintInfoChat(string? message) if (!_dalamudUtilService.IsLoggedIn) return;
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ")
.AddItalics(message ?? string.Empty);
_chatGui.Print(se.BuiltString);
}
private void PrintWarnChat(string? message) switch (msg.Type)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
.AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
_chatGui.Print(se.BuiltString);
}
private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _)
{
var activeRequests = _pairRequestService.GetActiveRequests();
var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet();
// Dismiss notifications for requests that are no longer active
var notificationsToRemove = _shownPairRequestNotifications
.Where(hashedCid => !activeRequestIds.Contains(hashedCid))
.ToList();
foreach (var hashedCid in notificationsToRemove)
{ {
var notificationId = $"pair_request_{hashedCid}"; case NotificationType.Info:
Mediator.Publish(new LightlessNotificationDismissMessage(notificationId)); ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
_shownPairRequestNotifications.Remove(hashedCid); break;
}
// Show/update notifications for all active requests case NotificationType.Warning:
foreach (var request in activeRequests) ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
{ break;
_shownPairRequestNotifications.Add(request.HashedCid);
ShowPairRequestNotification( case NotificationType.Error:
request.DisplayName, ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
request.HashedCid, break;
() => _pairRequestService.AcceptPairRequest(request.HashedCid, request.DisplayName),
() => _pairRequestService.DeclinePairRequest(request.HashedCid)
);
} }
} }
}
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
{
switch (location)
{
case NotificationLocation.Toast:
ShowToast(msg);
break;
case NotificationLocation.Chat:
ShowChat(msg);
break;
case NotificationLocation.Both:
ShowToast(msg);
ShowChat(msg);
break;
case NotificationLocation.Nowhere:
break;
}
}
private void ShowToast(NotificationMessage msg)
{
Dalamud.Interface.ImGuiNotification.NotificationType dalamudType = msg.Type switch
{
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
NotificationType.Info => Dalamud.Interface.ImGuiNotification.NotificationType.Info,
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
};
_notificationManager.AddNotification(new Notification()
{
Content = msg.Message ?? string.Empty,
Title = msg.Title,
Type = dalamudType,
Minimized = false,
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
});
}
}

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

@@ -1,8 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Pairs; using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -13,18 +11,16 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
{ {
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly Lazy<WebAPI.ApiController> _apiController;
private readonly object _syncRoot = new(); private readonly object _syncRoot = new();
private readonly List<PairRequestEntry> _requests = []; private readonly List<PairRequestEntry> _requests = [];
private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5); private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5);
public PairRequestService(ILogger<PairRequestService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager, Lazy<WebAPI.ApiController> apiController) public PairRequestService(ILogger<PairRequestService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager)
: base(logger, mediator) : base(logger, mediator)
{ {
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
_pairManager = pairManager; _pairManager = pairManager;
_apiController = apiController;
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ => Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ =>
{ {
@@ -187,40 +183,6 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
return _requests.RemoveAll(r => now - r.ReceivedAt > Expiration) > 0; return _requests.RemoveAll(r => now - r.ReceivedAt > Expiration) > 0;
} }
public void AcceptPairRequest(string hashedCid, string displayName)
{
_ = Task.Run(async () =>
{
try
{
await _apiController.Value.TryPairWithContentId(hashedCid).ConfigureAwait(false);
RemoveRequest(hashedCid);
var displayText = string.IsNullOrEmpty(displayName) ? hashedCid : displayName;
Mediator.Publish(new NotificationMessage(
"Pair request accepted",
$"Sent a pair request back to {displayText}.",
NotificationType.Info,
TimeSpan.FromSeconds(3)));
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to accept pair request for {HashedCid}", hashedCid);
Mediator.Publish(new NotificationMessage(
"Failed to Accept Pair Request",
ex.Message,
NotificationType.Error,
TimeSpan.FromSeconds(5)));
}
});
}
public void DeclinePairRequest(string hashedCid)
{
RemoveRequest(hashedCid);
Logger.LogDebug("Declined pair request from {HashedCid}", hashedCid);
}
private record struct PairRequestEntry(string HashedCid, string MessageTemplate, DateTime ReceivedAt); private record struct PairRequestEntry(string HashedCid, string MessageTemplate, DateTime ReceivedAt);
public readonly record struct PairRequestDisplay(string HashedCid, string DisplayName, string Message, DateTime ReceivedAt); public readonly record struct PairRequestDisplay(string HashedCid, string DisplayName, string Message, DateTime ReceivedAt);

View File

@@ -504,7 +504,7 @@ public class ServerConfigurationManager
internal void RenameTag(Dictionary<string, List<string>> tags, HashSet<string> storage, string oldName, string newName) internal void RenameTag(Dictionary<string, List<string>> tags, HashSet<string> storage, string oldName, string newName)
{ {
if (newName.Length < _maxCharactersFolder) if (newName.Length > _maxCharactersFolder)
{ {
storage.Remove(oldName); storage.Remove(oldName);
storage.Add(newName); storage.Add(newName);
@@ -607,9 +607,8 @@ public class ServerConfigurationManager
{ {
var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://"); var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
var oauthCheckUri = LightlessAuth.GetUIDsFullPath(new Uri(baseUri)); var oauthCheckUri = LightlessAuth.GetUIDsFullPath(new Uri(baseUri));
using var request = new HttpRequestMessage(HttpMethod.Get, oauthCheckUri); _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var response = await _httpClient.GetAsync(oauthCheckUri).ConfigureAwait(false);
using var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(responseStream).ConfigureAwait(false) ?? []; return await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(responseStream).ConfigureAwait(false) ?? [];
} }

View File

@@ -1,10 +1,9 @@
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.UI; using LightlessSync.UI;
using LightlessSync.UI.Style;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace LightlessSync.Services; namespace LightlessSync.Services;
@@ -23,8 +22,7 @@ public sealed class UiService : DisposableMediatorSubscriberBase
LightlessConfigService lightlessConfigService, WindowSystem windowSystem, LightlessConfigService lightlessConfigService, WindowSystem windowSystem,
IEnumerable<WindowMediatorSubscriberBase> windows, IEnumerable<WindowMediatorSubscriberBase> windows,
UiFactory uiFactory, FileDialogManager fileDialogManager, UiFactory uiFactory, FileDialogManager fileDialogManager,
LightlessMediator lightlessMediator, LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
NotificationService notificationService) : base(logger, lightlessMediator)
{ {
_logger = logger; _logger = logger;
_logger.LogTrace("Creating {type}", GetType().Name); _logger.LogTrace("Creating {type}", GetType().Name);
@@ -121,15 +119,7 @@ public sealed class UiService : DisposableMediatorSubscriberBase
private void Draw() private void Draw()
{ {
MainStyle.PushStyle(); _windowSystem.Draw();
try _fileDialogManager.Draw();
{
_windowSystem.Draw();
_fileDialogManager.Draw();
}
finally
{
MainStyle.PopStyle();
}
} }
} }

View File

@@ -1,5 +1,4 @@
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Utility; using Dalamud.Utility;
@@ -50,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()
@@ -120,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()
@@ -210,7 +211,7 @@ namespace LightlessSync.UI
else else
{ {
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.Text("The Lightfinders light wanes, but not in vain."); // cringe.. ImGui.Text("The Lightfinder<EFBFBD>s light wanes, but not in vain."); // cringe..
ImGui.PopStyleColor(); ImGui.PopStyleColor();
} }
} }
@@ -253,27 +254,12 @@ namespace LightlessSync.UI
_broadcastService.ToggleBroadcast(); _broadcastService.ToggleBroadcast();
} }
var toggleButtonHeight = ImGui.GetItemRectSize().Y;
if (isOnCooldown || !_broadcastService.IsLightFinderAvailable) if (isOnCooldown || !_broadcastService.IsLightFinderAvailable)
ImGui.EndDisabled(); ImGui.EndDisabled();
ImGui.PopStyleColor(); ImGui.PopStyleColor();
ImGui.PopStyleVar(); ImGui.PopStyleVar();
ImGui.SameLine();
if (_uiSharedService.IconButton(FontAwesomeIcon.Cog, toggleButtonHeight))
{
Mediator.Publish(new OpenLightfinderSettingsMessage());
}
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.TextUnformatted("Open Lightfinder settings.");
ImGui.EndTooltip();
}
ImGui.EndTabItem(); ImGui.EndTabItem();
} }
@@ -282,7 +268,6 @@ namespace LightlessSync.UI
if (_allSyncshells == null) if (_allSyncshells == null)
{ {
ImGui.Text("Loading Syncshells..."); ImGui.Text("Loading Syncshells...");
_ = RefreshSyncshells();
return; return;
} }
@@ -334,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;
@@ -87,7 +86,7 @@ public class CompactUi : WindowMediatorSubscriberBase
IpcManager ipcManager, IpcManager ipcManager,
BroadcastService broadcastService, BroadcastService broadcastService,
CharacterAnalyzer characterAnalyzer, CharacterAnalyzer characterAnalyzer,
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
{ {
_uiSharedService = uiShared; _uiSharedService = uiShared;
_configService = configService; _configService = configService;
@@ -105,7 +104,7 @@ public class CompactUi : WindowMediatorSubscriberBase
_renamePairTagUi = renameTagUi; _renamePairTagUi = renameTagUi;
_ipcManager = ipcManager; _ipcManager = ipcManager;
_broadcastService = broadcastService; _broadcastService = broadcastService;
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService); _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService);
AllowPinning = true; AllowPinning = true;
AllowClickthrough = false; AllowClickthrough = false;
@@ -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,7 +1,6 @@
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
@@ -20,22 +19,16 @@ public class DownloadUi : WindowMediatorSubscriberBase
private readonly DalamudUtilService _dalamudUtilService; private readonly DalamudUtilService _dalamudUtilService;
private readonly FileUploadManager _fileTransferManager; private readonly FileUploadManager _fileTransferManager;
private readonly UiSharedService _uiShared; private readonly UiSharedService _uiShared;
private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new(); private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
private readonly NotificationService _notificationService;
private bool _notificationDismissed = true;
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, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService)
PerformanceCollectorService performanceCollectorService, NotificationService notificationService)
: 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;
_notificationService = notificationService;
SizeConstraints = new WindowSizeConstraints() SizeConstraints = new WindowSizeConstraints()
{ {
@@ -60,14 +53,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
IsOpen = true; IsOpen = true;
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
{
_currentDownloads.TryRemove(msg.DownloadId, out _);
if (!_currentDownloads.Any())
{
_notificationService.DismissPairDownloadNotification();
}
});
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false); Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = true); Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = true);
Mediator.Subscribe<PlayerUploadingMessage>(this, (msg) => Mediator.Subscribe<PlayerUploadingMessage>(this, (msg) =>
@@ -87,13 +73,11 @@ public class DownloadUi : WindowMediatorSubscriberBase
{ {
if (_configService.Current.ShowTransferWindow) if (_configService.Current.ShowTransferWindow)
{ {
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();
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);
@@ -121,64 +105,28 @@ public class DownloadUi : WindowMediatorSubscriberBase
try try
{ {
// Check if download notifications are enabled (not set to TextOverlay) foreach (var item in _currentDownloads.ToList())
var useNotifications = _configService.Current.UseLightlessNotifications
? _configService.Current.LightlessDownloadNotification != NotificationLocation.TextOverlay
: _configService.Current.UseNotificationsForDownloads;
if (useNotifications)
{ {
// Use notification system var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
if (_currentDownloads.Any()) var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
{ var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
UpdateDownloadNotification(limiterSnapshot); var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
_notificationDismissed = false; var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
} var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
else if (!_notificationDismissed) var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
{ var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
_notificationService.DismissPairDownloadNotification();
_notificationDismissed = true;
}
}
else
{
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();
}
foreach (var item in _currentDownloads.ToList())
{
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.SameLine(); ImGui.SameLine();
var xDistance = ImGui.GetCursorPosX(); var xDistance = ImGui.GetCursorPosX();
UiSharedService.DrawOutlinedFont( UiSharedService.DrawOutlinedFont(
$"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]", $"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine(); ImGui.NewLine();
ImGui.SameLine(xDistance); ImGui.SameLine(xDistance);
UiSharedService.DrawOutlinedFont( UiSharedService.DrawOutlinedFont(
$"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})", $"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
}
} }
} }
catch catch
@@ -266,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;
} }
@@ -297,40 +245,4 @@ public class DownloadUi : WindowMediatorSubscriberBase
MaximumSize = new Vector2(300, maxHeight), MaximumSize = new Vector2(300, maxHeight),
}; };
} }
private void UpdateDownloadNotification(PairProcessingLimiterSnapshot limiterSnapshot)
{
var downloadStatus = new List<(string playerName, float progress, string status)>();
foreach (var item in _currentDownloads.ToList())
{
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
string status;
if (dlDecomp > 0) status = "decompressing";
else if (dlProg > 0) status = "downloading";
else if (dlQueue > 0) status = "queued";
else if (dlSlot > 0) status = "waiting";
else status = "completed";
downloadStatus.Add((item.Key.Name, progress, status));
}
// Pass queue waiting count separately, show notification if there are downloads or queue items
var queueWaiting = limiterSnapshot.IsEnabled ? limiterSnapshot.Waiting : 0;
if (downloadStatus.Any() || queueWaiting > 0)
{
_notificationService.ShowPairDownloadNotification(downloadStatus, queueWaiting);
}
}
} }

View File

@@ -1,92 +1,56 @@
using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Gui.Dtr;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Configurations; using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.PlayerData.Pairs; using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration; using LightlessSync.Services.ServerConfiguration;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using LightlessSync.WebAPI.SignalR.Utils; using LightlessSync.WebAPI.SignalR.Utils;
using LightlessSync.Utils;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
namespace LightlessSync.UI; namespace LightlessSync.UI;
public sealed class DtrEntry : IDisposable, IHostedService public sealed class DtrEntry : IDisposable, IHostedService
{ {
private static readonly TimeSpan _localHashedCidCacheDuration = TimeSpan.FromMinutes(2);
private static readonly TimeSpan _localHashedCidErrorCooldown = TimeSpan.FromMinutes(1);
private readonly ApiController _apiController; private readonly ApiController _apiController;
private readonly ServerConfigurationManager _serverManager; private readonly ServerConfigurationManager _serverManager;
private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly ConfigurationServiceBase<LightlessConfig> _configService; private readonly ConfigurationServiceBase<LightlessConfig> _configService;
private readonly IDtrBar _dtrBar; private readonly IDtrBar _dtrBar;
private readonly Lazy<IDtrBarEntry> _statusEntry; private readonly Lazy<IDtrBarEntry> _entry;
private readonly Lazy<IDtrBarEntry> _lightfinderEntry;
private readonly ILogger<DtrEntry> _logger; private readonly ILogger<DtrEntry> _logger;
private readonly BroadcastService _broadcastService;
private readonly BroadcastScannerService _broadcastScannerService;
private readonly LightlessMediator _lightlessMediator; private readonly LightlessMediator _lightlessMediator;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly PairRequestService _pairRequestService;
private readonly DalamudUtilService _dalamudUtilService;
private Task? _runTask; private Task? _runTask;
private string? _statusText; private string? _text;
private string? _statusTooltip; private string? _tooltip;
private Colors _statusColors; private Colors _colors;
private string? _lightfinderText;
private string? _lightfinderTooltip;
private Colors _lightfinderColors;
private string? _localHashedCid;
private DateTime _localHashedCidFetchedAt = DateTime.MinValue;
private DateTime _localHashedCidNextErrorLog = DateTime.MinValue;
private DateTime _pairRequestNextErrorLog = DateTime.MinValue;
public DtrEntry( public DtrEntry(ILogger<DtrEntry> logger, IDtrBar dtrBar, ConfigurationServiceBase<LightlessConfig> configService, LightlessMediator lightlessMediator, PairManager pairManager, ApiController apiController, ServerConfigurationManager serverManager)
ILogger<DtrEntry> logger,
IDtrBar dtrBar,
ConfigurationServiceBase<LightlessConfig> configService,
LightlessMediator lightlessMediator,
PairManager pairManager,
PairRequestService pairRequestService,
ApiController apiController,
ServerConfigurationManager serverManager,
BroadcastService broadcastService,
BroadcastScannerService broadcastScannerService,
DalamudUtilService dalamudUtilService)
{ {
_logger = logger; _logger = logger;
_dtrBar = dtrBar; _dtrBar = dtrBar;
_statusEntry = new(CreateStatusEntry); _entry = new(CreateEntry);
_lightfinderEntry = new(CreateLightfinderEntry);
_configService = configService; _configService = configService;
_lightlessMediator = lightlessMediator; _lightlessMediator = lightlessMediator;
_pairManager = pairManager; _pairManager = pairManager;
_pairRequestService = pairRequestService;
_apiController = apiController; _apiController = apiController;
_serverManager = serverManager; _serverManager = serverManager;
_broadcastService = broadcastService;
_broadcastScannerService = broadcastScannerService;
_dalamudUtilService = dalamudUtilService;
} }
public void Dispose() public void Dispose()
{ {
if (_statusEntry.IsValueCreated) if (_entry.IsValueCreated)
{ {
_logger.LogDebug("Disposing DtrEntry"); _logger.LogDebug("Disposing DtrEntry");
Clear(); Clear();
_statusEntry.Value.Remove(); _entry.Value.Remove();
} }
if (_lightfinderEntry.IsValueCreated)
_lightfinderEntry.Value.Remove();
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
@@ -106,7 +70,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// ignore cancelled
} }
finally finally
{ {
@@ -116,66 +80,33 @@ public sealed class DtrEntry : IDisposable, IHostedService
private void Clear() private void Clear()
{ {
HideStatusEntry(); if (!_entry.IsValueCreated) return;
HideLightfinderEntry(); _logger.LogInformation("Clearing entry");
_text = null;
_tooltip = null;
_colors = default;
_entry.Value.Shown = false;
} }
private void HideStatusEntry() private IDtrBarEntry CreateEntry()
{ {
if (_statusEntry.IsValueCreated && _statusEntry.Value.Shown) _logger.LogTrace("Creating new DtrBar entry");
{
_logger.LogInformation("Hiding status entry");
_statusEntry.Value.Shown = false;
}
_statusText = null;
_statusTooltip = null;
_statusColors = default;
}
private void HideLightfinderEntry()
{
if (_lightfinderEntry.IsValueCreated && _lightfinderEntry.Value.Shown)
{
_logger.LogInformation("Hiding Lightfinder entry");
_lightfinderEntry.Value.Shown = false;
}
_lightfinderText = null;
_lightfinderTooltip = null;
_lightfinderColors = default;
}
private IDtrBarEntry CreateStatusEntry()
{
_logger.LogTrace("Creating status DtrBar entry");
var entry = _dtrBar.Get("Lightless Sync"); var entry = _dtrBar.Get("Lightless Sync");
entry.OnClick = interactionEvent => OnStatusEntryClick(interactionEvent); entry.OnClick = interactionEvent => OnClickEvent(interactionEvent);
return entry; return entry;
} }
private IDtrBarEntry CreateLightfinderEntry()
{
_logger.LogTrace("Creating Lightfinder DtrBar entry");
var entry = _dtrBar.Get("Lightfinder");
entry.OnClick = interactionEvent => OnLightfinderEntryClick(interactionEvent);
return entry;
}
private void OnStatusEntryClick(DtrInteractionEvent interactionEvent) private void OnClickEvent(DtrInteractionEvent interactionEvent)
{ {
if (interactionEvent.ClickType.Equals(MouseClickType.Left)) if (interactionEvent.ClickType.Equals(MouseClickType.Left) && !interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift))
{ {
if (interactionEvent.ModifierKeys.HasFlag(ClickModifierKeys.Shift)) _lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
{ }
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); else if (interactionEvent.ClickType.Equals(MouseClickType.Left) && interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift))
} {
else _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
}
return;
} }
if (interactionEvent.ClickType.Equals(MouseClickType.Right)) if (interactionEvent.ClickType.Equals(MouseClickType.Right))
@@ -200,17 +131,6 @@ public sealed class DtrEntry : IDisposable, IHostedService
} }
} }
private void OnLightfinderEntryClick(DtrInteractionEvent interactionEvent)
{
if (!_configService.Current.ShowLightfinderInDtr)
return;
if (interactionEvent.ClickType.Equals(MouseClickType.Left))
{
_broadcastService.ToggleBroadcast();
}
}
private async Task RunAsync() private async Task RunAsync()
{ {
while (!_cancellationTokenSource.IsCancellationRequested) while (!_cancellationTokenSource.IsCancellationRequested)
@@ -223,278 +143,96 @@ public sealed class DtrEntry : IDisposable, IHostedService
private void Update() private void Update()
{ {
var config = _configService.Current; if (!_configService.Current.EnableDtrEntry || !_configService.Current.HasValidSetup())
if (!config.HasValidSetup())
{ {
HideStatusEntry(); if (_entry.IsValueCreated && _entry.Value.Shown)
HideLightfinderEntry(); {
_logger.LogInformation("Disabling entry");
Clear();
}
return; return;
} }
if (config.EnableDtrEntry) if (!_entry.Value.Shown)
UpdateStatusEntry(config); {
else _logger.LogInformation("Showing entry");
HideStatusEntry(); _entry.Value.Shown = true;
}
if (config.ShowLightfinderInDtr)
UpdateLightfinderEntry(config);
else
HideLightfinderEntry();
}
private void UpdateStatusEntry(LightlessConfig config)
{
string text; string text;
string tooltip; string tooltip;
Colors colors; Colors colors;
if (_apiController.IsConnected) if (_apiController.IsConnected)
{ {
var pairCount = _pairManager.GetVisibleUserCount(); var pairCount = _pairManager.GetVisibleUserCount();
text = $"\uE044 {pairCount}"; text = $"\uE044 {pairCount}";
if (pairCount > 0) if (pairCount > 0)
{ {
var preferNote = config.PreferNoteInDtrTooltip; IEnumerable<string> visiblePairs;
var showUid = config.ShowUidInDtrTooltip; if (_configService.Current.ShowUidInDtrTooltip)
{
var visiblePairsQuery = _pairManager.GetOnlineUserPairs() visiblePairs = _pairManager.GetOnlineUserPairs()
.Where(x => x.IsVisible); .Where(x => x.IsVisible)
.Select(x => string.Format("{0} ({1})", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID));
IEnumerable<string> visiblePairs = showUid }
? visiblePairsQuery.Select(x => string.Format("{0} ({1})", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID)) else
: visiblePairsQuery.Select(x => string.Format("{0}", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName)); {
visiblePairs = _pairManager.GetOnlineUserPairs()
.Where(x => x.IsVisible)
.Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName));
}
tooltip = $"Lightless Sync: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}"; tooltip = $"Lightless Sync: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}";
colors = config.DtrColorsPairsInRange; colors = _configService.Current.DtrColorsPairsInRange;
} }
else else
{ {
tooltip = "Lightless Sync: Connected"; tooltip = "Lightless Sync: Connected";
colors = config.DtrColorsDefault; colors = _configService.Current.DtrColorsDefault;
} }
} }
else else
{ {
text = "\uE044 \uE04C"; text = "\uE044 \uE04C";
tooltip = "Lightless Sync: Not Connected"; tooltip = "Lightless Sync: Not Connected";
colors = config.DtrColorsNotConnected; colors = _configService.Current.DtrColorsNotConnected;
} }
if (!config.UseColorsInDtr) if (!_configService.Current.UseColorsInDtr)
colors = default; colors = default;
var statusEntry = _statusEntry.Value; if (!string.Equals(text, _text, StringComparison.Ordinal) || !string.Equals(tooltip, _tooltip, StringComparison.Ordinal) || colors != _colors)
if (!statusEntry.Shown)
{ {
_logger.LogInformation("Showing status entry"); _text = text;
statusEntry.Shown = true; _tooltip = tooltip;
_colors = colors;
_entry.Value.Text = BuildColoredSeString(text, colors);
_entry.Value.Tooltip = tooltip;
} }
bool statusNeedsUpdate =
!string.Equals(text, _statusText, StringComparison.Ordinal) ||
!string.Equals(tooltip, _statusTooltip, StringComparison.Ordinal) ||
colors != _statusColors;
if (statusNeedsUpdate)
{
statusEntry.Text = BuildColoredSeString(text, colors);
statusEntry.Tooltip = tooltip;
_statusText = text;
_statusTooltip = tooltip;
_statusColors = colors;
}
}
private void UpdateLightfinderEntry(LightlessConfig config)
{
var lightfinderEntry = _lightfinderEntry.Value;
if (!lightfinderEntry.Shown)
{
_logger.LogInformation("Showing Lightfinder entry");
lightfinderEntry.Shown = true;
}
var indicator = BuildLightfinderIndicator();
var lightfinderText = indicator.Text ?? string.Empty;
var lightfinderColors = config.UseLightfinderColorsInDtr ? indicator.Colors : default;
var lightfinderTooltip = BuildLightfinderTooltip(indicator.Tooltip);
bool lightfinderNeedsUpdate =
!string.Equals(lightfinderText, _lightfinderText, StringComparison.Ordinal) ||
!string.Equals(lightfinderTooltip, _lightfinderTooltip, StringComparison.Ordinal) ||
lightfinderColors != _lightfinderColors;
if (lightfinderNeedsUpdate)
{
lightfinderEntry.Text = BuildColoredSeString(lightfinderText, lightfinderColors);
lightfinderEntry.Tooltip = lightfinderTooltip;
_lightfinderText = lightfinderText;
_lightfinderTooltip = lightfinderTooltip;
_lightfinderColors = lightfinderColors;
}
}
private string? GetLocalHashedCid()
{
var now = DateTime.UtcNow;
if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration)
return _localHashedCid;
try
{
var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult();
var hashedCid = cid.ToString().GetHash256();
_localHashedCid = hashedCid;
_localHashedCidFetchedAt = now;
return hashedCid;
}
catch (Exception ex)
{
if (now >= _localHashedCidNextErrorLog)
{
_logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry.");
_localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown;
}
_localHashedCid = null;
_localHashedCidFetchedAt = now;
return null;
}
}
private int GetNearbyBroadcastCount()
{
var localHashedCid = GetLocalHashedCid();
return _broadcastScannerService.CountActiveBroadcasts(
string.IsNullOrEmpty(localHashedCid) ? null : localHashedCid);
}
private int GetPendingPairRequestCount()
{
try
{
return _pairRequestService.GetActiveRequests().Count;
}
catch (Exception ex)
{
var now = DateTime.UtcNow;
if (now >= _pairRequestNextErrorLog)
{
_logger.LogDebug(ex, "Failed to retrieve pair request count for Lightfinder DTR entry.");
_pairRequestNextErrorLog = now + _localHashedCidErrorCooldown;
}
return 0;
}
}
private (string Text, Colors Colors, string Tooltip) BuildLightfinderIndicator()
{
var config = _configService.Current;
const string icon = "\uE048";
if (!_broadcastService.IsLightFinderAvailable)
{
return ($"{icon} --", SwapColorChannels(config.DtrColorsLightfinderUnavailable), "Lightfinder - Unavailable on this server.");
}
if (_broadcastService.IsBroadcasting)
{
var tooltipBuilder = new StringBuilder("Lightfinder - Enabled");
switch (config.LightfinderDtrDisplayMode)
{
case LightfinderDtrDisplayMode.PendingPairRequests:
{
var requestCount = GetPendingPairRequestCount();
tooltipBuilder.AppendLine();
tooltipBuilder.Append("Pending pair requests: ").Append(requestCount);
return ($"{icon} Requests {requestCount}", SwapColorChannels(config.DtrColorsLightfinderEnabled), tooltipBuilder.ToString());
}
default:
{
var broadcastCount = GetNearbyBroadcastCount();
tooltipBuilder.AppendLine();
tooltipBuilder.Append("Nearby Lightfinder users: ").Append(broadcastCount);
return ($"{icon} {broadcastCount}", SwapColorChannels(config.DtrColorsLightfinderEnabled), tooltipBuilder.ToString());
}
}
}
var tooltip = new StringBuilder("Lightfinder - Disabled");
var colors = SwapColorChannels(config.DtrColorsLightfinderDisabled);
if (_broadcastService.RemainingCooldown is { } cooldown && cooldown > TimeSpan.Zero)
{
tooltip.AppendLine();
tooltip.Append("Cooldown: ").Append(Math.Ceiling(cooldown.TotalSeconds)).Append("s");
colors = SwapColorChannels(config.DtrColorsLightfinderCooldown);
}
return ($"{icon} OFF", colors, tooltip.ToString());
}
private static string BuildLightfinderTooltip(string baseTooltip)
{
var builder = new StringBuilder();
if (!string.IsNullOrWhiteSpace(baseTooltip))
builder.Append(baseTooltip.TrimEnd());
else
builder.Append("Lightfinder status unavailable.");
return builder.ToString().TrimEnd();
}
private static void AppendColoredSegment(SeStringBuilder builder, string? text, Colors colors)
{
if (string.IsNullOrEmpty(text))
return;
if (colors.Foreground != default)
builder.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground));
if (colors.Glow != default)
builder.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow));
builder.AddText(text);
if (colors.Glow != default)
builder.Add(BuildColorEndPayload(_colorTypeGlow));
if (colors.Foreground != default)
builder.Add(BuildColorEndPayload(_colorTypeForeground));
} }
#region Colored SeString #region Colored SeString
private const byte _colorTypeForeground = 0x13; private const byte _colorTypeForeground = 0x13;
private const byte _colorTypeGlow = 0x14; private const byte _colorTypeGlow = 0x14;
private static Colors SwapColorChannels(Colors colors)
=> new(SwapColorComponent(colors.Foreground), SwapColorComponent(colors.Glow));
private static uint SwapColorComponent(uint color)
{
if (color == 0)
return 0;
return ((color & 0xFFu) << 16) | (color & 0xFF00u) | ((color >> 16) & 0xFFu);
}
private static SeString BuildColoredSeString(string text, Colors colors) private static SeString BuildColoredSeString(string text, Colors colors)
{ {
var ssb = new SeStringBuilder(); var ssb = new SeStringBuilder();
AppendColoredSegment(ssb, text, colors); if (colors.Foreground != default)
ssb.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground));
if (colors.Glow != default)
ssb.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow));
ssb.AddText(text);
if (colors.Glow != default)
ssb.Add(BuildColorEndPayload(_colorTypeGlow));
if (colors.Foreground != default)
ssb.Add(BuildColorEndPayload(_colorTypeForeground));
return ssb.Build(); return ssb.Build();
} }
private static RawPayload BuildColorStartPayload(byte colorType, uint color) private static RawPayload BuildColorStartPayload(byte colorType, uint color)
=> new(unchecked([ => new(unchecked([0x02, colorType, 0x05, 0xF6, byte.Max((byte)color, 0x01), byte.Max((byte)(color >> 8), 0x01), byte.Max((byte)(color >> 16), 0x01), 0x03]));
0x02,
colorType,
0x05,
0xF6,
byte.Max((byte)color, (byte)0x01),
byte.Max((byte)(color >> 8), (byte)0x01),
byte.Max((byte)(color >> 16), (byte)0x01),
0x03
]));
private static RawPayload BuildColorEndPayload(byte colorType) private static RawPayload BuildColorEndPayload(byte colorType)
=> new([0x02, colorType, 0x02, 0xEC, 0x03]); => new([0x02, colorType, 0x02, 0xEC, 0x03]);

View File

@@ -1,596 +0,0 @@
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Models;
using Microsoft.Extensions.Logging;
using System.Numerics;
using Dalamud.Bindings.ImGui;
namespace LightlessSync.UI;
public class LightlessNotificationUI : WindowMediatorSubscriberBase
{
private const float NotificationMinHeight = 60f;
private const float NotificationMaxHeight = 250f;
private const float WindowPaddingOffset = 6f;
private const float SlideAnimationDistance = 100f;
private const float OutAnimationSpeedMultiplier = 0.7f;
private readonly List<LightlessNotification> _notifications = new();
private readonly object _notificationLock = new();
private readonly LightlessConfigService _configService;
public LightlessNotificationUI(ILogger<LightlessNotificationUI> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService)
: base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector)
{
_configService = configService;
Flags = ImGuiWindowFlags.NoDecoration |
ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoResize |
ImGuiWindowFlags.NoSavedSettings |
ImGuiWindowFlags.NoFocusOnAppearing |
ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoBackground |
ImGuiWindowFlags.NoCollapse |
ImGuiWindowFlags.AlwaysAutoResize;
PositionCondition = ImGuiCond.Always;
SizeCondition = ImGuiCond.FirstUseEver;
IsOpen = false;
RespectCloseHotkey = false;
DisableWindowSounds = true;
Mediator.Subscribe<LightlessNotificationMessage>(this, HandleNotificationMessage);
Mediator.Subscribe<LightlessNotificationDismissMessage>(this, HandleNotificationDismissMessage);
}
private void HandleNotificationMessage(LightlessNotificationMessage message) =>
AddNotification(message.Notification);
private void HandleNotificationDismissMessage(LightlessNotificationDismissMessage message) =>
RemoveNotification(message.NotificationId);
public void AddNotification(LightlessNotification notification)
{
lock (_notificationLock)
{
var existingNotification = _notifications.FirstOrDefault(n => n.Id == notification.Id);
if (existingNotification != null)
{
UpdateExistingNotification(existingNotification, notification);
}
else
{
_notifications.Add(notification);
_logger.LogDebug("Added new notification: {Title}", notification.Title);
}
if (!IsOpen) IsOpen = true;
}
}
private void UpdateExistingNotification(LightlessNotification existing, LightlessNotification updated)
{
existing.Message = updated.Message;
existing.Progress = updated.Progress;
existing.ShowProgress = updated.ShowProgress;
existing.Title = updated.Title;
_logger.LogDebug("Updated existing notification: {Title}", updated.Title);
}
public void RemoveNotification(string id)
{
lock (_notificationLock)
{
var notification = _notifications.FirstOrDefault(n => n.Id == id);
if (notification != null)
{
StartOutAnimation(notification);
}
}
}
private void StartOutAnimation(LightlessNotification notification)
{
notification.IsAnimatingOut = true;
notification.IsAnimatingIn = false;
}
protected override void DrawInternal()
{
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
lock (_notificationLock)
{
UpdateNotifications();
if (_notifications.Count == 0)
{
ImGui.PopStyleVar();
IsOpen = false;
return;
}
var viewport = ImGui.GetMainViewport();
Position = CalculateWindowPosition(viewport);
DrawAllNotifications();
}
ImGui.PopStyleVar();
}
private Vector2 CalculateWindowPosition(ImGuiViewportPtr viewport)
{
var x = viewport.WorkPos.X + viewport.WorkSize.X -
_configService.Current.NotificationWidth -
_configService.Current.NotificationOffsetX -
WindowPaddingOffset;
var y = viewport.WorkPos.Y + _configService.Current.NotificationOffsetY;
return new Vector2(x, y);
}
private void DrawAllNotifications()
{
for (int i = 0; i < _notifications.Count; i++)
{
DrawNotification(_notifications[i], i);
if (i < _notifications.Count - 1)
{
ImGui.Dummy(new Vector2(0, _configService.Current.NotificationSpacing));
}
}
}
private void UpdateNotifications()
{
var deltaTime = ImGui.GetIO().DeltaTime;
EnforceMaxNotificationLimit();
UpdateAnimationsAndRemoveExpired(deltaTime);
}
private void EnforceMaxNotificationLimit()
{
var maxNotifications = _configService.Current.MaxSimultaneousNotifications;
while (_notifications.Count(n => !n.IsAnimatingOut) > maxNotifications)
{
var oldestNotification = _notifications
.Where(n => !n.IsAnimatingOut)
.OrderBy(n => n.CreatedAt)
.FirstOrDefault();
if (oldestNotification != null)
{
StartOutAnimation(oldestNotification);
}
}
}
private void UpdateAnimationsAndRemoveExpired(float deltaTime)
{
for (int i = _notifications.Count - 1; i >= 0; i--)
{
var notification = _notifications[i];
UpdateNotificationAnimation(notification, deltaTime);
if (ShouldRemoveNotification(notification))
{
_notifications.RemoveAt(i);
}
}
}
private void UpdateNotificationAnimation(LightlessNotification notification, float deltaTime)
{
if (notification.IsAnimatingIn && notification.AnimationProgress < 1f)
{
notification.AnimationProgress = Math.Min(1f,
notification.AnimationProgress + deltaTime * _configService.Current.NotificationAnimationSpeed);
}
else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f)
{
notification.AnimationProgress = Math.Max(0f,
notification.AnimationProgress - deltaTime * _configService.Current.NotificationAnimationSpeed * OutAnimationSpeedMultiplier);
}
else if (!notification.IsAnimatingOut && !notification.IsDismissed)
{
notification.IsAnimatingIn = false;
if (notification.IsExpired)
{
StartOutAnimation(notification);
}
}
}
private bool ShouldRemoveNotification(LightlessNotification notification) =>
notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f;
private void DrawNotification(LightlessNotification notification, int index)
{
var alpha = notification.AnimationProgress;
if (alpha <= 0f) return;
var slideOffset = (1f - alpha) * SlideAnimationDistance;
var originalCursorPos = ImGui.GetCursorPos();
ImGui.SetCursorPosX(originalCursorPos.X + slideOffset);
var notificationHeight = CalculateNotificationHeight(notification);
var notificationWidth = _configService.Current.NotificationWidth - slideOffset;
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
using var child = ImRaii.Child($"notification_{notification.Id}",
new Vector2(notificationWidth, notificationHeight),
false, ImGuiWindowFlags.NoScrollbar);
if (child.Success)
{
DrawNotificationContent(notification, alpha);
}
ImGui.PopStyleVar();
}
private void DrawNotificationContent(LightlessNotification notification, float alpha)
{
var drawList = ImGui.GetWindowDrawList();
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
var bgColor = CalculateBackgroundColor(alpha, ImGui.IsWindowHovered());
var accentColor = GetNotificationAccentColor(notification.Type);
accentColor.W *= alpha;
DrawShadow(drawList, windowPos, windowSize, alpha);
HandleClickToDismiss(notification);
DrawBackground(drawList, windowPos, windowSize, bgColor);
DrawAccentBar(drawList, windowPos, windowSize, accentColor);
DrawDurationProgressBar(notification, alpha, windowPos, windowSize, drawList);
DrawNotificationText(notification, alpha);
}
private Vector4 CalculateBackgroundColor(float alpha, bool isHovered)
{
var baseOpacity = _configService.Current.NotificationOpacity;
var finalOpacity = baseOpacity * alpha;
var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, finalOpacity);
if (isHovered)
{
bgColor *= 1.1f;
bgColor.W = Math.Min(bgColor.W, 0.98f);
}
return bgColor;
}
private void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha)
{
var shadowOffset = new Vector2(1f, 1f);
var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha);
drawList.AddRectFilled(
windowPos + shadowOffset,
windowPos + windowSize + shadowOffset,
ImGui.ColorConvertFloat4ToU32(shadowColor),
3f
);
}
private void HandleClickToDismiss(LightlessNotification notification)
{
if (ImGui.IsWindowHovered() &&
_configService.Current.DismissNotificationOnClick &&
!notification.Actions.Any() &&
ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
notification.IsDismissed = true;
StartOutAnimation(notification);
}
}
private void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor)
{
drawList.AddRectFilled(
windowPos,
windowPos + windowSize,
ImGui.ColorConvertFloat4ToU32(bgColor),
3f
);
}
private void DrawAccentBar(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 accentColor)
{
var accentWidth = _configService.Current.NotificationAccentBarWidth;
if (accentWidth > 0f)
{
drawList.AddRectFilled(
windowPos,
windowPos + new Vector2(accentWidth, windowSize.Y),
ImGui.ColorConvertFloat4ToU32(accentColor),
3f
);
}
}
private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList)
{
var progress = CalculateProgress(notification);
var progressBarColor = UIColors.Get("LightlessBlue");
var progressHeight = 2f;
var progressY = windowPos.Y + windowSize.Y - progressHeight;
var progressWidth = windowSize.X * progress;
DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha);
if (progress > 0)
{
DrawProgressForeground(drawList, windowPos, progressY, progressHeight, progressWidth, progressBarColor, alpha);
}
}
private float CalculateProgress(LightlessNotification notification)
{
if (notification.Type == NotificationType.Download && notification.ShowProgress)
{
return Math.Clamp(notification.Progress, 0f, 1f);
}
var elapsed = DateTime.UtcNow - notification.CreatedAt;
return Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds));
}
private void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha)
{
var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha);
drawList.AddRectFilled(
new Vector2(windowPos.X, progressY),
new Vector2(windowPos.X + windowSize.X, progressY + progressHeight),
ImGui.ColorConvertFloat4ToU32(bgProgressColor),
0f
);
}
private void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha)
{
var progressColor = progressBarColor;
progressColor.W *= alpha;
drawList.AddRectFilled(
new Vector2(windowPos.X, progressY),
new Vector2(windowPos.X + progressWidth, progressY + progressHeight),
ImGui.ColorConvertFloat4ToU32(progressColor),
0f
);
}
private void DrawNotificationText(LightlessNotification notification, float alpha)
{
var padding = new Vector2(10f, 6f);
var contentPos = new Vector2(padding.X, padding.Y);
var windowSize = ImGui.GetWindowSize();
var contentSize = new Vector2(windowSize.X - padding.X, windowSize.Y - padding.Y * 2);
ImGui.SetCursorPos(contentPos);
var titleHeight = DrawTitle(notification, contentSize.X, alpha);
DrawMessage(notification, contentPos, contentSize.X, titleHeight, alpha);
if (notification.Actions.Count > 0)
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetStyle().ItemSpacing.Y);
ImGui.SetCursorPosX(contentPos.X);
DrawNotificationActions(notification, contentSize.X, alpha);
}
}
private float DrawTitle(LightlessNotification notification, float contentWidth, float alpha)
{
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha)))
{
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentWidth);
var titleStartY = ImGui.GetCursorPosY();
var titleText = _configService.Current.ShowNotificationTimestamp
? $"[{notification.CreatedAt.ToLocalTime():HH:mm:ss}] {notification.Title}"
: notification.Title;
ImGui.TextWrapped(titleText);
var titleHeight = ImGui.GetCursorPosY() - titleStartY;
ImGui.PopTextWrapPos();
return titleHeight;
}
}
private void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha)
{
if (string.IsNullOrEmpty(notification.Message)) return;
ImGui.SetCursorPos(contentPos + new Vector2(0f, titleHeight + 4f));
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentWidth);
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(0.9f, 0.9f, 0.9f, alpha)))
{
ImGui.TextWrapped(notification.Message);
}
ImGui.PopTextWrapPos();
}
private void DrawNotificationActions(LightlessNotification notification, float availableWidth, float alpha)
{
var buttonSpacing = 8f;
var rightPadding = 10f;
var usableWidth = availableWidth - rightPadding;
var totalSpacing = (notification.Actions.Count - 1) * buttonSpacing;
var buttonWidth = (usableWidth - totalSpacing) / notification.Actions.Count;
_logger.LogDebug("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}",
notification.Actions.Count, buttonWidth, availableWidth);
var startCursorPos = ImGui.GetCursorPos();
for (int i = 0; i < notification.Actions.Count; i++)
{
var action = notification.Actions[i];
if (i > 0)
{
ImGui.SameLine();
var currentX = startCursorPos.X + i * (buttonWidth + buttonSpacing);
ImGui.SetCursorPosX(currentX);
}
DrawActionButton(action, notification, alpha, buttonWidth);
}
}
private void DrawActionButton(LightlessNotificationAction action, LightlessNotification notification, float alpha, float buttonWidth)
{
_logger.LogDebug("Drawing action button: {ActionId} - {ActionLabel}, width: {ButtonWidth}", action.Id, action.Label, buttonWidth);
var buttonColor = action.Color;
buttonColor.W *= alpha;
var hoveredColor = buttonColor * 1.1f;
hoveredColor.W = buttonColor.W;
var activeColor = buttonColor * 0.9f;
activeColor.W = buttonColor.W;
using (ImRaii.PushColor(ImGuiCol.Button, buttonColor))
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, hoveredColor))
using (ImRaii.PushColor(ImGuiCol.ButtonActive, activeColor))
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha)))
{
var buttonPressed = false;
if (action.Icon != FontAwesomeIcon.None)
{
buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth, alpha);
}
else
{
buttonPressed = ImGui.Button(action.Label, new Vector2(buttonWidth, 0));
}
_logger.LogDebug("Button {ActionId} pressed: {ButtonPressed}", action.Id, buttonPressed);
if (buttonPressed)
{
try
{
_logger.LogDebug("Executing action: {ActionId}", action.Id);
action.OnClick(notification);
_logger.LogDebug("Action executed successfully: {ActionId}", action.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing notification action: {ActionId}", action.Id);
}
}
}
}
private bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width, float alpha)
{
var drawList = ImGui.GetWindowDrawList();
var cursorPos = ImGui.GetCursorScreenPos();
var frameHeight = ImGui.GetFrameHeight();
Vector2 iconSize;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
iconSize = ImGui.CalcTextSize(icon.ToIconString());
}
var textSize = ImGui.CalcTextSize(text);
var spacing = 3f * ImGuiHelpers.GlobalScale;
var totalTextWidth = iconSize.X + spacing + textSize.X;
var buttonPressed = ImGui.InvisibleButton($"btn_{icon}_{text}", new Vector2(width, frameHeight));
var buttonMin = ImGui.GetItemRectMin();
var buttonMax = ImGui.GetItemRectMax();
var buttonSize = buttonMax - buttonMin;
var buttonColor = ImGui.GetColorU32(ImGuiCol.Button);
if (ImGui.IsItemHovered())
buttonColor = ImGui.GetColorU32(ImGuiCol.ButtonHovered);
if (ImGui.IsItemActive())
buttonColor = ImGui.GetColorU32(ImGuiCol.ButtonActive);
drawList.AddRectFilled(buttonMin, buttonMax, buttonColor, 3f);
var iconPos = buttonMin + new Vector2((buttonSize.X - totalTextWidth) / 2f, (buttonSize.Y - iconSize.Y) / 2f);
var textPos = iconPos + new Vector2(iconSize.X + spacing, (iconSize.Y - textSize.Y) / 2f);
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
// Draw icon
using (ImRaii.PushFont(UiBuilder.IconFont))
{
drawList.AddText(iconPos, textColor, icon.ToIconString());
}
// Draw text
drawList.AddText(textPos, textColor, text);
return buttonPressed;
}
private float CalculateNotificationHeight(LightlessNotification notification)
{
var contentWidth = _configService.Current.NotificationWidth - 35f;
var height = 12f;
height += CalculateTitleHeight(notification, contentWidth);
height += CalculateMessageHeight(notification, contentWidth);
if (notification.ShowProgress)
{
height += 12f;
}
if (notification.Actions.Count > 0)
{
height += ImGui.GetStyle().ItemSpacing.Y;
height += ImGui.GetFrameHeight();
height += 12f;
}
return Math.Clamp(height, NotificationMinHeight, NotificationMaxHeight);
}
private float CalculateTitleHeight(LightlessNotification notification, float contentWidth)
{
var titleText = _configService.Current.ShowNotificationTimestamp
? $"[{notification.CreatedAt.ToLocalTime():HH:mm:ss}] {notification.Title}"
: notification.Title;
return ImGui.CalcTextSize(titleText, true, contentWidth).Y;
}
private float CalculateMessageHeight(LightlessNotification notification, float contentWidth)
{
if (string.IsNullOrEmpty(notification.Message)) return 0f;
var messageHeight = ImGui.CalcTextSize(notification.Message, true, contentWidth).Y;
return 4f + messageHeight;
}
private Vector4 GetNotificationAccentColor(NotificationType type)
{
return type switch
{
NotificationType.Info => UIColors.Get("LightlessPurple"),
NotificationType.Warning => UIColors.Get("LightlessYellow"),
NotificationType.Error => UIColors.Get("DimRed"),
NotificationType.PairRequest => UIColors.Get("LightlessBlue"),
NotificationType.Download => UIColors.Get("LightlessGreen"),
_ => UIColors.Get("LightlessPurple")
};
}
}

View File

@@ -1,32 +0,0 @@
using Dalamud.Interface;
using LightlessSync.LightlessConfiguration.Models;
using System.Numerics;
namespace LightlessSync.UI.Models;
public class LightlessNotification
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Title { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public NotificationType Type { get; set; } = NotificationType.Info;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(5);
public bool IsExpired => DateTime.UtcNow - CreatedAt > Duration;
public bool IsDismissed { get; set; } = false;
public List<LightlessNotificationAction> Actions { get; set; } = new();
public bool ShowProgress { get; set; } = false;
public float Progress { get; set; } = 0f;
public float AnimationProgress { get; set; } = 0f;
public bool IsAnimatingIn { get; set; } = true;
public bool IsAnimatingOut { get; set; } = false;
public uint? SoundEffectId { get; set; } = null;
}
public class LightlessNotificationAction
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Label { get; set; } = string.Empty;
public FontAwesomeIcon Icon { get; set; } = FontAwesomeIcon.None;
public Vector4 Color { get; set; } = Vector4.One;
public Action<LightlessNotification> OnClick { get; set; } = _ => { };
public bool IsPrimary { get; set; } = false;
public bool IsDestructive { get; set; } = false;
}

View File

@@ -1,72 +0,0 @@
using LightlessSync.LightlessConfiguration.Models;
namespace LightlessSync.UI.Models;
/// <summary>
/// Common FFXIV <se.#> sound effect IDs for notifications.
/// These correspond to the same sound IDs used in macros (116).
/// </summary>
public static class NotificationSounds
{
// ─────────────────────────────────────────────
// Base <se.#> IDs (116)
// https://ffxiv.consolegameswiki.com/wiki/Macros#Sound_Effects
// ─────────────────────────────────────────────
public const uint Se1 = 1; // Soft chime
public const uint Se2 = 2; // Higher chime
public const uint Se3 = 3; // Bell tone
public const uint Se4 = 4; // Harp tone
public const uint Se5 = 5; // Mechanical click
public const uint Se6 = 6; // Drum / percussion
public const uint Se7 = 7; // Metallic chime
public const uint Se8 = 8; // Wooden tone
public const uint Se9 = 9; // Wind / flute tone
public const uint Se10 = 11; // Magical sparkle (ID 10 is skipped in game)
public const uint Se11 = 12; // Metallic ring
public const uint Se12 = 13; // Deep thud
public const uint Se13 = 14; // "Tell received" ping
public const uint Se14 = 15; // Success fanfare
public const uint Se15 = 16; // System warning
// Note: Se16 doesn't exist - Se15 is the last available sound
/// <summary>
/// General notification sound (<se.2>)
/// </summary>
public const uint Info = Se2;
/// <summary>
/// Warning/alert sound (<se.15>)
/// </summary>
public const uint Warning = Se15;
/// <summary>
/// Error sound (<se.15> - System warning, used for errors)
/// </summary>
public const uint Error = Se15;
/// <summary>
/// Success sound (<se.14>)
/// </summary>
public const uint Success = Se14;
/// <summary>
/// Pair request sound (<se.13>, same as tell notification)
/// </summary>
public const uint PairRequest = Se13;
/// <summary>
/// Download complete sound (<se.10>, a clean sparkle tone)
/// </summary>
public const uint DownloadComplete = Se10;
/// <summary>
/// Get default sound for notification type
/// </summary>
public static uint GetDefaultSound(NotificationType type) => type switch
{
NotificationType.Info => Info,
NotificationType.Warning => Warning,
NotificationType.Error => Error,
_ => Info
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,231 +1,169 @@
// inspiration: brio because it's style is fucking amazing // inspiration: brio because it's style is fucking amazing
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Configurations;
using System;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
namespace LightlessSync.UI.Style; namespace LightlessSync.UI.Style
internal static class MainStyle
{ {
public readonly record struct StyleColorOption(string Key, string Label, Func<Vector4> DefaultValue, ImGuiCol Target, string? Description = null, string? UiColorKey = null); internal static class MainStyle
public readonly record struct StyleFloatOption(string Key, string Label, float DefaultValue, ImGuiStyleVar Target, float? Min = null, float? Max = null, float Speed = 0.25f, string? Description = null);
public readonly record struct StyleVector2Option(string Key, string Label, Func<Vector2> DefaultValue, ImGuiStyleVar Target, Vector2? Min = null, Vector2? Max = null, float Speed = 0.25f, string? Description = null);
private static LightlessConfigService? _config;
private static UiThemeConfigService? _themeConfig;
public static void Init(LightlessConfigService config, UiThemeConfigService themeConfig)
{ {
_config = config; private static LightlessConfigService? _config;
_themeConfig = themeConfig; public static void Init(LightlessConfigService config) => _config = config;
} public static bool ShouldUseTheme => _config?.Current.UseLightlessRedesign ?? false;
public static bool ShouldUseTheme => _config?.Current.UseLightlessRedesign ?? false;
private static bool _hasPushed; private static bool _hasPushed;
private static int _pushedColorCount; private static int _pushedColorCount;
private static int _pushedStyleVarCount; private static int _pushedStyleVarCount;
private static readonly StyleColorOption[] _colorOptions = public static void PushStyle()
[
new("color.text", "Text", () => Rgba(255, 255, 255, 255), ImGuiCol.Text),
new("color.textDisabled", "Text (Disabled)", () => Rgba(128, 128, 128, 255), ImGuiCol.TextDisabled),
new("color.windowBg", "Window Background", () => Rgba(23, 23, 23, 248), ImGuiCol.WindowBg),
new("color.childBg", "Child Background", () => Rgba(23, 23, 23, 66), ImGuiCol.ChildBg),
new("color.popupBg", "Popup Background", () => Rgba(23, 23, 23, 248), ImGuiCol.PopupBg),
new("color.border", "Border", () => Rgba(65, 65, 65, 255), ImGuiCol.Border),
new("color.borderShadow", "Border Shadow", () => Rgba(0, 0, 0, 150), ImGuiCol.BorderShadow),
new("color.frameBg", "Frame Background", () => Rgba(40, 40, 40, 255), ImGuiCol.FrameBg),
new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 255), ImGuiCol.FrameBgHovered),
new("color.frameBgActive", "Frame Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.FrameBgActive),
new("color.titleBg", "Title Background", () => Rgba(24, 24, 24, 232), ImGuiCol.TitleBg),
new("color.titleBgActive", "Title Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.TitleBgActive),
new("color.titleBgCollapsed", "Title Background (Collapsed)", () => Rgba(27, 27, 27, 255), ImGuiCol.TitleBgCollapsed),
new("color.menuBarBg", "Menu Bar Background", () => Rgba(36, 36, 36, 255), ImGuiCol.MenuBarBg),
new("color.scrollbarBg", "Scrollbar Background", () => Rgba(0, 0, 0, 0), ImGuiCol.ScrollbarBg),
new("color.scrollbarGrab", "Scrollbar Grab", () => Rgba(62, 62, 62, 255), ImGuiCol.ScrollbarGrab),
new("color.scrollbarGrabHovered", "Scrollbar Grab (Hover)", () => Rgba(70, 70, 70, 255), ImGuiCol.ScrollbarGrabHovered),
new("color.scrollbarGrabActive", "Scrollbar Grab (Active)", () => Rgba(70, 70, 70, 255), ImGuiCol.ScrollbarGrabActive),
new("color.checkMark", "Check Mark", () => UIColors.Get("LightlessPurple"), ImGuiCol.CheckMark, UiColorKey: "LightlessPurple"),
new("color.sliderGrab", "Slider Grab", () => Rgba(101, 101, 101, 255), ImGuiCol.SliderGrab),
new("color.sliderGrabActive", "Slider Grab (Active)", () => Rgba(123, 123, 123, 255), ImGuiCol.SliderGrabActive),
new("color.button", "Button", () => UIColors.Get("ButtonDefault"), ImGuiCol.Button, UiColorKey: "ButtonDefault"),
new("color.buttonHovered", "Button (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.ButtonHovered, UiColorKey: "LightlessPurple"),
new("color.buttonActive", "Button (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.ButtonActive, UiColorKey: "LightlessPurpleActive"),
new("color.header", "Header", () => Rgba(0, 0, 0, 60), ImGuiCol.Header),
new("color.headerHovered", "Header (Hover)", () => Rgba(0, 0, 0, 90), ImGuiCol.HeaderHovered),
new("color.headerActive", "Header (Active)", () => Rgba(0, 0, 0, 120), ImGuiCol.HeaderActive),
new("color.separator", "Separator", () => Rgba(75, 75, 75, 121), ImGuiCol.Separator),
new("color.separatorHovered", "Separator (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.SeparatorHovered, UiColorKey: "LightlessPurple"),
new("color.separatorActive", "Separator (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.SeparatorActive, UiColorKey: "LightlessPurpleActive"),
new("color.resizeGrip", "Resize Grip", () => Rgba(0, 0, 0, 0), ImGuiCol.ResizeGrip),
new("color.resizeGripHovered", "Resize Grip (Hover)", () => Rgba(0, 0, 0, 0), ImGuiCol.ResizeGripHovered),
new("color.resizeGripActive", "Resize Grip (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.ResizeGripActive, UiColorKey: "LightlessPurpleActive"),
new("color.tab", "Tab", () => Rgba(40, 40, 40, 255), ImGuiCol.Tab),
new("color.tabHovered", "Tab (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.TabHovered, UiColorKey: "LightlessPurple"),
new("color.tabActive", "Tab (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.TabActive, UiColorKey: "LightlessPurpleActive"),
new("color.tabUnfocused", "Tab (Unfocused)", () => Rgba(40, 40, 40, 255), ImGuiCol.TabUnfocused),
new("color.tabUnfocusedActive", "Tab (Unfocused Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.TabUnfocusedActive, UiColorKey: "LightlessPurpleActive"),
new("color.dockingPreview", "Docking Preview", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.DockingPreview, UiColorKey: "LightlessPurpleActive"),
new("color.dockingEmptyBg", "Docking Empty Background", () => Rgba(50, 50, 50, 255), ImGuiCol.DockingEmptyBg),
new("color.plotLines", "Plot Lines", () => Rgba(150, 150, 150, 255), ImGuiCol.PlotLines),
new("color.tableHeaderBg", "Table Header Background", () => Rgba(48, 48, 48, 255), ImGuiCol.TableHeaderBg),
new("color.tableBorderStrong", "Table Border Strong", () => Rgba(79, 79, 89, 255), ImGuiCol.TableBorderStrong),
new("color.tableBorderLight", "Table Border Light", () => Rgba(59, 59, 64, 255), ImGuiCol.TableBorderLight),
new("color.tableRowBg", "Table Row Background", () => Rgba(0, 0, 0, 0), ImGuiCol.TableRowBg),
new("color.tableRowBgAlt", "Table Row Background (Alt)", () => Rgba(255, 255, 255, 15), ImGuiCol.TableRowBgAlt),
new("color.textSelectedBg", "Text Selection Background", () => Rgba(173, 138, 245, 255), ImGuiCol.TextSelectedBg),
new("color.dragDropTarget", "Drag & Drop Target", () => Rgba(173, 138, 245, 255), ImGuiCol.DragDropTarget),
new("color.navHighlight", "Navigation Highlight", () => Rgba(173, 138, 245, 179), ImGuiCol.NavHighlight),
new("color.navWindowingDimBg", "Navigation Window Dim", () => Rgba(204, 204, 204, 51), ImGuiCol.NavWindowingDimBg),
new("color.navWindowingHighlight", "Navigation Window Highlight", () => Rgba(204, 204, 204, 89), ImGuiCol.NavWindowingHighlight)
];
private static readonly StyleVector2Option[] _vector2Options =
[
new("vector.windowPadding", "Window Padding", () => new Vector2(6f, 6f), ImGuiStyleVar.WindowPadding),
new("vector.framePadding", "Frame Padding", () => new Vector2(4f, 3f), ImGuiStyleVar.FramePadding),
new("vector.cellPadding", "Cell Padding", () => new Vector2(4f, 4f), ImGuiStyleVar.CellPadding),
new("vector.itemSpacing", "Item Spacing", () => new Vector2(4f, 4f), ImGuiStyleVar.ItemSpacing),
new("vector.itemInnerSpacing", "Item Inner Spacing", () => new Vector2(4f, 4f), ImGuiStyleVar.ItemInnerSpacing)
];
private static readonly StyleFloatOption[] _floatOptions =
[
new("float.indentSpacing", "Indent Spacing", 21f, ImGuiStyleVar.IndentSpacing, 0f, 100f, 0.5f),
new("float.scrollbarSize", "Scrollbar Size", 10f, ImGuiStyleVar.ScrollbarSize, 4f, 30f, 0.5f),
new("float.grabMinSize", "Grab Minimum Size", 20f, ImGuiStyleVar.GrabMinSize, 1f, 80f, 0.5f),
new("float.windowBorderSize", "Window Border Size", 1.5f, ImGuiStyleVar.WindowBorderSize, 0f, 5f, 0.1f),
new("float.childBorderSize", "Child Border Size", 1.5f, ImGuiStyleVar.ChildBorderSize, 0f, 5f, 0.1f),
new("float.popupBorderSize", "Popup Border Size", 1.5f, ImGuiStyleVar.PopupBorderSize, 0f, 5f, 0.1f),
new("float.frameBorderSize", "Frame Border Size", 0f, ImGuiStyleVar.FrameBorderSize, 0f, 5f, 0.1f),
new("float.windowRounding", "Window Rounding", 7f, ImGuiStyleVar.WindowRounding, 0f, 20f, 0.2f),
new("float.childRounding", "Child Rounding", 4f, ImGuiStyleVar.ChildRounding, 0f, 20f, 0.2f),
new("float.frameRounding", "Frame Rounding", 4f, ImGuiStyleVar.FrameRounding, 0f, 20f, 0.2f),
new("float.popupRounding", "Popup Rounding", 4f, ImGuiStyleVar.PopupRounding, 0f, 20f, 0.2f),
new("float.scrollbarRounding", "Scrollbar Rounding", 4f, ImGuiStyleVar.ScrollbarRounding, 0f, 20f, 0.2f),
new("float.grabRounding", "Grab Rounding", 4f, ImGuiStyleVar.GrabRounding, 0f, 20f, 0.2f),
new("float.tabRounding", "Tab Rounding", 4f, ImGuiStyleVar.TabRounding, 0f, 20f, 0.2f)
];
public static IReadOnlyList<StyleColorOption> ColorOptions => _colorOptions;
public static IReadOnlyList<StyleFloatOption> FloatOptions => _floatOptions;
public static IReadOnlyList<StyleVector2Option> Vector2Options => _vector2Options;
public static void PushStyle()
{
if (_hasPushed)
PopStyle();
if (!ShouldUseTheme)
{ {
if (_hasPushed)
PopStyle();
if (!ShouldUseTheme)
{
_hasPushed = false;
return;
}
_hasPushed = true;
_pushedColorCount = 0;
_pushedStyleVarCount = 0;
Push(ImGuiCol.Text, new Vector4(255, 255, 255, 255));
Push(ImGuiCol.TextDisabled, new Vector4(128, 128, 128, 255));
Push(ImGuiCol.WindowBg, new Vector4(23, 23, 23, 248));
Push(ImGuiCol.ChildBg, new Vector4(23, 23, 23, 66));
Push(ImGuiCol.PopupBg, new Vector4(23, 23, 23, 248));
Push(ImGuiCol.Border, new Vector4(65, 65, 65, 255));
Push(ImGuiCol.BorderShadow, new Vector4(0, 0, 0, 150));
Push(ImGuiCol.FrameBg, new Vector4(40, 40, 40, 255));
Push(ImGuiCol.FrameBgHovered, new Vector4(50, 50, 50, 255));
Push(ImGuiCol.FrameBgActive, new Vector4(30, 30, 30, 255));
Push(ImGuiCol.TitleBg, new Vector4(24, 24, 24, 232));
Push(ImGuiCol.TitleBgActive, new Vector4(30, 30, 30, 255));
Push(ImGuiCol.TitleBgCollapsed, new Vector4(27, 27, 27, 255));
Push(ImGuiCol.MenuBarBg, new Vector4(36, 36, 36, 255));
Push(ImGuiCol.ScrollbarBg, new Vector4(0, 0, 0, 0));
Push(ImGuiCol.ScrollbarGrab, new Vector4(62, 62, 62, 255));
Push(ImGuiCol.ScrollbarGrabHovered, new Vector4(70, 70, 70, 255));
Push(ImGuiCol.ScrollbarGrabActive, new Vector4(70, 70, 70, 255));
Push(ImGuiCol.CheckMark, UIColors.Get("LightlessPurple"));
Push(ImGuiCol.SliderGrab, new Vector4(101, 101, 101, 255));
Push(ImGuiCol.SliderGrabActive, new Vector4(123, 123, 123, 255));
Push(ImGuiCol.Button, UIColors.Get("ButtonDefault"));
Push(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple"));
Push(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive"));
Push(ImGuiCol.Header, new Vector4(0, 0, 0, 60));
Push(ImGuiCol.HeaderHovered, new Vector4(0, 0, 0, 90));
Push(ImGuiCol.HeaderActive, new Vector4(0, 0, 0, 120));
Push(ImGuiCol.Separator, new Vector4(75, 75, 75, 121));
Push(ImGuiCol.SeparatorHovered, UIColors.Get("LightlessPurple"));
Push(ImGuiCol.SeparatorActive, UIColors.Get("LightlessPurpleActive"));
Push(ImGuiCol.ResizeGrip, new Vector4(0, 0, 0, 0));
Push(ImGuiCol.ResizeGripHovered, new Vector4(0, 0, 0, 0));
Push(ImGuiCol.ResizeGripActive, UIColors.Get("LightlessPurpleActive"));
Push(ImGuiCol.Tab, new Vector4(40, 40, 40, 255));
Push(ImGuiCol.TabHovered, UIColors.Get("LightlessPurple"));
Push(ImGuiCol.TabActive, UIColors.Get("LightlessPurpleActive"));
Push(ImGuiCol.TabUnfocused, new Vector4(40, 40, 40, 255));
Push(ImGuiCol.TabUnfocusedActive, UIColors.Get("LightlessPurpleActive"));
Push(ImGuiCol.DockingPreview, UIColors.Get("LightlessPurpleActive"));
Push(ImGuiCol.DockingEmptyBg, new Vector4(50, 50, 50, 255));
Push(ImGuiCol.PlotLines, new Vector4(150, 150, 150, 255));
Push(ImGuiCol.TableHeaderBg, new Vector4(48, 48, 48, 255));
Push(ImGuiCol.TableBorderStrong, new Vector4(79, 79, 89, 255));
Push(ImGuiCol.TableBorderLight, new Vector4(59, 59, 64, 255));
Push(ImGuiCol.TableRowBg, new Vector4(0, 0, 0, 0));
Push(ImGuiCol.TableRowBgAlt, new Vector4(255, 255, 255, 15));
Push(ImGuiCol.TextSelectedBg, new Vector4(98, 75, 224, 255));
Push(ImGuiCol.DragDropTarget, new Vector4(98, 75, 224, 255));
Push(ImGuiCol.NavHighlight, new Vector4(98, 75, 224, 179));
Push(ImGuiCol.NavWindowingDimBg, new Vector4(204, 204, 204, 51));
Push(ImGuiCol.NavWindowingHighlight, new Vector4(204, 204, 204, 89));
PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(6, 6));
PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(4, 3));
PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(4, 4));
PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(4, 4));
PushStyleVar(ImGuiStyleVar.ItemInnerSpacing, new Vector2(4, 4));
PushStyleVar(ImGuiStyleVar.IndentSpacing, 21.0f);
PushStyleVar(ImGuiStyleVar.ScrollbarSize, 10.0f);
PushStyleVar(ImGuiStyleVar.GrabMinSize, 20.0f);
PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1.5f);
PushStyleVar(ImGuiStyleVar.ChildBorderSize, 1.5f);
PushStyleVar(ImGuiStyleVar.PopupBorderSize, 1.5f);
PushStyleVar(ImGuiStyleVar.FrameBorderSize, 0f);
PushStyleVar(ImGuiStyleVar.WindowRounding, 7f);
PushStyleVar(ImGuiStyleVar.ChildRounding, 4f);
PushStyleVar(ImGuiStyleVar.FrameRounding, 4f);
PushStyleVar(ImGuiStyleVar.PopupRounding, 4f);
PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 4f);
PushStyleVar(ImGuiStyleVar.GrabRounding, 4f);
PushStyleVar(ImGuiStyleVar.TabRounding, 4f);
}
public static void PopStyle()
{
if (!_hasPushed)
return;
if (_pushedStyleVarCount > 0)
ImGui.PopStyleVar(_pushedStyleVarCount);
if (_pushedColorCount > 0)
ImGui.PopStyleColor(_pushedColorCount);
_hasPushed = false; _hasPushed = false;
return; _pushedColorCount = 0;
_pushedStyleVarCount = 0;
} }
_hasPushed = true; private static void Push(ImGuiCol col, Vector4 rgba)
_pushedColorCount = 0;
_pushedStyleVarCount = 0;
foreach (var option in _colorOptions)
Push(option.Target, ResolveColor(option));
foreach (var option in _vector2Options)
PushStyleVar(option.Target, ResolveVector(option));
foreach (var option in _floatOptions)
PushStyleVar(option.Target, ResolveFloat(option));
}
public static void PopStyle()
{
if (!_hasPushed)
return;
if (_pushedStyleVarCount > 0)
ImGui.PopStyleVar(_pushedStyleVarCount);
if (_pushedColorCount > 0)
ImGui.PopStyleColor(_pushedColorCount);
_hasPushed = false;
_pushedColorCount = 0;
_pushedStyleVarCount = 0;
}
private static Vector4 ResolveColor(StyleColorOption option)
{
var defaultValue = NormalizeColorVector(option.DefaultValue());
if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Color is { } packed)
return PackedColorToVector4(packed);
return defaultValue;
}
private static Vector2 ResolveVector(StyleVector2Option option)
{
var value = option.DefaultValue();
if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Vector2 is { } vectorOverride)
{ {
value = vectorOverride; if (rgba.X > 1f || rgba.Y > 1f || rgba.Z > 1f || rgba.W > 1f)
rgba /= 255f;
ImGui.PushStyleColor(col, rgba);
_pushedColorCount++;
} }
if (option.Min is { } min) private static void Push(ImGuiCol col, uint packedRgba)
value = Vector2.Max(value, min);
if (option.Max is { } max)
value = Vector2.Min(value, max);
return value;
}
private static float ResolveFloat(StyleFloatOption option)
{
var value = option.DefaultValue;
if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Float is { } floatOverride)
{ {
value = floatOverride; ImGui.PushStyleColor(col, packedRgba);
_pushedColorCount++;
} }
if (option.Min.HasValue) private static void PushStyleVar(ImGuiStyleVar var, float value)
value = MathF.Max(option.Min.Value, value); {
if (option.Max.HasValue) ImGui.PushStyleVar(var, value);
value = MathF.Min(option.Max.Value, value); _pushedStyleVarCount++;
return value; }
private static void PushStyleVar(ImGuiStyleVar var, Vector2 value)
{
ImGui.PushStyleVar(var, value);
_pushedStyleVarCount++;
}
} }
private static void Push(ImGuiCol col, Vector4 rgba)
{
rgba = NormalizeColorVector(rgba);
ImGui.PushStyleColor(col, rgba);
_pushedColorCount++;
}
private static void PushStyleVar(ImGuiStyleVar var, float value)
{
ImGui.PushStyleVar(var, value);
_pushedStyleVarCount++;
}
private static void PushStyleVar(ImGuiStyleVar var, Vector2 value)
{
ImGui.PushStyleVar(var, value);
_pushedStyleVarCount++;
}
private static Vector4 Rgba(byte r, byte g, byte b, byte a = 255)
=> new Vector4(r / 255f, g / 255f, b / 255f, a / 255f);
internal static Vector4 NormalizeColorVector(Vector4 rgba)
{
if (rgba.X > 1f || rgba.Y > 1f || rgba.Z > 1f || rgba.W > 1f)
rgba /= 255f;
return rgba;
}
internal static Vector4 PackedColorToVector4(uint color)
=> new(
(color & 0xFF) / 255f,
((color >> 8) & 0xFF) / 255f,
((color >> 16) & 0xFF) / 255f,
((color >> 24) & 0xFF) / 255f);
} }

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,4 +1,4 @@
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;
@@ -11,8 +11,12 @@ using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Utils; using LightlessSync.Utils;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using Serilog;
using System;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Reflection.Emit;
using System.Threading.Tasks;
namespace LightlessSync.UI; namespace LightlessSync.UI;
@@ -29,14 +33,13 @@ public class TopTabMenu
private bool _pairRequestsExpanded; // useless for now private bool _pairRequestsExpanded; // useless for now
private int _lastRequestCount; private int _lastRequestCount;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly NotificationService _lightlessNotificationService;
private string _filter = string.Empty; private string _filter = string.Empty;
private int _globalControlCountdown = 0; private int _globalControlCountdown = 0;
private float _pairRequestsHeight = 150f; private float _pairRequestsHeight = 150f;
private string _pairToAdd = string.Empty; private string _pairToAdd = string.Empty;
private SelectedTab _selectedTab = SelectedTab.None; private SelectedTab _selectedTab = SelectedTab.None;
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService)
{ {
_lightlessMediator = lightlessMediator; _lightlessMediator = lightlessMediator;
_apiController = apiController; _apiController = apiController;
@@ -44,7 +47,6 @@ public class TopTabMenu
_pairRequestService = pairRequestService; _pairRequestService = pairRequestService;
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_lightlessNotificationService = lightlessNotificationService;
} }
private enum SelectedTab private enum SelectedTab
@@ -197,79 +199,16 @@ public class TopTabMenu
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
#if DEBUG #if DEBUG
if (ImGui.Button("Test Pair Request")) if (ImGui.Button("Add Test Pair Request"))
{
_lightlessNotificationService.ShowPairRequestNotification(
"Debug User",
"debug-user-id",
onAccept: () =>
{
_lightlessMediator.Publish(new NotificationMessage(
"Pair Accepted",
"Debug pair request was accepted!",
NotificationType.Info,
TimeSpan.FromSeconds(3)));
},
onDecline: () =>
{
_lightlessMediator.Publish(new NotificationMessage(
"Pair Declined",
"Debug pair request was declined.",
NotificationType.Warning,
TimeSpan.FromSeconds(3)));
}
);
}
ImGui.SameLine();
if (ImGui.Button("Test Info"))
{ {
var fakeCid = Guid.NewGuid().ToString("N");
var display = _pairRequestService.RegisterIncomingRequest(fakeCid, "Debug pair request");
_lightlessMediator.Publish(new NotificationMessage( _lightlessMediator.Publish(new NotificationMessage(
"Information", "Pair request received (debug)",
"This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps.", display.Message,
NotificationType.Info, NotificationType.Info,
TimeSpan.FromSeconds(5))); TimeSpan.FromSeconds(5)));
} }
ImGui.SameLine();
if (ImGui.Button("Test Warning"))
{
_lightlessMediator.Publish(new NotificationMessage(
"Warning",
"This is a test warning notification.",
NotificationType.Warning,
TimeSpan.FromSeconds(7)));
}
ImGui.SameLine();
if (ImGui.Button("Test Error"))
{
_lightlessMediator.Publish(new NotificationMessage(
"Error",
"This is a test error notification erp police",
NotificationType.Error,
TimeSpan.FromSeconds(10)));
}
if (ImGui.Button("Test Download Progress"))
{
var downloadStatus = new List<(string playerName, float progress, string status)>
{
("Mauwmauw Nekochan", 0.85f, "downloading"),
("Raelynn Kitsune", 0.34f, "downloading"),
("Jaina Elraeth", 0.67f, "downloading"),
("Vaelstra Bloodthorn", 0.19f, "downloading"),
("Lydia Hera Moondrop", 0.86f, "downloading"),
("C'liina Star", 1.0f, "completed")
};
_lightlessNotificationService.ShowPairDownloadNotification(downloadStatus);
}
ImGui.SameLine();
if (ImGui.Button("Dismiss Download"))
{
_lightlessNotificationService.DismissPairDownloadNotification();
}
#endif #endif
DrawIncomingPairRequests(availableWidth); DrawIncomingPairRequests(availableWidth);
@@ -462,7 +401,7 @@ public class TopTabMenu
try try
{ {
var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false); await _apiController.TryPairWithContentId(request.HashedCid, myCidHash).ConfigureAwait(false);
_pairRequestService.RemoveRequest(request.HashedCid); _pairRequestService.RemoveRequest(request.HashedCid);
var display = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName; var display = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName;
@@ -911,4 +850,4 @@ public class TopTabMenu
ImGui.EndPopup(); ImGui.EndPopup();
} }
} }
} }

View File

@@ -26,9 +26,6 @@ namespace LightlessSync.UI
{ "LightlessAdminGlow", "#b09343" }, { "LightlessAdminGlow", "#b09343" },
{ "LightlessModeratorText", "#94ffda" }, { "LightlessModeratorText", "#94ffda" },
{ "LightlessModeratorGlow", "#599c84" }, { "LightlessModeratorGlow", "#599c84" },
{ "Lightfinder", "#ad8af5" },
{ "LightfinderEdge", "#000000" },
}; };
private static LightlessConfigService? _configService; private static LightlessConfigService? _configService;

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();
}
}
} }
} }

View File

@@ -35,16 +35,16 @@ public partial class ApiController
await _lightlessHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false); await _lightlessHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false);
} }
public async Task TryPairWithContentId(string otherCid) public async Task TryPairWithContentId(string otherCid, string myCid)
{ {
if (!IsConnected) return; if (!IsConnected) return;
await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid).ConfigureAwait(false); await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid, myCid).ConfigureAwait(false);
} }
public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null) public async Task SetBroadcastStatus(string hashedCid, bool enabled, GroupBroadcastRequestDto? groupDto = null)
{ {
CheckConnection(); CheckConnection();
await _lightlessHub!.InvokeAsync(nameof(SetBroadcastStatus), enabled, groupDto).ConfigureAwait(false); await _lightlessHub!.InvokeAsync(nameof(SetBroadcastStatus), hashedCid, enabled, groupDto).ConfigureAwait(false);
} }
public async Task<BroadcastStatusInfoDto?> IsUserBroadcasting(string hashedCid) public async Task<BroadcastStatusInfoDto?> IsUserBroadcasting(string hashedCid)
@@ -59,10 +59,10 @@ public partial class ApiController
return await _lightlessHub!.InvokeAsync<BroadcastStatusBatchDto>(nameof(AreUsersBroadcasting), hashedCids).ConfigureAwait(false); return await _lightlessHub!.InvokeAsync<BroadcastStatusBatchDto>(nameof(AreUsersBroadcasting), hashedCids).ConfigureAwait(false);
} }
public async Task<TimeSpan?> GetBroadcastTtl() public async Task<TimeSpan?> GetBroadcastTtl(string hashedCid)
{ {
CheckConnection(); CheckConnection();
return await _lightlessHub!.InvokeAsync<TimeSpan?>(nameof(GetBroadcastTtl)).ConfigureAwait(false); return await _lightlessHub!.InvokeAsync<TimeSpan?>(nameof(GetBroadcastTtl), hashedCid).ConfigureAwait(false);
} }
public async Task UserDelete() public async Task UserDelete()

View File

@@ -1,4 +1,4 @@
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Dto; using LightlessSync.API.Dto;
using LightlessSync.API.Dto.CharaData; using LightlessSync.API.Dto.CharaData;
@@ -6,7 +6,6 @@ using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSync.LightlessConfiguration.Models; using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -105,27 +104,25 @@ public partial class ApiController
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto) public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto)
{ {
if (dto == null) if (dto == null)
return Task.CompletedTask; return Task.CompletedTask;
var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty); var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty);
var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName;
_lightlessNotificationService.ShowPairRequestNotification( Mediator.Publish(new NotificationMessage(
senderName, "Pair request received",
request.HashedCid, request.Message,
onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid, senderName), NotificationType.Info,
onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid)); TimeSpan.FromSeconds(5)));
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo) public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo)
{ {
SystemInfoDto = systemInfo; SystemInfoDto = systemInfo;
//Mediator.Publish(new UpdateSystemInfoMessage(systemInfo));
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -1,4 +1,4 @@
using Dalamud.Utility; using Dalamud.Utility;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto; using LightlessSync.API.Dto;
@@ -32,7 +32,6 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
private readonly ServerConfigurationManager _serverManager; private readonly ServerConfigurationManager _serverManager;
private readonly TokenProvider _tokenProvider; private readonly TokenProvider _tokenProvider;
private readonly LightlessConfigService _lightlessConfigService; private readonly LightlessConfigService _lightlessConfigService;
private readonly NotificationService _lightlessNotificationService;
private CancellationTokenSource _connectionCancellationTokenSource; private CancellationTokenSource _connectionCancellationTokenSource;
private ConnectionDto? _connectionDto; private ConnectionDto? _connectionDto;
private bool _doNotNotifyOnNextInfo = false; private bool _doNotNotifyOnNextInfo = false;
@@ -45,7 +44,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, DalamudUtilService dalamudUtil, public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, DalamudUtilService dalamudUtil,
PairManager pairManager, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator, PairManager pairManager, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator,
TokenProvider tokenProvider, LightlessConfigService lightlessConfigService, NotificationService lightlessNotificationService) : base(logger, mediator) TokenProvider tokenProvider, LightlessConfigService lightlessConfigService) : base(logger, mediator)
{ {
_hubFactory = hubFactory; _hubFactory = hubFactory;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
@@ -54,7 +53,6 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
_serverManager = serverManager; _serverManager = serverManager;
_tokenProvider = tokenProvider; _tokenProvider = tokenProvider;
_lightlessConfigService = lightlessConfigService; _lightlessConfigService = lightlessConfigService;
_lightlessNotificationService = lightlessNotificationService;
_connectionCancellationTokenSource = new CancellationTokenSource(); _connectionCancellationTokenSource = new CancellationTokenSource();
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn()); Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());