Compare commits

...

74 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
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
choco
4060ba96f1 moved settings button from compact UI to top tab menu 2025-09-25 00:15:42 +02: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
35 changed files with 2046 additions and 926 deletions

View File

@@ -241,6 +241,7 @@ jobs:
updatedRepoJson=$(jq \
--arg internalName "$internalName" \
--arg dalamudApiLevel "$dalamudApiLevel" \
--arg assemblyVersion "$assemblyVersion" \
--arg version "$version" \
--arg downloadUrl "$downloadUrl" \
'

View File

@@ -1,140 +0,0 @@
name: Tag and Release Lightless
on:
push:
branches: [ master ]
env:
PLUGIN_NAME: LightlessSync
DOTNET_VERSION: 9.x
jobs:
tag-and-release:
runs-on: windows-2022
permissions:
contents: write
steps:
- name: Checkout Lightless
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
- name: Setup .NET 9 SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.x
- name: Download Dalamud
run: |
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
- name: Lets Build Lightless!
run: |
dotnet restore
dotnet build --configuration Release --no-restore
dotnet publish --configuration Release --no-build
- name: Get version
id: package_version
uses: KageKirin/get-csproj-version@v0
with:
file: LightlessSync/LightlessSync.csproj
- name: Display version
run: |
echo "Version: ${{ steps.package_version.outputs.version }}"
- name: Prepare Lightless Client
run: |
$publishPath = "${{ env.PLUGIN_NAME }}/bin/x64/Release/publish"
if (Test-Path $publishPath) {
Remove-Item -Recurse -Force $publishPath
Write-Host "Removed $publishPath"
} else {
Write-Host "$publishPath does not exist, nothing to remove."
}
mkdir output
Compress-Archive -Path ${{ env.PLUGIN_NAME }}/bin/x64/Release/* -DestinationPath output/LightlessClient.zip
- name: Create Git tag if not exists
shell: pwsh
run: |
$tag = "${{ steps.package_version.outputs.version }}"
git fetch --tags
if (-not (git tag -l $tag)) {
Write-Host "Tag $tag does not exist. Creating and pushing..."
git config user.name "GitHub Action"
git config user.email "action@github.com"
git tag $tag
git push origin $tag
} else {
Write-Host "Tag $tag already exists. Skipping tag creation."
}
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.package_version.outputs.version }}
name: ${{ steps.package_version.outputs.version }}
draft: false
prerelease: false
files: output/LightlessClient.zip
- name: Clone plugin hosting repo
run: |
mkdir LightlessSyncRepo
cd LightlessSyncRepo
git clone https://github.com/${{ github.repository_owner }}/LightlessSync.git
env:
GIT_TERMINAL_PROMPT: 0
- name: Update plogonmaster.json with version
shell: pwsh
env:
VERSION: ${{ steps.package_version.outputs.version }}
run: |
$pluginJsonPath = "${{ env.PLUGIN_NAME }}/bin/x64/Release/${{ env.PLUGIN_NAME }}.json"
$pluginJson = Get-Content $pluginJsonPath | ConvertFrom-Json
$repoJsonPath = "LightlessSyncRepo/LightlessSync/plogonmaster.json"
$repoJsonRaw = Get-Content $repoJsonPath -Raw
$repoJson = $repoJsonRaw | ConvertFrom-Json
$version = $env:VERSION
$downloadUrl = "https://github.com/${{ github.repository_owner }}/LightlessClient/releases/download/$version/LightlessClient.zip"
if (-not ($repoJson -is [System.Collections.IEnumerable])) {
$repoJson = @($repoJson)
}
foreach ($plugin in $repoJson) {
if ($plugin.InternalName -eq $pluginJson.InternalName) {
$plugin.DalamudApiLevel = $pluginJson.DalamudApiLevel
$plugin.AssemblyVersion = $version
$plugin.DownloadLinkInstall = $downloadUrl
$plugin.DownloadLinkTesting = $downloadUrl
$plugin.DownloadLinkUpdate = $downloadUrl
}
}
$repoJson | ConvertTo-Json -Depth 100 | Set-Content $repoJsonPath
# Convert to JSON and force array brackets if necessary
$repoJsonString = $repoJson | ConvertTo-Json -Depth 100
# If the output is not an array, wrap it manually
if ($repoJsonString.Trim().StartsWith('{')) {
$repoJsonString = "[$repoJsonString]"
}
$repoJsonString | Set-Content $repoJsonPath
- name: Commit and push to LightlessSync
run: |
cd LightlessSyncRepo/LightlessSync
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git add .
git commit -m "Update ${{ env.PLUGIN_NAME }} to ${{ steps.package_version.outputs.version }}"
git push https://x-access-token:${{ secrets.LIGHTLESS_TOKEN }}@github.com/${{ github.repository_owner }}/LightlessSync.git HEAD:main

View File

@@ -20,7 +20,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
private long _currentFileProgress = 0;
private CancellationTokenSource _scanCancellationTokenSource = new();
private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk"];
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,

View File

@@ -189,7 +189,14 @@ public sealed class FileCacheManager : IHostedService
Parallel.ForEach(allEntities, entity =>
{
cacheDict[entity.PrefixedFilePath] = entity;
if (entity != null && entity.PrefixedFilePath != null)
{
cacheDict[entity.PrefixedFilePath] = entity;
}
else
{
_logger.LogWarning("Null FileCacheEntity or PrefixedFilePath encountered in cache population: {entity}", entity);
}
});
var cleanedPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -253,6 +260,7 @@ public sealed class FileCacheManager : IHostedService
{
if (_fileCaches.TryGetValue(hash, out var caches))
{
_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);
@@ -397,6 +405,12 @@ public sealed class FileCacheManager : IHostedService
private FileCacheEntity? Validate(FileCacheEntity fileCache)
{
if (string.IsNullOrWhiteSpace(fileCache.ResolvedFilepath))
{
_logger.LogWarning("FileCacheEntity has empty ResolvedFilepath for hash {hash}, prefixed path {prefixed}", fileCache.Hash, fileCache.PrefixedFilePath);
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
return null;
}
var file = new FileInfo(fileCache.ResolvedFilepath);
if (!file.Exists)
{

View File

@@ -17,7 +17,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
private readonly HashSet<string> _cachedHandledPaths = new(StringComparer.Ordinal);
private readonly TransientConfigService _configurationService;
private readonly DalamudUtilService _dalamudUtil;
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk"];
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "kdb"];
private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
private ConcurrentDictionary<IntPtr, ObjectKind> _cachedFrameAddresses = [];

View File

@@ -67,6 +67,8 @@ public class LightlessConfig : ILightlessConfiguration
public bool UseFocusTarget { get; set; } = false;
public bool overrideFriendColor { get; set; } = false;
public bool overridePartyColor { get; set; } = false;
public bool overrideFcTagColor { get; set; } = false;
public bool useColoredUIDs { get; set; } = true;
public bool BroadcastEnabled { get; set; } = false;
public DateTime BroadcastTtl { get; set; } = DateTime.MinValue;
public bool SyncshellFinderEnabled { get; set; } = false;

View File

@@ -1,4 +1,4 @@
using Dalamud.Game;
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Windowing;
@@ -113,6 +113,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<PluginWarningNotificationService>();
collection.AddSingleton<FileCompactor>();
collection.AddSingleton<TagHandler>();
collection.AddSingleton<PairRequestService>();
collection.AddSingleton<IdDisplayHandler>();
collection.AddSingleton<PlayerPerformanceService>();
collection.AddSingleton<TransientResourceManager>();
@@ -147,7 +148,9 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<RedrawManager>();
collection.AddSingleton<BroadcastService>();
collection.AddSingleton(addonLifecycle);
collection.AddSingleton(p => new ContextMenu(contextMenu, pluginInterface, gameData, p.GetRequiredService<ILogger<ContextMenu>>(), p.GetRequiredService<DalamudUtilService>(), p.GetRequiredService<ApiController>(), objectTable));
collection.AddSingleton(p => new ContextMenuService(contextMenu, pluginInterface, gameData,
p.GetRequiredService<ILogger<ContextMenuService>>(), p.GetRequiredService<DalamudUtilService>(), p.GetRequiredService<ApiController>(), objectTable,
p.GetRequiredService<LightlessConfigService>(), p.GetRequiredService<PairRequestService>(), p.GetRequiredService<PairManager>(), clientState));
collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService<ILogger<IpcCallerPenumbra>>(), pluginInterface,
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<RedrawManager>()));
collection.AddSingleton((s) => new IpcCallerGlamourer(s.GetRequiredService<ILogger<IpcCallerGlamourer>>(), pluginInterface,
@@ -230,7 +233,7 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<LightlessProfileManager>(), s.GetRequiredService<PerformanceCollectorService>()));
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
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<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<IPopupHandler, BanUserPopupHandler>();
collection.AddScoped<IPopupHandler, CensusPopupHandler>();
collection.AddScoped<CacheCreationService>();
@@ -261,7 +264,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
collection.AddHostedService(p => p.GetRequiredService<LightlessPlugin>());
collection.AddHostedService(p => p.GetRequiredService<ContextMenu>());
collection.AddHostedService(p => p.GetRequiredService<ContextMenuService>());
collection.AddHostedService(p => p.GetRequiredService<BroadcastService>());
})
.Build();
@@ -274,4 +277,4 @@ public sealed class Plugin : IDalamudPlugin
_host.StopAsync().GetAwaiter().GetResult();
_host.Dispose();
}
}
}

View File

@@ -4,9 +4,7 @@ using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using LightlessSync.WebAPI.SignalR;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -16,7 +14,6 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
private readonly ILogger<BroadcastService> _logger;
private readonly ApiController _apiController;
private readonly LightlessMediator _mediator;
private readonly HubFactory _hubFactory;
private readonly LightlessConfigService _config;
private readonly DalamudUtilService _dalamudUtil;
public LightlessMediator Mediator => _mediator;
@@ -29,35 +26,37 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
private TimeSpan? _remainingTtl = null;
private DateTime _lastTtlCheck = DateTime.MinValue;
private DateTime _lastForcedDisableTime = DateTime.MinValue;
private static readonly TimeSpan DisableCooldown = TimeSpan.FromSeconds(5);
private static readonly TimeSpan _disableCooldown = TimeSpan.FromSeconds(5);
public TimeSpan? RemainingTtl => _remainingTtl;
public TimeSpan? RemainingCooldown
{
get
{
var elapsed = DateTime.UtcNow - _lastForcedDisableTime;
if (elapsed >= DisableCooldown) return null;
return DisableCooldown - elapsed;
if (elapsed >= _disableCooldown) return null;
return _disableCooldown - elapsed;
}
}
public BroadcastService(ILogger<BroadcastService> logger, LightlessMediator mediator, HubFactory hubFactory, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController)
public BroadcastService(ILogger<BroadcastService> logger, LightlessMediator mediator, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController)
{
_logger = logger;
_mediator = mediator;
_hubFactory = hubFactory;
_config = config;
_dalamudUtil = dalamudUtil;
_apiController = apiController;
}
private async Task RequireConnectionAsync(string context, Func<Task> action)
{
if (!_apiController.IsConnected)
{
_logger.LogDebug($"{context} skipped, not connected");
_logger.LogDebug(context + " skipped, not connected");
return;
}
await action().ConfigureAwait(false);
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_mediator.Subscribe<EnableBroadcastMessage>(this, OnEnableBroadcast);
@@ -65,7 +64,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
_apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken);
_ = CheckLightfinderSupportAsync(cancellationToken);
//_ = CheckLightfinderSupportAsync(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
@@ -86,13 +85,12 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
if (cancellationToken.IsCancellationRequested)
return;
var hub = _hubFactory.GetOrCreate(CancellationToken.None);
var dummy = "0".PadLeft(64, '0');
await hub.InvokeAsync<BroadcastStatusInfoDto?>("IsUserBroadcasting", dummy, cancellationToken);
await hub.InvokeAsync("SetBroadcastStatus", dummy, true, null, cancellationToken);
await hub.InvokeAsync<TimeSpan?>("GetBroadcastTtl", dummy, cancellationToken);
await hub.InvokeAsync<Dictionary<string, BroadcastStatusInfoDto?>>("AreUsersBroadcasting", new[] { dummy }, cancellationToken);
await _apiController.IsUserBroadcasting(dummy).ConfigureAwait(false);
await _apiController.SetBroadcastStatus(dummy, true, null).ConfigureAwait(false);
await _apiController.GetBroadcastTtl(dummy).ConfigureAwait(false);
await _apiController.AreUsersBroadcasting([dummy]).ConfigureAwait(false);
IsLightFinderAvailable = true;
_logger.LogInformation("Lightfinder is available.");
@@ -119,6 +117,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
}
}
@@ -142,7 +141,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
await _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false);
_logger.LogInformation("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)
{
@@ -151,13 +150,13 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
_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;
}
_waitingForTtlFetch = true;
var ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
if (ttl is { } remaining && remaining > TimeSpan.Zero)
{
@@ -165,7 +164,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
_config.Current.BroadcastEnabled = true;
_config.Save();
_logger.LogInformation("Fetched TTL from server: {TTL}", remaining);
_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}")));
}
@@ -202,13 +201,13 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
{
try
{
_logger.LogInformation("[BroadcastCheck] Checking CID: {cid}", targetCid);
_logger.LogDebug("[BroadcastCheck] Checking CID: {cid}", targetCid);
var info = await _apiController.IsUserBroadcasting(targetCid).ConfigureAwait(false);
result = info?.TTL > TimeSpan.Zero;
_logger.LogInformation("[BroadcastCheck] Result for {cid}: {result} (TTL: {ttl}, GID: {gid})", targetCid, result, info?.TTL, info?.GID);
_logger.LogDebug("[BroadcastCheck] Result for {cid}: {result} (TTL: {ttl}, GID: {gid})", targetCid, result, info?.TTL, info?.GID);
}
catch (Exception ex)
{
@@ -252,7 +251,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
result[kv.Key] = kv.Value;
}
_logger.LogInformation("Batch broadcast status check complete for {Count} CIDs", hashedCids.Count);
_logger.LogTrace("Batch broadcast status check complete for {Count} CIDs", hashedCids.Count);
}
catch (Exception ex)
{
@@ -292,10 +291,10 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
if (!newStatus)
{
_lastForcedDisableTime = DateTime.UtcNow;
_logger.LogInformation("Manual disable: cooldown timer started.");
_logger.LogDebug("Manual disable: cooldown timer started.");
}
_logger.LogInformation("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus);
_logger.LogDebug("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus);
_mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus));
}
@@ -325,15 +324,15 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
_syncedOnStartup = true;
try
{
var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
var ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
string hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
if (ttl is { }
remaining && remaining > TimeSpan.Zero)
{
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
_config.Current.BroadcastEnabled = true;
_config.Save();
_logger.LogInformation("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining);
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining);
}
else
{
@@ -357,12 +356,12 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
return;
}
var expiry = _config.Current.BroadcastTtl;
var remaining = expiry - DateTime.UtcNow;
DateTime expiry = _config.Current.BroadcastTtl;
TimeSpan remaining = expiry - DateTime.UtcNow;
_remainingTtl = remaining > TimeSpan.Zero ? remaining : null;
if (_remainingTtl == null)
{
_logger.LogInformation("Broadcast TTL expired. Disabling broadcast locally.");
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();

View File

@@ -0,0 +1,204 @@
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
internal class ContextMenuService : IHostedService
{
private readonly IContextMenu _contextMenu;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IDataManager _gameData;
private readonly ILogger<ContextMenuService> _logger;
private readonly DalamudUtilService _dalamudUtil;
private readonly IClientState _clientState;
private readonly PairManager _pairManager;
private readonly PairRequestService _pairRequestService;
private readonly ApiController _apiController;
private readonly IObjectTable _objectTable;
public ContextMenuService(
IContextMenu contextMenu,
IDalamudPluginInterface pluginInterface,
IDataManager gameData,
ILogger<ContextMenuService> logger,
DalamudUtilService dalamudUtil,
ApiController apiController,
IObjectTable objectTable,
LightlessConfigService configService,
PairRequestService pairRequestService,
PairManager pairManager,
IClientState clientState)
{
_contextMenu = contextMenu;
_pluginInterface = pluginInterface;
_gameData = gameData;
_logger = logger;
_dalamudUtil = dalamudUtil;
_apiController = apiController;
_objectTable = objectTable;
_pairManager = pairManager;
_pairRequestService = pairRequestService;
_clientState = clientState;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_contextMenu.OnMenuOpened += OnMenuOpened;
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_contextMenu.OnMenuOpened -= OnMenuOpened;
return Task.CompletedTask;
}
public void Enable()
{
_contextMenu.OnMenuOpened += OnMenuOpened;
_logger.LogDebug("Context menu enabled.");
}
public void Disable()
{
_contextMenu.OnMenuOpened -= OnMenuOpened;
_logger.LogDebug("Context menu disabled.");
}
private void OnMenuOpened(IMenuOpenedArgs args)
{
if (!_pluginInterface.UiBuilder.ShouldModifyUi)
return;
if (args.AddonName != null)
return;
//Check if target is not menutargetdefault.
if (args.Target is not MenuTargetDefault target)
return;
//Check if name or target id isnt null/zero
if (string.IsNullOrEmpty(target.TargetName) || target.TargetObjectId == 0 || target.TargetHomeWorld.RowId == 0)
return;
//Check if it is a real target.
IPlayerCharacter? targetData = GetPlayerFromObjectTable(target);
if (targetData == null || targetData.Address == nint.Zero)
return;
//Check if user is paired or is own.
if (VisibleUserIds.Any(u => u == target.TargetObjectId) || _clientState.LocalPlayer.GameObjectId == target.TargetObjectId)
return;
//Check if in PVP or GPose
if (_clientState.IsPvPExcludingDen || _clientState.IsGPosing)
return;
//Check for valid world.
var world = GetWorld(target.TargetHomeWorld.RowId);
if (!IsWorldValid(world))
return;
args.AddMenuItem(new MenuItem
{
Name = "Send Pair Request",
PrefixChar = 'L',
UseDefaultPrefix = false,
PrefixColor = 708,
OnClicked = async _ => await HandleSelection(args).ConfigureAwait(false)
});
}
private async Task HandleSelection(IMenuArgs args)
{
if (args.Target is not MenuTargetDefault target)
return;
var world = GetWorld(target.TargetHomeWorld.RowId);
if (!IsWorldValid(world))
return;
try
{
IPlayerCharacter? targetData = GetPlayerFromObjectTable(target);
if (targetData == null || targetData.Address == nint.Zero)
{
_logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name);
return;
}
var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
await _apiController.TryPairWithContentId(receiverCid, senderCid).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(receiverCid))
{
_pairRequestService.RemoveRequest(receiverCid);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending pair request.");
}
}
private HashSet<ulong> VisibleUserIds => [.. _pairManager.GetOnlineUserPairs()
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
.Select(u => (ulong)u.PlayerCharacterId)];
private IPlayerCharacter? GetPlayerFromObjectTable(MenuTargetDefault target)
{
return _objectTable
.OfType<IPlayerCharacter>()
.FirstOrDefault(p =>
string.Equals(p.Name.TextValue, target.TargetName, StringComparison.OrdinalIgnoreCase) &&
p.HomeWorld.RowId == target.TargetHomeWorld.RowId);
}
private World GetWorld(uint worldId)
{
var sheet = _gameData.GetExcelSheet<World>()!;
var luminaWorlds = sheet.Where(x =>
{
var dc = x.DataCenter.ValueNullable;
var name = x.Name.ExtractText();
var internalName = x.InternalName.ExtractText();
if (dc == null || dc.Value.Region == 0 || string.IsNullOrWhiteSpace(dc.Value.Name.ExtractText()))
return false;
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(internalName))
return false;
if (name.Contains('-', StringComparison.Ordinal) || name.Contains('_', StringComparison.Ordinal))
return false;
return x.DataCenter.Value.Region != 5 || x.RowId > 3001 && x.RowId != 1200 && IsChineseJapaneseKoreanString(name);
});
return luminaWorlds.FirstOrDefault(x => x.RowId == worldId);
}
private static bool IsChineseJapaneseKoreanString(string text) => text.All(IsChineseJapaneseKoreanCharacter);
private static bool IsChineseJapaneseKoreanCharacter(char c) => c >= 0x4E00 && c <= 0x9FFF;
public bool IsWorldValid(uint worldId) => IsWorldValid(GetWorld(worldId));
public static bool IsWorldValid(World world)
{
var name = world.Name.ToString();
return !string.IsNullOrWhiteSpace(name) && char.IsUpper(name[0]);
}
}

View File

@@ -1,4 +1,4 @@
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
@@ -541,7 +541,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
curWaitTime += tick;
Thread.Sleep(tick);
}
Thread.Sleep(tick * 2);
}
@@ -557,6 +556,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return result;
}
public string? GetWorldNameFromPlayerAddress(nint address)
{
if (address == nint.Zero) return null;
EnsureIsOnFramework();
var playerCharacter = _objectTable.OfType<IPlayerCharacter>().FirstOrDefault(p => p.Address == address);
if (playerCharacter == null) return null;
var worldId = (ushort)playerCharacter.HomeWorld.RowId;
return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
}
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
{
var gameObj = (GameObject*)address;

View File

@@ -100,6 +100,7 @@ public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
public record SyncshellBroadcastsUpdatedMessage : MessageBase;
public record PairRequestsUpdatedMessage : MessageBase;
public record VisibilityChange : MessageBase;
#pragma warning restore S2094
#pragma warning restore MA0048 // File name must match type name

View File

@@ -1,4 +1,4 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.Gui.NamePlate;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
@@ -40,7 +40,6 @@ public class NameplateService : DisposableMediatorSubscriberBase
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
{
if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen))
return;
@@ -69,16 +68,26 @@ public class NameplateService : DisposableMediatorSubscriberBase
(isFriend && !friendColorAllowed)
))
{
//_logger.LogInformation("added nameplate color to {Name}", playerCharacter.Name.TextValue);
handler.NameParts.TextWrap = CreateTextWrap(colors);
}
if (_configService.Current.overrideFcTagColor)
{
bool hasActualFcTag = playerCharacter.CompanyTag.TextValue.Length > 0;
bool isFromDifferentRealm = playerCharacter.HomeWorld.RowId != playerCharacter.CurrentWorld.RowId;
bool shouldColorFcArea = hasActualFcTag || (!hasActualFcTag && isFromDifferentRealm);
if (shouldColorFcArea)
{
handler.FreeCompanyTagParts.OuterWrap = CreateTextWrap(colors);
handler.FreeCompanyTagParts.TextWrap = CreateTextWrap(colors);
}
}
}
}
}
public void RequestRedraw()
{
_namePlateGui.RequestRedraw();
}

View File

@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.Linq;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public sealed class PairRequestService : DisposableMediatorSubscriberBase
{
private readonly DalamudUtilService _dalamudUtil;
private readonly PairManager _pairManager;
private readonly object _syncRoot = new();
private readonly List<PairRequestEntry> _requests = [];
private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5);
public PairRequestService(ILogger<PairRequestService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager)
: base(logger, mediator)
{
_dalamudUtil = dalamudUtil;
_pairManager = pairManager;
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ =>
{
bool removed;
lock (_syncRoot)
{
removed = CleanupExpiredUnsafe();
}
if (removed)
{
Mediator.Publish(new PairRequestsUpdatedMessage());
}
});
}
public PairRequestDisplay RegisterIncomingRequest(string hashedCid, string messageTemplate)
{
if (string.IsNullOrWhiteSpace(hashedCid))
{
hashedCid = string.Empty;
}
messageTemplate ??= string.Empty;
PairRequestEntry entry = new(hashedCid, messageTemplate, DateTime.UtcNow);
lock (_syncRoot)
{
CleanupExpiredUnsafe();
var index = _requests.FindIndex(r => string.Equals(r.HashedCid, hashedCid, StringComparison.Ordinal));
if (index >= 0)
{
_requests[index] = entry;
}
else
{
_requests.Add(entry);
}
}
var display = _dalamudUtil.IsOnFrameworkThread
? ToDisplay(entry)
: _dalamudUtil.RunOnFrameworkThread(() => ToDisplay(entry)).GetAwaiter().GetResult();
Mediator.Publish(new PairRequestsUpdatedMessage());
return display;
}
public IReadOnlyList<PairRequestDisplay> GetActiveRequests()
{
List<PairRequestEntry> entries;
lock (_syncRoot)
{
CleanupExpiredUnsafe();
entries = _requests
.OrderByDescending(r => r.ReceivedAt)
.ToList();
}
return _dalamudUtil.IsOnFrameworkThread
? entries.Select(ToDisplay).ToList()
: _dalamudUtil.RunOnFrameworkThread(() => entries.Select(ToDisplay).ToList()).GetAwaiter().GetResult();
}
public bool RemoveRequest(string hashedCid)
{
bool removed;
lock (_syncRoot)
{
removed = _requests.RemoveAll(r => string.Equals(r.HashedCid, hashedCid, StringComparison.Ordinal)) > 0;
}
if (removed)
{
Mediator.Publish(new PairRequestsUpdatedMessage());
}
return removed;
}
public bool HasPendingRequests()
{
lock (_syncRoot)
{
CleanupExpiredUnsafe();
return _requests.Count > 0;
}
}
private PairRequestDisplay ToDisplay(PairRequestEntry entry)
{
var displayName = ResolveDisplayName(entry.HashedCid);
var message = FormatMessage(entry.MessageTemplate, displayName);
return new PairRequestDisplay(entry.HashedCid, displayName, message, entry.ReceivedAt);
}
private string ResolveDisplayName(string hashedCid)
{
if (string.IsNullOrWhiteSpace(hashedCid))
{
return string.Empty;
}
var (name, address) = _dalamudUtil.FindPlayerByNameHash(hashedCid);
if (!string.IsNullOrWhiteSpace(name))
{
var worldName = _dalamudUtil.GetWorldNameFromPlayerAddress(address);
return !string.IsNullOrWhiteSpace(worldName)
? $"{name} @ {worldName}"
: name;
}
var pair = _pairManager
.GetOnlineUserPairs()
.FirstOrDefault(p => string.Equals(p.Ident, hashedCid, StringComparison.Ordinal));
if (pair != null)
{
if (!string.IsNullOrWhiteSpace(pair.PlayerName))
{
return pair.PlayerName;
}
if (!string.IsNullOrWhiteSpace(pair.UserData.AliasOrUID))
{
return pair.UserData.AliasOrUID;
}
}
return string.Empty;
}
private static string FormatMessage(string template, string displayName)
{
var safeName = string.IsNullOrWhiteSpace(displayName) ? "Someone" : displayName;
template ??= string.Empty;
const string placeholder = "{DisplayName}";
if (!string.IsNullOrEmpty(template) && template.Contains(placeholder, StringComparison.Ordinal))
{
return template.Replace(placeholder, safeName, StringComparison.Ordinal);
}
if (!string.IsNullOrWhiteSpace(template))
{
return $"{safeName}: {template}";
}
return $"{safeName} sent you a pair request.";
}
private bool CleanupExpiredUnsafe()
{
if (_requests.Count == 0)
{
return false;
}
var now = DateTime.UtcNow;
return _requests.RemoveAll(r => now - r.ReceivedAt > Expiration) > 0;
}
private record struct PairRequestEntry(string HashedCid, string MessageTemplate, DateTime ReceivedAt);
public readonly record struct PairRequestDisplay(string HashedCid, string DisplayName, string Message, DateTime ReceivedAt);
}

View File

@@ -1,10 +1,12 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
using LightlessSync.API.Dto.Group;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using System.Numerics;
@@ -22,7 +24,7 @@ namespace LightlessSync.UI
private IReadOnlyList<GroupFullInfoDto> _allSyncshells;
private string _userUid = string.Empty;
private List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new();
private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new();
public BroadcastUI(
ILogger<BroadcastUI> logger,
@@ -44,11 +46,11 @@ namespace LightlessSync.UI
IsOpen = false;
this.SizeConstraints = new()
{
MinimumSize = new(600, 340),
MaximumSize = new(750, 400)
MinimumSize = new(600, 465),
MaximumSize = new(750, 525)
};
mediator.Subscribe<RefreshUiMessage>(this, async _ => await RefreshSyncshells());
mediator.Subscribe<RefreshUiMessage>(this, async _ => await RefreshSyncshells().ConfigureAwait(false));
}
private void RebuildSyncshellDropdownOptions()
@@ -62,7 +64,7 @@ namespace LightlessSync.UI
_syncshellOptions.Clear();
_syncshellOptions.Add(("None", null, true));
var addedGids = new HashSet<string>();
var addedGids = new HashSet<string>(StringComparer.Ordinal);
foreach (var shell in ownedSyncshells)
{
@@ -73,7 +75,7 @@ namespace LightlessSync.UI
if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid))
{
var matching = allSyncshells.FirstOrDefault(g => g.GID == selectedGid);
var matching = allSyncshells.FirstOrDefault(g => string.Equals(g.GID, selectedGid, StringComparison.Ordinal));
if (matching != null)
{
var label = matching.GroupAliasOrGID ?? matching.GID;
@@ -97,7 +99,7 @@ namespace LightlessSync.UI
{
if (!_apiController.IsConnected)
{
_allSyncshells = Array.Empty<GroupFullInfoDto>();
_allSyncshells = [];
RebuildSyncshellDropdownOptions();
return;
}
@@ -109,7 +111,7 @@ namespace LightlessSync.UI
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch Syncshells.");
_allSyncshells = Array.Empty<GroupFullInfoDto>();
_allSyncshells = [];
}
RebuildSyncshellDropdownOptions();
@@ -131,25 +133,66 @@ namespace LightlessSync.UI
ImGuiHelpers.ScaledDummy(0.25f);
}
if (ImGui.BeginTabBar("##MyTabBar"))
if (ImGui.BeginTabBar("##BroadcastTabs"))
{
if (ImGui.BeginTabItem("Lightfinder"))
{
_uiSharedService.MediumText("Lightfinder", UIColors.Get("PairBlue"));
ImGui.PushTextWrapPos();
ImGui.Text("This lets other Lightless users know you use Lightless.");
ImGui.Text("By enabling this, the server will allow other people to see that you are using Lightless.");
ImGui.Text("When disabled, pairing is still possible but both parties need to mutually send each other requests, receiving party will not be notified about the request unless the pairing is complete.");
ImGui.Text("At no point ever, even when Lightfinder is active that any Lightless data is getting sent to other people (including ID's), the server keeps this to itself.");
ImGui.Text("You can request to pair by right-clicking any (not yourself) character and using 'Send Pair Request'.");
ImGui.PopTextWrapPos();
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, -2));
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"),"This lets other Lightless users know you use Lightless. While enabled, you and others using Lightfinder can see each other identified as Lightless users.");
ImGui.Indent(15f);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
ImGui.Text("- This is done using a 'Lightless' label above player nameplates.");
ImGui.PopStyleColor();
ImGui.Unindent(15f);
ImGuiHelpers.ScaledDummy(3f);
_uiSharedService.MediumText("Pairing", UIColors.Get("PairBlue"));
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Pairing may be initiated via the right-click context menu on another player." +
" The process requires mutual confirmation: the sender initiates the request, and the recipient completes it by responding with a request in return.");
_uiSharedService.DrawNoteLine(
"! ",
UIColors.Get("LightlessYellow"),
new SeStringUtils.RichTextEntry("If Lightfinder is "),
new SeStringUtils.RichTextEntry("ENABLED", UIColors.Get("LightlessGreen"), true),
new SeStringUtils.RichTextEntry(" when a pair request is made, the receiving user will get notified about it."));
_uiSharedService.DrawNoteLine(
"! ",
UIColors.Get("LightlessYellow"),
new SeStringUtils.RichTextEntry("If Lightfinder is "),
new SeStringUtils.RichTextEntry("DISABLED", UIColors.Get("DimRed"), true),
new SeStringUtils.RichTextEntry(" when a pair request is made, the receiving user will "),
new SeStringUtils.RichTextEntry("NOT", UIColors.Get("DimRed"), true),
new SeStringUtils.RichTextEntry(" get a notification, and the request will not be visible to them in any way."));
ImGuiHelpers.ScaledDummy(3f);
_uiSharedService.MediumText("Privacy", UIColors.Get("PairBlue"));
_uiSharedService.DrawNoteLine(
"! ",
UIColors.Get("DimRed"),
new SeStringUtils.RichTextEntry("Lightfinder is entirely "),
new SeStringUtils.RichTextEntry("opt-in", UIColors.Get("LightlessYellow"), true),
new SeStringUtils.RichTextEntry(" and does not share any data with other users. All identifying information remains private to the server."));
_uiSharedService.DrawNoteLine("! ", UIColors.Get("DimRed"), "Pairing is intended as a mutual agreement between both parties. A pair request will not be visible to the recipient unless Lightfinder is enabled.");
ImGuiHelpers.ScaledDummy(5f);
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.Text("Use it only when you want to be visible.");
ImGui.TextWrapped("Use Lightfinder when you're okay with being visible to other users and understand that you are responsible for your own experience.");
ImGui.PopStyleColor();
ImGuiHelpers.ScaledDummy(0.2f);
ImGui.PopStyleVar();
ImGuiHelpers.ScaledDummy(3f);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
if (_configService.Current.BroadcastEnabled)
@@ -168,7 +211,7 @@ namespace LightlessSync.UI
else
{
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();
}
}
@@ -260,14 +303,14 @@ namespace LightlessSync.UI
}
var selectedGid = _configService.Current.SelectedFinderSyncshell;
var currentOption = _syncshellOptions.FirstOrDefault(o => o.GID == selectedGid);
var currentOption = _syncshellOptions.FirstOrDefault(o => string.Equals(o.GID, selectedGid, StringComparison.Ordinal));
var preview = currentOption.Label ?? "Select a Syncshell...";
if (ImGui.BeginCombo("##SyncshellDropdown", preview))
{
foreach (var (label, gid, available) in _syncshellOptions)
{
bool isSelected = gid == selectedGid;
bool isSelected = string.Equals(gid, selectedGid, StringComparison.Ordinal);
if (!available)
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
@@ -310,6 +353,7 @@ namespace LightlessSync.UI
ImGui.EndTabItem();
}
#if DEBUG
if (ImGui.BeginTabItem("Debug"))
{
ImGui.Text("Broadcast Cache");
@@ -366,17 +410,12 @@ namespace LightlessSync.UI
ImGui.EndTable();
}
ImGui.EndTabItem();
}
#endif
ImGui.EndTabBar();
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
}
}
}

View File

@@ -24,6 +24,7 @@ using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Reflection;
@@ -85,7 +86,7 @@ public class CompactUi : WindowMediatorSubscriberBase
IpcManager ipcManager,
BroadcastService broadcastService,
CharacterAnalyzer characterAnalyzer,
PlayerPerformanceConfigService playerPerformanceConfig) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
{
_uiSharedService = uiShared;
_configService = configService;
@@ -103,7 +104,7 @@ public class CompactUi : WindowMediatorSubscriberBase
_renamePairTagUi = renameTagUi;
_ipcManager = ipcManager;
_broadcastService = broadcastService;
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService);
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService);
AllowPinning = true;
AllowClickthrough = false;
@@ -141,7 +142,7 @@ public class CompactUi : WindowMediatorSubscriberBase
},
};
_drawFolders = [.. GetDrawFolders()];
_drawFolders = [.. DrawFolders];
#if DEBUG
string dev = "Dev Build";
@@ -158,7 +159,7 @@ public class CompactUi : WindowMediatorSubscriberBase
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = GetDrawFolders().ToList());
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = DrawFolders.ToList());
Flags |= ImGuiWindowFlags.NoDocking;
@@ -401,7 +402,7 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGui.TextUnformatted("No uploads in progress");
}
var currentDownloads = _currentDownloads.SelectMany(d => d.Value.Values).ToList();
var currentDownloads = BuildCurrentDownloadSnapshot();
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(FontAwesomeIcon.Download);
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
@@ -428,10 +429,53 @@ public class CompactUi : WindowMediatorSubscriberBase
}
}
private List<FileDownloadStatus> BuildCurrentDownloadSnapshot()
{
List<FileDownloadStatus> snapshot = new();
foreach (var kvp in _currentDownloads.ToArray())
{
var value = kvp.Value;
if (value == null || value.Count == 0)
continue;
try
{
snapshot.AddRange(value.Values.ToArray());
}
catch (System.ArgumentException)
{
// skibidi
}
}
return snapshot;
}
private void DrawUIDHeader()
{
var uidText = GetUidText();
Vector4? vanityTextColor = null;
Vector4? vanityGlowColor = null;
bool useVanityColors = false;
if (_configService.Current.useColoredUIDs && _apiController.HasVanity)
{
if (!string.IsNullOrWhiteSpace(_apiController.TextColorHex))
{
vanityTextColor = UIColors.HexToRgba(_apiController.TextColorHex);
}
if (!string.IsNullOrWhiteSpace(_apiController.TextGlowColorHex))
{
vanityGlowColor = UIColors.HexToRgba(_apiController.TextGlowColorHex);
}
useVanityColors = vanityTextColor is not null || vanityGlowColor is not null;
}
//Getting information of character and triangles threshold to show overlimit status in UID bar.
_cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone();
@@ -446,7 +490,7 @@ public class CompactUi : WindowMediatorSubscriberBase
float uidStartX = (contentWidth - uidTextSize.X) / 2f;
float cursorY = ImGui.GetCursorPosY();
if (_configService.Current.BroadcastEnabled)
if (_configService.Current.BroadcastEnabled && _apiController.IsConnected)
{
float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f;
var buttonSize = new Vector2(iconSize.X, uidTextSize.Y);
@@ -467,14 +511,8 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGui.Text("Lightfinder");
ImGui.PopStyleColor();
ImGui.Text("This lets other Lightless users know you use Lightless.");
ImGui.Text("By enabling this, the server will allow other people to see that you are using Lightless.");
ImGui.Text("When disabled, pairing is still possible but both parties need to mutually send each other requests, receiving party will not be notified about the request unless the pairing is complete.");
ImGui.Text("At no point ever, even when Lightfinder is active that any Lightless data is getting sent to other people (including ID's), the server keeps this to itself.");
ImGui.Text("You can request to pair by right-clicking any (not yourself) character and using 'Send Pair Request'.");
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.Text("Use it only when you want to be visible.");
ImGui.TextWrapped("Use Lightfinder when you're okay with being visible to other users and understand that you are responsible for your own experience.");
ImGui.PopStyleColor();
ImGuiHelpers.ScaledDummy(0.2f);
@@ -524,12 +562,30 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGui.SetCursorPosY(cursorY);
ImGui.SetCursorPosX(uidStartX);
bool headerItemClicked;
using (_uiSharedService.UidFont.Push())
{
ImGui.TextColored(GetUidColor(), uidText);
if (ImGui.IsItemClicked())
ImGui.SetClipboardText(uidText);
if (useVanityColors)
{
var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor);
var cursorPos = ImGui.GetCursorScreenPos();
var fontPtr = ImGui.GetFont();
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr);
}
else
{
ImGui.TextColored(GetUidColor(), uidText);
}
}
headerItemClicked = ImGui.IsItemClicked();
if (headerItemClicked)
{
ImGui.SetClipboardText(uidText);
}
UiSharedService.AttachToolTip("Click to copy");
if (_cachedAnalysis != null && _apiController.ServerState is ServerState.Connected)
@@ -561,6 +617,7 @@ public class CompactUi : WindowMediatorSubscriberBase
if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds)
{
ImGui.SameLine();
ImGui.SetCursorPosY(cursorY + 15f);
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
string warningMessage = "";
@@ -588,7 +645,7 @@ public class CompactUi : WindowMediatorSubscriberBase
if (_apiController.ServerState is ServerState.Connected)
{
if (ImGui.IsItemClicked())
if (headerItemClicked)
{
ImGui.SetClipboardText(_apiController.DisplayName);
}
@@ -597,9 +654,22 @@ public class CompactUi : WindowMediatorSubscriberBase
{
var origTextSize = ImGui.CalcTextSize(_apiController.UID);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (origTextSize.X / 2));
ImGui.TextColored(GetUidColor(), _apiController.UID);
if (useVanityColors)
{
var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.UID, vanityTextColor, vanityGlowColor);
var cursorPos = ImGui.GetCursorScreenPos();
var fontPtr = ImGui.GetFont();
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr);
}
else
{
ImGui.TextColored(GetUidColor(), _apiController.UID);
}
bool uidFooterClicked = ImGui.IsItemClicked();
UiSharedService.AttachToolTip("Click to copy");
if (ImGui.IsItemClicked())
if (uidFooterClicked)
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
}
@@ -611,168 +681,166 @@ public class CompactUi : WindowMediatorSubscriberBase
}
}
private IEnumerable<IDrawFolder> GetDrawFolders()
private IEnumerable<IDrawFolder> DrawFolders
{
List<IDrawFolder> drawFolders = [];
var allPairs = _pairManager.PairsWithGroups
.ToDictionary(k => k.Key, k => k.Value);
var filteredPairs = allPairs
.Where(p =>
{
if (_tabMenu.Filter.IsNullOrEmpty()) return true;
return p.Key.UserData.AliasOrUID.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ||
(p.Key.GetNote()?.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ?? false) ||
(p.Key.PlayerName?.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ?? false);
})
.ToDictionary(k => k.Key, k => k.Value);
string? AlphabeticalSort(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(u.Key.PlayerName)
? (_configService.Current.PreferNotesOverNamesForVisible ? u.Key.GetNote() : u.Key.PlayerName)
: (u.Key.GetNote() ?? u.Key.UserData.AliasOrUID));
bool FilterOnlineOrPausedSelf(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> (u.Key.IsOnline || (!u.Key.IsOnline && !_configService.Current.ShowOfflineUsersSeparately)
|| u.Key.UserPair.OwnPermissions.IsPaused());
Dictionary<Pair, List<GroupFullInfoDto>> BasicSortedDictionary(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> u)
=> u.OrderByDescending(u => u.Key.IsVisible)
.ThenByDescending(u => u.Key.IsOnline)
.ThenBy(AlphabeticalSort, StringComparer.OrdinalIgnoreCase)
.ToDictionary(u => u.Key, u => u.Value);
ImmutableList<Pair> ImmutablePairList(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> u)
=> u.Select(k => k.Key).ToImmutableList();
bool FilterVisibleUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> u.Key.IsVisible
&& (_configService.Current.ShowSyncshellUsersInVisible || !(!_configService.Current.ShowSyncshellUsersInVisible && !u.Key.IsDirectlyPaired));
bool FilterTagUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u, string tag)
=> u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && _tagHandler.HasPairTag(u.Key.UserData.UID, tag);
bool FilterGroupUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u, GroupFullInfoDto group)
=> u.Value.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal));
bool FilterNotTaggedUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && !_tagHandler.HasAnyPairTag(u.Key.UserData.UID);
bool FilterNotTaggedSyncshells(GroupFullInfoDto group)
=> (!_tagHandler.HasAnySyncshellTag(group.GID) && !_configService.Current.ShowGroupedSyncshellsInAll) || true;
bool FilterOfflineUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> ((u.Key.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately)
|| !_configService.Current.ShowSyncshellOfflineUsersSeparately)
&& (!u.Key.IsOneSidedPair || u.Value.Any()) && !u.Key.IsOnline && !u.Key.UserPair.OwnPermissions.IsPaused();
bool FilterOfflineSyncshellUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> (!u.Key.IsDirectlyPaired && !u.Key.IsOnline && !u.Key.UserPair.OwnPermissions.IsPaused());
if (_configService.Current.ShowVisibleUsersSeparately)
get
{
var allVisiblePairs = ImmutablePairList(allPairs
.Where(FilterVisibleUsers));
var filteredVisiblePairs = BasicSortedDictionary(filteredPairs
.Where(FilterVisibleUsers));
var drawFolders = new List<IDrawFolder>();
var filter = _tabMenu.Filter;
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs));
}
var allPairs = _pairManager.PairsWithGroups.ToDictionary(k => k.Key, k => k.Value);
var filteredPairs = allPairs.Where(p => PassesFilter(p.Key, filter)).ToDictionary(k => k.Key, k => k.Value);
List<IDrawFolder> groupFolders = new();
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
{
GetGroups(allPairs, filteredPairs, group, out ImmutableList<Pair> allGroupPairs, out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
if (FilterNotTaggedSyncshells(group))
//Filter of online/visible pairs
if (_configService.Current.ShowVisibleUsersSeparately)
{
groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs));
var allVisiblePairs = ImmutablePairList(allPairs.Where(p => FilterVisibleUsers(p.Key)));
var filteredVisiblePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterVisibleUsers(p.Key)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs));
}
}
if (_configService.Current.GroupUpSyncshells)
drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, ""));
else
drawFolders.AddRange(groupFolders);
var tags = _tagHandler.GetAllPairTagsSorted();
foreach (var tag in tags)
{
var allTagPairs = ImmutablePairList(allPairs
.Where(u => FilterTagUsers(u, tag)));
var filteredTagPairs = BasicSortedDictionary(filteredPairs
.Where(u => FilterTagUsers(u, tag) && FilterOnlineOrPausedSelf(u)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(tag, filteredTagPairs, allTagPairs));
}
var syncshellTags = _tagHandler.GetAllSyncshellTagsSorted();
foreach (var syncshelltag in syncshellTags)
{
List<IDrawFolder> syncshellFolderTags = [];
//Filter of not foldered syncshells
var groupFolders = new List<IDrawFolder>();
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
{
if (_tagHandler.HasSyncshellTag(group.GID, syncshelltag))
GetGroups(allPairs, filteredPairs, group, out ImmutableList<Pair> allGroupPairs, out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
if (FilterNotTaggedSyncshells(group))
{
GetGroups(allPairs, filteredPairs, group, out ImmutableList<Pair> allGroupPairs, out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
syncshellFolderTags.Add(_drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs));
groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs));
}
}
if (syncshellFolderTags.Count > 0)
//Filter of grouped up syncshells (All Syncshells Folder)
if (_configService.Current.GroupUpSyncshells)
drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler, _uiSharedService,
_selectSyncshellForTagUi, _renameSyncshellTagUi, ""));
else
drawFolders.AddRange(groupFolders);
//Filter of grouped/foldered pairs
foreach (var tag in _tagHandler.GetAllPairTagsSorted())
{
drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshelltag));
var allTagPairs = ImmutablePairList(allPairs.Where(p => FilterTagUsers(p.Key, tag)));
var filteredTagPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterTagUsers(p.Key, tag) && FilterOnlineOrPausedSelf(p.Key)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(tag, filteredTagPairs, allTagPairs));
}
}
var allOnlineNotTaggedPairs = ImmutablePairList(allPairs
.Where(FilterNotTaggedUsers));
var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs
.Where(u => FilterNotTaggedUsers(u) && FilterOnlineOrPausedSelf(u)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag),
onlineNotTaggedPairs, allOnlineNotTaggedPairs));
if (_configService.Current.ShowOfflineUsersSeparately)
{
var allOfflinePairs = ImmutablePairList(allPairs
.Where(FilterOfflineUsers));
var filteredOfflinePairs = BasicSortedDictionary(filteredPairs
.Where(FilterOfflineUsers));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs));
if (_configService.Current.ShowSyncshellOfflineUsersSeparately)
//Filter of grouped/foldered syncshells
foreach (var syncshellTag in _tagHandler.GetAllSyncshellTagsSorted())
{
var allOfflineSyncshellUsers = ImmutablePairList(allPairs
.Where(FilterOfflineSyncshellUsers));
var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs
.Where(FilterOfflineSyncshellUsers));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag,
filteredOfflineSyncshellUsers,
allOfflineSyncshellUsers));
}
}
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag,
BasicSortedDictionary(filteredPairs.Where(u => u.Key.IsOneSidedPair)),
ImmutablePairList(allPairs.Where(u => u.Key.IsOneSidedPair))));
return drawFolders;
void GetGroups(Dictionary<Pair, List<GroupFullInfoDto>> allPairs, Dictionary<Pair, List<GroupFullInfoDto>> filteredPairs, GroupFullInfoDto group, out ImmutableList<Pair> allGroupPairs, out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs)
{
allGroupPairs = ImmutablePairList(allPairs
.Where(u => FilterGroupUsers(u, group)));
filteredGroupPairs = filteredPairs
.Where(u => FilterGroupUsers(u, group) && FilterOnlineOrPausedSelf(u))
.OrderByDescending(u => u.Key.IsOnline)
.ThenBy(u =>
var syncshellFolderTags = new List<IDrawFolder>();
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
{
if (string.Equals(u.Key.UserData.UID, group.OwnerUID, StringComparison.Ordinal)) return 0;
if (group.GroupPairUserInfos.TryGetValue(u.Key.UserData.UID, out var info))
if (_tagHandler.HasSyncshellTag(group.GID, syncshellTag))
{
if (info.IsModerator()) return 1;
if (info.IsPinned()) return 2;
GetGroups(allPairs, filteredPairs, group,
out ImmutableList<Pair> allGroupPairs,
out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
syncshellFolderTags.Add(_drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs));
}
return u.Key.IsVisible ? 3 : 4;
})
.ThenBy(AlphabeticalSort, StringComparer.OrdinalIgnoreCase)
.ToDictionary(k => k.Key, k => k.Value);
}
drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshellTag));
}
//Filter of not grouped/foldered and offline pairs
var allOnlineNotTaggedPairs = ImmutablePairList(allPairs.Where(p => FilterNotTaggedUsers(p.Key)));
var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterNotTaggedUsers(p.Key) && FilterOnlineOrPausedSelf(p.Key)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag), onlineNotTaggedPairs, allOnlineNotTaggedPairs));
if (_configService.Current.ShowOfflineUsersSeparately)
{
var allOfflinePairs = ImmutablePairList(allPairs.Where(p => FilterOfflineUsers(p.Key, p.Value)));
var filteredOfflinePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineUsers(p.Key, p.Value)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs));
if (_configService.Current.ShowSyncshellOfflineUsersSeparately)
{
var allOfflineSyncshellUsers = ImmutablePairList(allPairs.Where(p => FilterOfflineSyncshellUsers(p.Key)));
var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineSyncshellUsers(p.Key)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag, filteredOfflineSyncshellUsers, allOfflineSyncshellUsers));
}
}
//Unpaired
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag,
BasicSortedDictionary(filteredPairs.Where(p => p.Key.IsOneSidedPair)),
ImmutablePairList(allPairs.Where(p => p.Key.IsOneSidedPair))));
return drawFolders;
}
}
private static bool PassesFilter(Pair pair, string filter)
{
if (string.IsNullOrEmpty(filter)) return true;
return pair.UserData.AliasOrUID.Contains(filter, StringComparison.OrdinalIgnoreCase) || (pair.GetNote()?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false) || (pair.PlayerName?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false);
}
private string AlphabeticalSortKey(Pair pair)
{
if (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(pair.PlayerName))
{
return _configService.Current.PreferNotesOverNamesForVisible ? (pair.GetNote() ?? string.Empty) : pair.PlayerName;
}
return pair.GetNote() ?? pair.UserData.AliasOrUID;
}
private bool FilterOnlineOrPausedSelf(Pair pair) => pair.IsOnline || (!pair.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) || pair.UserPair.OwnPermissions.IsPaused();
private bool FilterVisibleUsers(Pair pair) => pair.IsVisible && (_configService.Current.ShowSyncshellUsersInVisible || pair.IsDirectlyPaired);
private bool FilterTagUsers(Pair pair, string tag) => pair.IsDirectlyPaired && !pair.IsOneSidedPair && _tagHandler.HasPairTag(pair.UserData.UID, tag);
private static bool FilterGroupUsers(List<GroupFullInfoDto> groups, GroupFullInfoDto group) => groups.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal));
private bool FilterNotTaggedUsers(Pair pair) => pair.IsDirectlyPaired && !pair.IsOneSidedPair && !_tagHandler.HasAnyPairTag(pair.UserData.UID);
private bool FilterNotTaggedSyncshells(GroupFullInfoDto group) => !_tagHandler.HasAnySyncshellTag(group.GID) || _configService.Current.ShowGroupedSyncshellsInAll;
private bool FilterOfflineUsers(Pair pair, List<GroupFullInfoDto> groups) => ((pair.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately) || !_configService.Current.ShowSyncshellOfflineUsersSeparately) && (!pair.IsOneSidedPair || groups.Count != 0) && !pair.IsOnline && !pair.UserPair.OwnPermissions.IsPaused();
private static bool FilterOfflineSyncshellUsers(Pair pair) => !pair.IsDirectlyPaired && !pair.IsOnline && !pair.UserPair.OwnPermissions.IsPaused();
private Dictionary<Pair, List<GroupFullInfoDto>> BasicSortedDictionary(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> pairs) => pairs.OrderByDescending(u => u.Key.IsVisible).ThenByDescending(u => u.Key.IsOnline).ThenBy(u => AlphabeticalSortKey(u.Key), StringComparer.OrdinalIgnoreCase).ToDictionary(u => u.Key, u => u.Value);
private static ImmutableList<Pair> ImmutablePairList(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> pairs) => [.. pairs.Select(k => k.Key)];
private void GetGroups(Dictionary<Pair, List<GroupFullInfoDto>> allPairs,
Dictionary<Pair, List<GroupFullInfoDto>> filteredPairs,
GroupFullInfoDto group,
out ImmutableList<Pair> allGroupPairs,
out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs)
{
allGroupPairs = ImmutablePairList(allPairs
.Where(u => FilterGroupUsers(u.Value, group)));
filteredGroupPairs = filteredPairs
.Where(u => FilterGroupUsers( u.Value, group) && FilterOnlineOrPausedSelf(u.Key))
.OrderByDescending(u => u.Key.IsOnline)
.ThenBy(u =>
{
if (string.Equals(u.Key.UserData.UID, group.OwnerUID, StringComparison.Ordinal)) return 0;
if (group.GroupPairUserInfos.TryGetValue(u.Key.UserData.UID, out var info))
{
if (info.IsModerator()) return 1;
if (info.IsPinned()) return 2;
}
return u.Key.IsVisible ? 3 : 4;
})
.ThenBy(u => AlphabeticalSortKey(u.Key), StringComparer.OrdinalIgnoreCase)
.ToDictionary(k => k.Key, k => k.Value);
}
private string GetServerError()
{
return _apiController.ServerState switch

View File

@@ -34,8 +34,6 @@ public class DrawGroupedGroupFolder : IDrawFolder
public void Draw()
{
if (!_groups.Any()) return;
string _id = "__folder_syncshells";
if (_tag != "")
{

View File

@@ -1,151 +0,0 @@
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using LightlessSync.Services;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace LightlessSync.UI;
internal class ContextMenu : IHostedService
{
private readonly IContextMenu _contextMenu;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IDataManager _gameData;
private readonly ILogger<ContextMenu> _logger;
private readonly DalamudUtilService _dalamudUtil;
private readonly ApiController _apiController;
private readonly IObjectTable _objectTable;
private static readonly string[] ValidAddons = new[]
{
null,
"PartyMemberList", "FriendList", "FreeCompany", "LinkShell", "CrossWorldLinkshell",
"_PartyList", "ChatLog", "LookingForGroup", "BlackList", "ContentMemberList",
"SocialList", "ContactList", "BeginnerChatList", "MuteList"
};
public ContextMenu(
IContextMenu contextMenu,
IDalamudPluginInterface pluginInterface,
IDataManager gameData,
ILogger<ContextMenu> logger,
DalamudUtilService dalamudUtil,
ApiController apiController,
IObjectTable objectTable)
{
_contextMenu = contextMenu;
_pluginInterface = pluginInterface;
_gameData = gameData;
_logger = logger;
_dalamudUtil = dalamudUtil;
_apiController = apiController;
_objectTable = objectTable;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_contextMenu.OnMenuOpened += OnMenuOpened;
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_contextMenu.OnMenuOpened -= OnMenuOpened;
return Task.CompletedTask;
}
public void Enable()
{
_contextMenu.OnMenuOpened += OnMenuOpened;
_logger.LogDebug("Context menu enabled.");
}
public void Disable()
{
_contextMenu.OnMenuOpened -= OnMenuOpened;
_logger.LogDebug("Context menu disabled.");
}
private void OnMenuOpened(IMenuOpenedArgs args)
{
if (!_pluginInterface.UiBuilder.ShouldModifyUi)
return;
if (!ValidAddons.Contains(args.AddonName))
return;
if (args.Target is not MenuTargetDefault target)
return;
if (string.IsNullOrEmpty(target.TargetName) || target.TargetObjectId == 0 || target.TargetHomeWorld.RowId == 0)
return;
var world = GetWorld(target.TargetHomeWorld.RowId);
if (!IsWorldValid(world))
return;
args.AddMenuItem(new MenuItem
{
Name = "Send Pair Request",
PrefixChar = 'L',
UseDefaultPrefix = false,
PrefixColor = 708,
OnClicked = async _ => await HandleSelection(args)
});
}
private async Task HandleSelection(IMenuArgs args)
{
if (args.Target is not MenuTargetDefault target)
return;
var world = GetWorld(target.TargetHomeWorld.RowId);
if (!IsWorldValid(world))
return;
try
{
var targetData = _objectTable
.OfType<IPlayerCharacter>()
.FirstOrDefault(p =>
string.Equals(p.Name.TextValue, target.TargetName, StringComparison.OrdinalIgnoreCase) &&
p.HomeWorld.RowId == target.TargetHomeWorld.RowId);
if (targetData == null || targetData.Address == IntPtr.Zero)
{
_logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name);
return;
}
var senderCid = (await _dalamudUtil.GetCIDAsync()).ToString().GetHash256();
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
await _apiController.TryPairWithContentId(receiverCid, senderCid);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending pair request.");
}
}
private World GetWorld(uint worldId)
{
var sheet = _gameData.GetExcelSheet<World>()!;
return sheet.TryGetRow(worldId, out var world) ? world : sheet.First();
}
public bool IsWorldValid(uint worldId) => IsWorldValid(GetWorld(worldId));
public static bool IsWorldValid(World world)
{
var name = world.Name.ToString();
return !string.IsNullOrWhiteSpace(name) && char.IsUpper(name[0]);
}
}

View File

@@ -547,73 +547,147 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString());
if (tab.Success)
{
var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal)
.OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal).OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
ImGui.TextUnformatted("Files for " + kvp.Key);
ImGui.SameLine();
ImGui.TextUnformatted(kvp.Value.Count.ToString());
ImGui.SameLine();
ImGui.PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(1f, 1f));
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1f, 1f));
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
if (ImGui.BeginTable($"##fileStats_{kvp.Key}", 3,
ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingFixedFit))
{
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
}
if (ImGui.IsItemHovered())
{
string text = "";
text = string.Join(Environment.NewLine, groupedfiles
.Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))
+ ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))));
ImGui.SetTooltip(text);
}
ImGui.TextUnformatted($"{kvp.Key} size (actual):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize)));
ImGui.TextUnformatted($"{kvp.Key} size (compressed for up/download only):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize)));
ImGui.Separator();
var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
if (vramUsage != null)
{
var actualVramUsage = vramUsage.Sum(f => f.OriginalSize);
ImGui.TextUnformatted($"{kvp.Key} VRAM usage:");
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"Files for {kvp.Key}");
ImGui.TableNextColumn();
ImGui.TextUnformatted(kvp.Value.Count.ToString());
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(actualVramUsage));
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
if (ImGui.IsItemHovered())
{
string text = string.Join(Environment.NewLine, groupedfiles.Select(f =>
$"{f.Key}: {f.Count()} files, size: {UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))}, compressed: {UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))}"));
ImGui.SetTooltip(text);
}
ImGui.TableNextColumn();
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{kvp.Key} size (actual):");
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize)));
ImGui.TableNextColumn();
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{kvp.Key} size (compressed for up/download only):");
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize)));
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TableNextColumn();
var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
if (vramUsage != null)
{
var actualVramUsage = vramUsage.Sum(f => f.OriginalSize);
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{kvp.Key} VRAM usage:");
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(actualVramUsage));
ImGui.TableNextColumn();
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
{
var currentVramWarning = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB;
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted("Configured VRAM threshold:");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{currentVramWarning} MiB.");
ImGui.TableNextColumn();
if (currentVramWarning * 1024 * 1024 < actualVramUsage)
{
UiSharedService.ColorText(
$"You exceed your own threshold by {UiSharedService.ByteToString(actualVramUsage - (currentVramWarning * 1024 * 1024))}",
UIColors.Get("LightlessYellow"));
}
}
}
var actualTriCount = kvp.Value.Sum(f => f.Value.Triangles);
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{kvp.Key} modded model triangles:");
ImGui.TableNextColumn();
ImGui.TextUnformatted(actualTriCount.ToString());
ImGui.TableNextColumn();
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
{
using var _ = ImRaii.PushIndent(10f);
var currentVramWarning = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB;
ImGui.TextUnformatted($"Configured VRAM warning threshold: {currentVramWarning} MiB.");
if (currentVramWarning * 1024 * 1024 < actualVramUsage)
var currentTriWarning = _playerPerformanceConfig.Current.TrisWarningThresholdThousands;
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted("Configured triangle threshold:");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{currentTriWarning * 1000} triangles.");
ImGui.TableNextColumn();
if (currentTriWarning * 1000 < actualTriCount)
{
UiSharedService.ColorText($"You exceed your own threshold by " +
$"{UiSharedService.ByteToString(actualVramUsage - (currentVramWarning * 1024 * 1024))}.",
UiSharedService.ColorText(
$"You exceed your own threshold by {actualTriCount - (currentTriWarning * 1000)}",
UIColors.Get("LightlessYellow"));
}
}
ImGui.EndTable();
}
var actualTriCount = kvp.Value.Sum(f => f.Value.Triangles);
ImGui.TextUnformatted($"{kvp.Key} modded model triangles: {actualTriCount}");
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
ImGui.PopStyleVar(2);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
_uiSharedService.MediumText("Selected file:", UIColors.Get("LightlessBlue"));
ImGui.SameLine();
_uiSharedService.MediumText(_selectedHash, UIColors.Get("LightlessYellow"));
if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item))
{
using var _ = ImRaii.PushIndent(10f);
var currentTriWarning = _playerPerformanceConfig.Current.TrisWarningThresholdThousands;
ImGui.TextUnformatted($"Configured triangle warning threshold: {currentTriWarning * 1000} triangles.");
if (currentTriWarning * 1000 < actualTriCount)
var filePaths = item.FilePaths;
UiSharedService.ColorText("Local file path:", UIColors.Get("LightlessBlue"));
ImGui.SameLine();
UiSharedService.TextWrapped(filePaths[0]);
if (filePaths.Count > 1)
{
UiSharedService.ColorText($"You exceed your own threshold by " +
$"{actualTriCount - (currentTriWarning * 1000)} triangles.",
UIColors.Get("LightlessYellow"));
ImGui.SameLine();
ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1)));
}
var gamepaths = item.GamePaths;
UiSharedService.ColorText("Used by game path:", UIColors.Get("LightlessBlue"));
ImGui.SameLine();
UiSharedService.TextWrapped(gamepaths[0]);
if (gamepaths.Count > 1)
{
ImGui.SameLine();
ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1)));
}
}
ImGui.Separator();
if (_selectedObjectTab != kvp.Key)
{
_selectedHash = string.Empty;
@@ -692,41 +766,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
}
}
}
ImGui.Separator();
ImGui.TextUnformatted("Selected file:");
ImGui.SameLine();
UiSharedService.ColorText(_selectedHash, UIColors.Get("LightlessYellow"));
if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item))
{
var filePaths = item.FilePaths;
ImGui.TextUnformatted("Local file path:");
ImGui.SameLine();
UiSharedService.TextWrapped(filePaths[0]);
if (filePaths.Count > 1)
{
ImGui.SameLine();
ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1)));
}
var gamepaths = item.GamePaths;
ImGui.TextUnformatted("Used by game path:");
ImGui.SameLine();
UiSharedService.TextWrapped(gamepaths[0]);
if (gamepaths.Count > 1)
{
ImGui.SameLine();
ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1)));
}
}
}
public override void OnOpen()
@@ -855,7 +894,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
}
var filePath = item.FilePaths[0];
bool toConvert = _texturesToConvert.ContainsKey(filePath);
if (ImGui.Checkbox("###convert" + item.Hash, ref toConvert))
if (UiSharedService.CheckboxWithBorder("###convert" + item.Hash, ref toConvert, UIColors.Get("LightlessPurple"), 1.5f))
{
if (toConvert && !_texturesToConvert.ContainsKey(filePath))
{

View File

@@ -4,14 +4,18 @@ using Dalamud.Interface.Colors;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data;
using LightlessSync.API.Dto.User;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Style;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System.Numerics;
namespace LightlessSync.UI;
@@ -30,6 +34,16 @@ public class EditProfileUi : WindowMediatorSubscriberBase
private bool _showFileDialogError = false;
private bool _wasOpen;
private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f);
private bool vanityInitialized; // useless for now
private bool textEnabled;
private bool glowEnabled;
private Vector4 textColor;
private Vector4 glowColor;
private record VanityState(bool TextEnabled, bool GlowEnabled, Vector4 TextColor, Vector4 GlowColor);
private VanityState _savedVanity;
public EditProfileUi(ILogger<EditProfileUi> logger, LightlessMediator mediator,
ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager,
LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService)
@@ -38,8 +52,8 @@ public class EditProfileUi : WindowMediatorSubscriberBase
IsOpen = false;
this.SizeConstraints = new()
{
MinimumSize = new(768, 512),
MaximumSize = new(768, 2000)
MinimumSize = new(850, 640),
MaximumSize = new(850, 700)
};
_apiController = apiController;
_uiSharedService = uiSharedService;
@@ -57,172 +71,320 @@ public class EditProfileUi : WindowMediatorSubscriberBase
_pfpTextureWrap = null;
}
});
Mediator.Subscribe<ConnectedMessage>(this, msg =>
{
LoadVanity();
});
}
private void LoadVanity()
{
textEnabled = !string.IsNullOrEmpty(_apiController.TextColorHex);
glowEnabled = !string.IsNullOrEmpty(_apiController.TextGlowColorHex);
textColor = textEnabled ? UIColors.HexToRgba(_apiController.TextColorHex!) : Vector4.One;
glowColor = glowEnabled ? UIColors.HexToRgba(_apiController.TextGlowColorHex!) : Vector4.Zero;
_savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor);
vanityInitialized = true;
}
protected override void DrawInternal()
{
_uiSharedService.BigText("Current Profile (as saved on server)");
_uiSharedService.UnderlinedBigText("Notes and Rules for Profiles", UIColors.Get("LightlessYellow"));
ImGui.Dummy(new Vector2(5));
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, 1));
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "All users that are paired and unpaused with you will be able to see your profile picture and description.");
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Other users have the possibility to report your profile for breaking the rules.");
_uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.)");
_uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Slurs of any kind in the description that can be considered highly offensive");
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "In case of valid reports from other users this can lead to disabling your profile forever or terminating your Lightless account indefinitely.");
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Judgement of your profile validity from reports through staff is not up to debate and the decisions to disable your profile/account permanent.");
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessBlue"), "If your profile picture or profile description could be considered NSFW, enable the toggle in profile settings.");
ImGui.PopStyleVar();
ImGui.Dummy(new Vector2(3));
var profile = _lightlessProfileManager.GetLightlessProfile(new UserData(_apiController.UID));
if (profile.IsFlagged)
{
UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed);
return;
}
if (!_profileImage.SequenceEqual(profile.ImageData.Value))
{
_profileImage = profile.ImageData.Value;
_pfpTextureWrap?.Dispose();
_pfpTextureWrap = _uiSharedService.LoadImage(_profileImage);
}
if (!string.Equals(_profileDescription, profile.Description, StringComparison.OrdinalIgnoreCase))
{
_profileDescription = profile.Description;
_descriptionText = _profileDescription;
}
if (_pfpTextureWrap != null)
{
ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height));
}
var spacing = ImGui.GetStyle().ItemSpacing.X;
ImGuiHelpers.ScaledRelativeSameLine(256, spacing);
using (_uiSharedService.GameFont.Push())
{
var descriptionTextSize = ImGui.CalcTextSize(profile.Description, wrapWidth: 256f);
var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256);
if (descriptionTextSize.Y > childFrame.Y)
if (ImGui.BeginTabBar("##EditProfileTabs"))
{
if (ImGui.BeginTabItem("Current Profile"))
{
_adjustedForScollBarsOnlineProfile = true;
}
else
{
_adjustedForScollBarsOnlineProfile = false;
}
childFrame = childFrame with
{
X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0),
};
if (ImGui.BeginChildFrame(101, childFrame))
{
UiSharedService.TextWrapped(profile.Description);
}
ImGui.EndChildFrame();
}
_uiSharedService.MediumText("Current Profile (as saved on server)", UIColors.Get("LightlessPurple"));
ImGui.Dummy(new Vector2(5));
var nsfw = profile.IsNSFW;
ImGui.BeginDisabled();
ImGui.Checkbox("Is NSFW", ref nsfw);
ImGui.EndDisabled();
ImGui.Separator();
_uiSharedService.BigText("Notes and Rules for Profiles");
ImGui.TextWrapped($"- All users that are paired and unpaused with you will be able to see your profile picture and description.{Environment.NewLine}" +
$"- Other users have the possibility to report your profile for breaking the rules.{Environment.NewLine}" +
$"- !!! AVOID: anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.){Environment.NewLine}" +
$"- !!! AVOID: slurs of any kind in the description that can be considered highly offensive{Environment.NewLine}" +
$"- In case of valid reports from other users this can lead to disabling your profile forever or terminating your Lightless account indefinitely.{Environment.NewLine}" +
$"- Judgement of your profile validity from reports through staff is not up to debate and the decisions to disable your profile/account permanent.{Environment.NewLine}" +
$"- If your profile picture or profile description could be considered NSFW, enable the toggle below.");
ImGui.Separator();
_uiSharedService.BigText("Profile Settings");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
{
_fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) =>
{
if (!success) return;
_ = Task.Run(async () =>
if (profile.IsFlagged)
{
var fileContent = File.ReadAllBytes(file);
using MemoryStream ms = new(fileContent);
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed);
return;
}
if (!_profileImage.SequenceEqual(profile.ImageData.Value))
{
_profileImage = profile.ImageData.Value;
_pfpTextureWrap?.Dispose();
_pfpTextureWrap = _uiSharedService.LoadImage(_profileImage);
}
if (!string.Equals(_profileDescription, profile.Description, StringComparison.OrdinalIgnoreCase))
{
_profileDescription = profile.Description;
_descriptionText = _profileDescription;
}
if (_pfpTextureWrap != null)
{
ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height));
}
var spacing = ImGui.GetStyle().ItemSpacing.X;
ImGuiHelpers.ScaledRelativeSameLine(256, spacing);
using (_uiSharedService.GameFont.Push())
{
var descriptionTextSize = ImGui.CalcTextSize(profile.Description, wrapWidth: 256f);
var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256);
if (descriptionTextSize.Y > childFrame.Y)
{
_showFileDialogError = true;
return;
_adjustedForScollBarsOnlineProfile = true;
}
using var image = Image.Load<Rgba32>(fileContent);
if (image.Width > 256 || image.Height > 256 || (fileContent.Length > 250 * 1024))
else
{
_showFileDialogError = true;
return;
_adjustedForScollBarsOnlineProfile = false;
}
childFrame = childFrame with
{
X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0),
};
if (ImGui.BeginChildFrame(101, childFrame))
{
UiSharedService.TextWrapped(profile.Description);
}
ImGui.EndChildFrame();
}
_showFileDialogError = false;
await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null))
.ConfigureAwait(false);
});
});
}
UiSharedService.AttachToolTip("Select and upload a new profile picture");
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture"))
{
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null));
}
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
if (_showFileDialogError)
{
UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed);
}
var isNsfw = profile.IsNSFW;
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
{
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null));
}
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
var widthTextBox = 400;
var posX = ImGui.GetCursorPosX();
ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500");
ImGui.SetCursorPosX(posX);
ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X);
ImGui.TextUnformatted("Preview (approximate)");
using (_uiSharedService.GameFont.Push())
ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200));
var nsfw = profile.IsNSFW;
ImGui.BeginDisabled();
ImGui.Checkbox("Is NSFW", ref nsfw);
ImGui.EndDisabled();
ImGui.SameLine();
using (_uiSharedService.GameFont.Push())
{
var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f);
var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200);
if (descriptionTextSizeLocal.Y > childFrameLocal.Y)
{
_adjustedForScollBarsLocalProfile = true;
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.EndTabItem();
}
else
{
_adjustedForScollBarsLocalProfile = false;
}
childFrameLocal = childFrameLocal with
{
X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0),
};
if (ImGui.BeginChildFrame(102, childFrameLocal))
{
UiSharedService.TextWrapped(_descriptionText);
}
ImGui.EndChildFrame();
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
{
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText));
if (ImGui.BeginTabItem("Profile Settings"))
{
_uiSharedService.MediumText("Profile Settings", UIColors.Get("LightlessPurple"));
ImGui.Dummy(new Vector2(5));
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
{
_fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) =>
{
if (!success) return;
_ = Task.Run(async () =>
{
var fileContent = File.ReadAllBytes(file);
using MemoryStream ms = new(fileContent);
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
{
_showFileDialogError = true;
return;
}
using var image = Image.Load<Rgba32>(fileContent);
if (image.Width > 256 || image.Height > 256 || (fileContent.Length > 250 * 1024))
{
_showFileDialogError = true;
return;
}
_showFileDialogError = false;
await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null))
.ConfigureAwait(false);
});
});
}
UiSharedService.AttachToolTip("Select and upload a new profile picture");
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture"))
{
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null));
}
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
if (_showFileDialogError)
{
UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed);
}
var isNsfw = profile.IsNSFW;
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
{
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null));
}
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
var widthTextBox = 400;
var posX = ImGui.GetCursorPosX();
ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500");
ImGui.SetCursorPosX(posX);
ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X);
ImGui.TextUnformatted("Preview (approximate)");
using (_uiSharedService.GameFont.Push())
ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200));
ImGui.SameLine();
using (_uiSharedService.GameFont.Push())
{
var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f);
var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200);
if (descriptionTextSizeLocal.Y > childFrameLocal.Y)
{
_adjustedForScollBarsLocalProfile = true;
}
else
{
_adjustedForScollBarsLocalProfile = false;
}
childFrameLocal = childFrameLocal with
{
X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0),
};
if (ImGui.BeginChildFrame(102, childFrameLocal))
{
UiSharedService.TextWrapped(_descriptionText);
}
ImGui.EndChildFrame();
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
{
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText));
}
UiSharedService.AttachToolTip("Sets your profile description text");
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
{
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, ""));
}
UiSharedService.AttachToolTip("Clears your profile description text");
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Vanity Settings"))
{
_uiSharedService.MediumText("Supporter Vanity Settings", UIColors.Get("LightlessPurple"));
ImGui.Dummy(new Vector2(4));
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Must be a supporter through Patreon/Ko-fi to access these settings.");
var hasVanity = _apiController.HasVanity;
if (!hasVanity)
{
UiSharedService.ColorTextWrapped("You do not currently have vanity access. Become a supporter to unlock these features.", UIColors.Get("DimRed"));
ImGui.Dummy(new Vector2(8));
ImGui.BeginDisabled();
}
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
_uiSharedService.MediumText("Colored UID", UIColors.Get("LightlessPurple"));
ImGui.Dummy(new Vector2(5));
var font = UiBuilder.MonoFont;
var playerUID = _apiController.UID;
var playerDisplay = _apiController.DisplayName;
var previewTextColor = textEnabled ? textColor : Vector4.One;
var previewGlowColor = glowEnabled ? glowColor : Vector4.Zero;
var seString = SeStringUtils.BuildFormattedPlayerName(playerDisplay, previewTextColor, previewGlowColor);
using (ImRaii.PushFont(font))
{
var drawList = ImGui.GetWindowDrawList();
var textSize = ImGui.CalcTextSize(seString.TextValue);
float minWidth = 150f * ImGuiHelpers.GlobalScale;
float bgWidth = Math.Max(textSize.X + 20f, minWidth);
float paddingY = 5f * ImGuiHelpers.GlobalScale;
var cursor = ImGui.GetCursorScreenPos();
var rectMin = cursor;
var rectMax = rectMin + new Vector2(bgWidth, textSize.Y + (paddingY * 2f));
float boost = Luminance.ComputeHighlight(previewTextColor, previewGlowColor);
var baseBg = new Vector4(0.15f + boost, 0.15f + boost, 0.15f + boost, 1f);
var bgColor = Luminance.BackgroundContrast(previewTextColor, previewGlowColor, baseBg, ref _currentBg);
var borderColor = UIColors.Get("LightlessPurple");
drawList.AddRectFilled(rectMin, rectMax, ImGui.GetColorU32(bgColor), 6.0f);
drawList.AddRect(rectMin, rectMax, ImGui.GetColorU32(borderColor), 6.0f, ImDrawFlags.None, 1.5f);
var textPos = new Vector2(
rectMin.X + (bgWidth - textSize.X) * 0.5f,
rectMin.Y + paddingY
);
SeStringUtils.RenderSeStringWithHitbox(seString, textPos, font);
ImGui.Dummy(new Vector2(5));
}
const float colorPickAlign = 90f;
_uiSharedService.DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Text Color");
ImGui.SameLine(colorPickAlign);
ImGui.Checkbox("##toggleTextColor", ref textEnabled);
ImGui.SameLine();
ImGui.BeginDisabled(!textEnabled);
ImGui.ColorEdit4($"##color_text", ref textColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf);
ImGui.EndDisabled();
_uiSharedService.DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Glow Color");
ImGui.SameLine(colorPickAlign);
ImGui.Checkbox("##toggleGlowColor", ref glowEnabled);
ImGui.SameLine();
ImGui.BeginDisabled(!glowEnabled);
ImGui.ColorEdit4($"##color_glow", ref glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf);
ImGui.EndDisabled();
bool changed = !Equals(_savedVanity, new VanityState(textEnabled, glowEnabled, textColor, glowColor));
if (!changed)
ImGui.BeginDisabled();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Changes"))
{
string? newText = textEnabled ? UIColors.RgbaToHex(textColor) : string.Empty;
string? newGlow = glowEnabled ? UIColors.RgbaToHex(glowColor) : string.Empty;
_ = _apiController.UserUpdateVanityColors(new UserVanityColorsDto(newText, newGlow));
_savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor);
}
if (!changed)
ImGui.EndDisabled();
ImGui.Dummy(new Vector2(5));
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
if (!hasVanity)
ImGui.EndDisabled();
ImGui.EndTabItem();
}
ImGui.EndTabBar();
}
UiSharedService.AttachToolTip("Sets your profile description text");
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
{
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, ""));
}
UiSharedService.AttachToolTip("Clears your profile description text");
}
protected override void Dispose(bool disposing)

View File

@@ -1,12 +1,15 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Dto.Group;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Style;
using LightlessSync.Utils;
using System;
using System.Numerics;
namespace LightlessSync.UI.Handlers;
@@ -24,6 +27,9 @@ public class IdDisplayHandler
private bool _popupShown = false;
private DateTime? _popupTime;
private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f);
private float _highlightBoost;
public IdDisplayHandler(LightlessMediator mediator, ServerConfigurationManager serverManager, LightlessConfigService lightlessConfigService)
{
_mediator = mediator;
@@ -96,31 +102,102 @@ public class IdDisplayHandler
{
ImGui.AlignTextToFramePadding();
var font = UiBuilder.MonoFont;
var font = textIsUid ? UiBuilder.MonoFont : ImGui.GetFont();
var isAdmin = pair.UserData.IsAdmin;
var isModerator = pair.UserData.IsModerator;
Vector4? textColor = null;
Vector4? glowColor = null;
Vector4? textColor = isAdmin
? UIColors.Get("LightlessAdminText")
: isModerator
? UIColors.Get("LightlessModeratorText")
: null;
if (pair.UserData.HasVanity)
{
if (!string.IsNullOrWhiteSpace(pair.UserData.TextColorHex))
{
textColor = UIColors.HexToRgba(pair.UserData.TextColorHex);
}
Vector4? glowColor = isAdmin
? UIColors.Get("LightlessAdminGlow")
: isModerator
? UIColors.Get("LightlessModeratorGlow")
: null;
if (!string.IsNullOrWhiteSpace(pair.UserData.TextGlowColorHex))
{
glowColor = UIColors.HexToRgba(pair.UserData.TextGlowColorHex);
}
}
var seString = (textColor != null || glowColor != null)
var useVanityColors = _lightlessConfigService.Current.useColoredUIDs && (textColor != null || glowColor != null);
var seString = useVanityColors
? SeStringUtils.BuildFormattedPlayerName(playerText, textColor, glowColor)
: SeStringUtils.BuildPlain(playerText);
var rowStart = ImGui.GetCursorScreenPos();
var drawList = ImGui.GetWindowDrawList();
bool useHighlight = false;
float highlightPadX = 0f;
float highlightPadY = 0f;
if (useVanityColors)
{
float boost = Luminance.ComputeHighlight(textColor, glowColor);
if (boost > 0f)
{
var style = ImGui.GetStyle();
useHighlight = true;
highlightPadX = MathF.Max(style.FramePadding.X * 0.6f, 2f * ImGuiHelpers.GlobalScale);
highlightPadY = MathF.Max(style.FramePadding.Y * 0.55f, 1.25f * ImGuiHelpers.GlobalScale);
drawList.ChannelsSplit(2);
drawList.ChannelsSetCurrent(1);
_highlightBoost = boost;
}
else
{
_highlightBoost = 0f;
}
}
Vector2 itemMin;
Vector2 itemMax;
Vector2 textSize;
using (ImRaii.PushFont(font, textIsUid))
{
var pos = ImGui.GetCursorScreenPos();
SeStringUtils.RenderSeStringWithHitbox(seString, pos, font);
SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font);
itemMin = ImGui.GetItemRectMin();
itemMax = ImGui.GetItemRectMax();
//textSize = itemMax - itemMin;
}
if (useHighlight)
{
var style = ImGui.GetStyle();
var frameHeight = ImGui.GetFrameHeight();
var rowTop = rowStart.Y - style.FramePadding.Y;
var rowBottom = rowTop + frameHeight;
var highlightMin = new Vector2(itemMin.X - highlightPadX, rowTop - highlightPadY);
var highlightMax = new Vector2(itemMax.X + highlightPadX, rowBottom + highlightPadY);
var windowPos = ImGui.GetWindowPos();
var contentMin = windowPos + ImGui.GetWindowContentRegionMin();
var contentMax = windowPos + ImGui.GetWindowContentRegionMax();
highlightMin.X = MathF.Max(highlightMin.X, contentMin.X);
highlightMax.X = MathF.Min(highlightMax.X, contentMax.X);
highlightMin.Y = MathF.Max(highlightMin.Y, contentMin.Y);
highlightMax.Y = MathF.Min(highlightMax.Y, contentMax.Y);
var highlightColor = new Vector4(
0.25f + _highlightBoost,
0.25f + _highlightBoost,
0.25f + _highlightBoost,
1f
);
highlightColor = Luminance.BackgroundContrast(textColor, glowColor, highlightColor, ref _currentBg);
float rounding = style.FrameRounding > 0f ? style.FrameRounding : 5f * ImGuiHelpers.GlobalScale;
drawList.ChannelsSetCurrent(0);
drawList.AddRectFilled(highlightMin, highlightMax, ImGui.GetColorU32(highlightColor), rounding);
var borderColor = style.Colors[(int)ImGuiCol.Border];
borderColor.W *= 0.25f;
drawList.AddRect(highlightMin, highlightMax, ImGui.GetColorU32(borderColor), rounding);
drawList.ChannelsMerge();
}
if (ImGui.IsItemHovered())

View File

@@ -167,7 +167,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
}
else
{
UiSharedService.TextWrapped("To not unnecessary download files already present on your computer, Lightless Sync will have to scan your Penumbra mod directory. " +
UiSharedService.TextWrapped("To not unnecessarily download files already present on your computer, Lightless Sync will have to scan your Penumbra mod directory. " +
"Additionally, a local storage folder must be set where Lightless Sync will download other character files to. " +
"Once the storage folder is set and the scan complete, this page will automatically forward to registration at a service.");
UiSharedService.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed.");

View File

@@ -63,7 +63,7 @@ internal class JoinSyncshellUI : WindowMediatorSubscriberBase
"Joining a Syncshell will pair you implicitly with all existing users in the Syncshell." + Environment.NewLine +
"All permissions to all users in the Syncshell will be set to the preferred Syncshell permissions on joining, excluding prior set preferred permissions.");
ImGui.Separator();
ImGui.TextUnformatted("Note: Syncshell ID and Password are case sensitive. MSS- is part of Syncshell IDs, unless using Vanity IDs.");
ImGui.TextUnformatted("Note: Syncshell ID and Password are case sensitive. LLS- is part of Syncshell IDs, unless using Vanity IDs.");
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Syncshell ID");

View File

@@ -1,4 +1,4 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
@@ -978,37 +978,68 @@ public class SettingsUi : WindowMediatorSubscriberBase
var colorNames = new[]
{
("LightlessPurple", "Lightless Purple", "Primary colors"),
("LightlessBlue", "Lightless Blue", "Secondary colors"),
("LightlessYellow", "Lightless Yellow", "Warning colors"),
("PairBlue", "Pair Blue", "Pair UI elements"),
("DimRed", "Dim Red", "Error and offline")
("LightlessPurple", "Primary Purple", "Section titles and dividers"),
("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"),
("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"),
("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"),
("LightlessGreen", "Success Green", "Join buttons and success messages"),
("LightlessYellow", "Warning Yellow", "Warning colors"),
("LightlessYellow2", "Warning Yellow (Alt)", "Warning colors"),
("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"),
("DimRed", "Error Red", "Error and offline colors")
};
foreach (var (colorKey, displayName, description) in colorNames)
if (ImGui.BeginTable("##ColorTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
{
var currentColor = UIColors.Get(colorKey);
var colorToEdit = currentColor;
ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthFixed);
ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40);
ImGui.TableHeadersRow();
ImGui.AlignTextToFramePadding();
if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf))
foreach (var (colorKey, displayName, description) in colorNames)
{
UIColors.Set(colorKey, colorToEdit);
}
ImGui.SameLine();
ImGui.TextUnformatted($"{displayName} - {description}");
if (UIColors.IsCustom(colorKey))
{
ImGui.SameLine();
if (_uiShared.IconTextButton(FontAwesomeIcon.Undo, $"Reset {colorKey}"))
ImGui.TableNextRow();
// color column
ImGui.TableSetColumnIndex(0);
var currentColor = UIColors.Get(colorKey);
var colorToEdit = currentColor;
if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf))
{
UIColors.Reset(colorKey);
UIColors.Set(colorKey, colorToEdit);
}
UiSharedService.AttachToolTip("Reset this color to default");
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(displayName);
// description column
ImGui.TableSetColumnIndex(1);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(description);
// actions column
ImGui.TableSetColumnIndex(2);
using var resetId = ImRaii.PushId($"Reset_{colorKey}");
var availableWidth = ImGui.GetContentRegionAvail().X;
var isCustom = UIColors.IsCustom(colorKey);
using (ImRaii.Disabled(!isCustom))
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0)))
{
UIColors.Reset(colorKey);
}
}
}
UiSharedService.AttachToolTip(isCustom ? "Reset this color to default" : "Color is already at default value");
}
ImGui.EndTable();
}
ImGui.Spacing();
@@ -1020,6 +1051,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Spacing();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Server Info Bar Colors");
if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr))
@@ -1054,12 +1087,16 @@ public class SettingsUi : WindowMediatorSubscriberBase
}
ImGui.Spacing();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Nameplate Colors");
var nameColorsEnabled = _configService.Current.IsNameplateColorsEnabled;
var nameColors = _configService.Current.NameplateColors;
var isFriendOverride = _configService.Current.overrideFriendColor;
var isPartyOverride = _configService.Current.overridePartyColor;
var isFcTagOverride = _configService.Current.overrideFcTagColor;
if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled))
{
@@ -1090,14 +1127,35 @@ public class SettingsUi : WindowMediatorSubscriberBase
_configService.Save();
_nameplateService.RequestRedraw();
}
if (ImGui.Checkbox("Override FC tag color", ref isFcTagOverride))
{
_configService.Current.overrideFcTagColor = isFcTagOverride;
_configService.Save();
_nameplateService.RequestRedraw();
}
}
if (ImGui.Checkbox("Use the complete redesign of the UI for Lightless client.", ref useLightlessRedesign))
ImGui.Spacing();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("UI Theme");
if (ImGui.Checkbox("Use the redesign of the UI for Lightless client", ref useLightlessRedesign))
{
_configService.Current.UseLightlessRedesign = useLightlessRedesign;
_configService.Save();
}
var usePairColoredUIDs = _configService.Current.useColoredUIDs;
if (ImGui.Checkbox("Toggle the colored UID's in pair list", ref usePairColoredUIDs))
{
_configService.Current.useColoredUIDs = usePairColoredUIDs;
_configService.Save();
}
_uiShared.DrawHelpText("This changes the vanity colored UID's in pair list.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
@@ -1152,12 +1210,12 @@ public class SettingsUi : WindowMediatorSubscriberBase
}
_uiShared.DrawHelpText("This will group up all Syncshells in a special 'All Syncshells' folder in the main UI.");
//if (ImGui.Checkbox("Show grouped syncshells in main screen/all syncshells", ref groupedSyncshells))
//{
// _configService.Current.ShowGroupedSyncshellsInAll = groupedSyncshells;
// _configService.Save();
// Mediator.Publish(new RefreshUiMessage());
//}
if (ImGui.Checkbox("Show grouped syncshells in main screen/all syncshells", ref groupedSyncshells))
{
_configService.Current.ShowGroupedSyncshellsInAll = groupedSyncshells;
_configService.Save();
Mediator.Publish(new RefreshUiMessage());
}
_uiShared.DrawHelpText("This will show grouped syncshells in main screen or group 'All Syncshells'.");
if (ImGui.Checkbox("Show player name for visible players", ref showNameInsteadOfNotes))
@@ -2235,4 +2293,4 @@ public class SettingsUi : WindowMediatorSubscriberBase
_wasOpen = IsOpen;
IsOpen = false;
}
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.Numerics;
namespace LightlessSync.UI.Style
{
internal static class Luminance
{
public static float BrightnessThreshold { get; set; } = 0.4f;
public static float HighlightBoostMax { get; set; } = 0.1f;
public static float SmoothFactor { get; set; } = 0.15f;
private static float Brightness(Vector4 color)
=> Math.Max(color.X, Math.Max(color.Y, color.Z));
public static float ComputeHighlight(Vector4? textColor, Vector4? glowColor)
{
float brightnessText = textColor.HasValue ? Brightness(textColor.Value) : 1f;
float brightnessGlow = glowColor.HasValue ? Brightness(glowColor.Value) : 1f;
if (brightnessText >= BrightnessThreshold || brightnessGlow >= BrightnessThreshold)
return 0f;
float deficit = Math.Min(BrightnessThreshold - brightnessText,
BrightnessThreshold - brightnessGlow);
float factor = Math.Clamp(deficit / BrightnessThreshold, 0f, 1f);
factor = MathF.Pow(factor, 2.0f);
return factor * HighlightBoostMax;
}
public static Vector4 BackgroundContrast(Vector4? textColor, Vector4? glowColor, Vector4 backgroundColor, ref Vector4 currentBg)
{
if (!textColor.HasValue && !glowColor.HasValue)
return backgroundColor;
float brightnessText = textColor.HasValue ? Brightness(textColor.Value) : 0f;
float brightnessGlow = glowColor.HasValue ? Brightness(glowColor.Value) : 0f;
float fgBrightness = Math.Max(brightnessText, brightnessGlow);
float bgBrightness = Brightness(backgroundColor);
float diff = Math.Abs(bgBrightness - fgBrightness);
bool shouldBeDark = fgBrightness > 0.5f;
Vector4 targetBg;
if (diff >= BrightnessThreshold)
{
targetBg = backgroundColor;
}
else
{
targetBg = shouldBeDark
? new Vector4(0.05f, 0.05f, 0.05f, backgroundColor.W)
: new Vector4(0.95f, 0.95f, 0.95f, backgroundColor.W);
}
float t = Math.Clamp(SmoothFactor, 0f, 1f);
currentBg = t <= 0f ? targetBg : Vector4.Lerp(currentBg, targetBg, t);
return currentBg;
}
}
}

View File

@@ -336,7 +336,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
}
ImGui.Separator();
if (_uiSharedService.MediumTreeNode("Mass Cleanup", UIColors.Get("LightlessPurple")))
if (_uiSharedService.MediumTreeNode("Mass Cleanup", UIColors.Get("DimRed")))
{
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
{
@@ -348,6 +348,18 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
UiSharedService.AttachToolTip("This will remove all non-pinned, non-moderator users from the Syncshell."
+ UiSharedService.TooltipSeparator + "Hold CTRL to enable this button");
ImGui.SameLine();
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Brush, "Clear Lightfinder Users"))
{
_ = _apiController.GroupClearFinder(new(GroupFullInfo.Group));
}
}
UiSharedService.AttachToolTip("This will remove all users that joined through Lightfinder from the Syncshell."
+ UiSharedService.TooltipSeparator + "Hold CTRL to enable this button");
ImGuiHelpers.ScaledDummy(2f);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(2f);
@@ -410,12 +422,12 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
UiSharedService.TextWrapped($"Syncshell was pruned and {_pruneTask.Result} inactive user(s) have been removed.");
}
}
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
_uiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
if (_uiSharedService.MediumTreeNode("User Bans", UIColors.Get("LightlessPurple")))
if (_uiSharedService.MediumTreeNode("User Bans", UIColors.Get("LightlessYellow")))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server"))
{
@@ -456,7 +468,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
}
ImGui.EndTable();
}
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();

View File

@@ -1,18 +1,18 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto;
using LightlessSync.API.Dto.Group;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using LightlessSync.API.Data.Extensions;
using System.Numerics;
namespace LightlessSync.UI;
@@ -20,12 +20,14 @@ namespace LightlessSync.UI;
public class SyncshellFinderUI : WindowMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly LightlessConfigService _configService;
private readonly BroadcastService _broadcastService;
private readonly UiSharedService _uiSharedService;
private readonly BroadcastScannerService _broadcastScannerService;
private readonly PairManager _pairManager;
private readonly DalamudUtilService _dalamudUtilService;
private readonly List<GroupJoinDto> _nearbySyncshells = new();
private readonly List<GroupJoinDto> _nearbySyncshells = [];
private List<GroupFullInfoDto> _currentSyncshells = [];
private int _selectedNearbyIndex = -1;
private GroupJoinDto? _joinDto;
@@ -37,17 +39,18 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
LightlessMediator mediator,
PerformanceCollectorService performanceCollectorService,
BroadcastService broadcastService,
LightlessConfigService configService,
UiSharedService uiShared,
ApiController apiController,
BroadcastScannerService broadcastScannerService
) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService)
BroadcastScannerService broadcastScannerService,
PairManager pairManager,
DalamudUtilService dalamudUtilService) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService)
{
_broadcastService = broadcastService;
_uiSharedService = uiShared;
_configService = configService;
_apiController = apiController;
_broadcastScannerService = broadcastScannerService;
_pairManager = pairManager;
_dalamudUtilService = dalamudUtilService;
IsOpen = false;
SizeConstraints = new()
@@ -56,14 +59,14 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
MaximumSize = new(600, 550)
};
Mediator.Subscribe<SyncshellBroadcastsUpdatedMessage>(this, async _ => await RefreshSyncshellsAsync());
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, async _ => await RefreshSyncshellsAsync());
Mediator.Subscribe<SyncshellBroadcastsUpdatedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false));
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false));
}
public override async void OnOpen()
{
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
await RefreshSyncshellsAsync();
await RefreshSyncshellsAsync().ConfigureAwait(false);
}
protected override void DrawInternal()
@@ -100,22 +103,45 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
return;
}
DrawSyncshellTable();
if (_joinDto != null && _joinInfo != null && _joinInfo.Success)
DrawConfirmation();
}
private void DrawSyncshellTable()
{
if (ImGui.BeginTable("##NearbySyncshellsTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg))
{
ImGui.TableSetupColumn("Alias", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("GID", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Syncshell", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Broadcaster", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Join", ImGuiTableColumnFlags.WidthFixed, 80f * ImGuiHelpers.GlobalScale);
ImGui.TableHeadersRow();
for (int i = 0; i < _nearbySyncshells.Count; i++)
foreach (var shell in _nearbySyncshells)
{
var shell = _nearbySyncshells[i];
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted(shell.Group.Alias ?? "(No Alias)");
var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID;
ImGui.TextUnformatted(displayName);
ImGui.TableNextColumn();
ImGui.TextUnformatted(shell.Group.GID);
var broadcasterName = "Unknown";
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.TableNextColumn();
var label = $"Join##{shell.Group.GID}";
@@ -123,75 +149,85 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f));
if (ImGui.Button(label))
if (!_currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal)))
{
_logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})");
_ = Task.Run(async () =>
if (ImGui.Button(label))
{
try
{
var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto(
shell.Group,
shell.Password,
shell.GroupUserPreferredPermissions
)).ConfigureAwait(false);
_logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})");
if (info != null && info.Success)
{
_joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions);
_joinInfo = info;
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
_logger.LogInformation($"Fetched join info for {shell.Group.GID}");
}
else
{
_logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful");
}
}
catch (Exception ex)
_ = Task.Run(async () =>
{
_logger.LogError(ex, $"Join failed for {shell.Group.GID}");
}
});
try
{
var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto(
shell.Group,
shell.Password,
shell.GroupUserPreferredPermissions
)).ConfigureAwait(false);
if (info != null && info.Success)
{
_joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions);
_joinInfo = info;
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
_logger.LogInformation($"Fetched join info for {shell.Group.GID}");
}
else
{
_logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Join failed for {shell.Group.GID}");
}
});
}
}
else
{
using (ImRaii.Disabled())
{
ImGui.Button(label);
}
UiSharedService.AttachToolTip("Already a member or owner of this Syncshell.");
}
ImGui.PopStyleColor(3);
}
ImGui.EndTable();
}
if (_joinDto != null && _joinInfo != null && _joinInfo.Success)
DrawConfirmation();
}
private void DrawConfirmation()
{
ImGui.Separator();
ImGui.TextUnformatted($"Join Syncshell: {_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}");
ImGuiHelpers.ScaledDummy(2f);
ImGui.TextUnformatted("Suggested Syncshell Permissions:");
DrawPermissionRow("Sounds", _joinInfo.GroupPermissions.IsPreferDisableSounds(), _ownPermissions.DisableGroupSounds, v => _ownPermissions.DisableGroupSounds = v);
DrawPermissionRow("Animations", _joinInfo.GroupPermissions.IsPreferDisableAnimations(), _ownPermissions.DisableGroupAnimations, v => _ownPermissions.DisableGroupAnimations = v);
DrawPermissionRow("VFX", _joinInfo.GroupPermissions.IsPreferDisableVFX(), _ownPermissions.DisableGroupVFX, v => _ownPermissions.DisableGroupVFX = v);
ImGui.NewLine();
ImGui.NewLine();
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, $"Finalize and join {_joinDto.Group.AliasOrGID}"))
if (_joinDto != null && _joinInfo != null)
{
var finalPermissions = GroupUserPreferredPermissions.NoneSet;
finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds);
finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations);
finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX);
ImGui.Separator();
ImGui.TextUnformatted($"Join Syncshell: {_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}");
ImGuiHelpers.ScaledDummy(2f);
ImGui.TextUnformatted("Suggested Syncshell Permissions:");
_ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions));
_joinDto = null;
_joinInfo = null;
DrawPermissionRow("Sounds", _joinInfo.GroupPermissions.IsPreferDisableSounds(), _ownPermissions.DisableGroupSounds, v => _ownPermissions.DisableGroupSounds = v);
DrawPermissionRow("Animations", _joinInfo.GroupPermissions.IsPreferDisableAnimations(), _ownPermissions.DisableGroupAnimations, v => _ownPermissions.DisableGroupAnimations = v);
DrawPermissionRow("VFX", _joinInfo.GroupPermissions.IsPreferDisableVFX(), _ownPermissions.DisableGroupVFX, v => _ownPermissions.DisableGroupVFX = v);
ImGui.NewLine();
ImGui.NewLine();
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, $"Finalize and join {_joinDto.Group.AliasOrGID}"))
{
var finalPermissions = GroupUserPreferredPermissions.NoneSet;
finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds);
finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations);
finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX);
_ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions));
_joinDto = null;
_joinInfo = null;
_ = RefreshSyncshellsAsync();
}
}
}
@@ -224,6 +260,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
private async Task RefreshSyncshellsAsync()
{
var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
_currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)];
if (syncshellBroadcasts.Count == 0)
{
@@ -231,11 +268,11 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
return;
}
List<GroupJoinDto> updatedList;
List<GroupJoinDto>? updatedList = [];
try
{
var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts);
updatedList = groups?.ToList() ?? new();
var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts).ConfigureAwait(false);
updatedList = groups?.ToList();
}
catch (Exception ex)
{
@@ -243,24 +280,28 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
return;
}
var currentGids = _nearbySyncshells.Select(s => s.Group.GID).ToHashSet();
var newGids = updatedList.Select(s => s.Group.GID).ToHashSet();
var currentGids = _nearbySyncshells.Select(s => s.Group.GID).ToHashSet(StringComparer.Ordinal);
if (currentGids.SetEquals(newGids))
return;
var previousGid = GetSelectedGid();
_nearbySyncshells.Clear();
_nearbySyncshells.AddRange(updatedList);
if (previousGid != null)
if (updatedList != null)
{
var newIndex = _nearbySyncshells.FindIndex(s => s.Group.GID == previousGid);
if (newIndex >= 0)
{
_selectedNearbyIndex = newIndex;
var newGids = updatedList.Select(s => s.Group.GID).ToHashSet(StringComparer.Ordinal);
if (currentGids.SetEquals(newGids))
return;
var previousGid = GetSelectedGid();
_nearbySyncshells.Clear();
_nearbySyncshells.AddRange(updatedList);
if (previousGid != null)
{
var newIndex = _nearbySyncshells.FindIndex(s => string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal));
if (newIndex >= 0)
{
_selectedNearbyIndex = newIndex;
return;
}
}
}
@@ -291,8 +332,4 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
return _nearbySyncshells[_selectedNearbyIndex].Group.GID;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
}
}

View File

@@ -5,10 +5,18 @@ using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Serilog;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Reflection.Emit;
using System.Threading.Tasks;
namespace LightlessSync.UI;
@@ -19,18 +27,25 @@ public class TopTabMenu
private readonly LightlessMediator _lightlessMediator;
private readonly PairManager _pairManager;
private readonly PairRequestService _pairRequestService;
private readonly DalamudUtilService _dalamudUtilService;
private readonly HashSet<string> _pendingPairRequestActions = new(StringComparer.Ordinal);
private bool _pairRequestsExpanded; // useless for now
private int _lastRequestCount;
private readonly UiSharedService _uiSharedService;
private string _filter = string.Empty;
private int _globalControlCountdown = 0;
private float _pairRequestsHeight = 150f;
private string _pairToAdd = string.Empty;
private SelectedTab _selectedTab = SelectedTab.None;
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService)
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService)
{
_lightlessMediator = lightlessMediator;
_apiController = apiController;
_pairManager = pairManager;
_pairRequestService = pairRequestService;
_dalamudUtilService = dalamudUtilService;
_uiSharedService = uiSharedService;
}
@@ -40,7 +55,8 @@ public class TopTabMenu
Individual,
Syncshell,
Lightfinder,
UserConfig
UserConfig,
Settings
}
public string Filter
@@ -67,7 +83,7 @@ public class TopTabMenu
{
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
var spacing = ImGui.GetStyle().ItemSpacing;
var buttonX = (availableWidth - (spacing.X * 3)) / 4f;
var buttonX = (availableWidth - (spacing.X * 4)) / 5f;
var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y;
var buttonSize = new Vector2(buttonX, buttonY);
var drawList = ImGui.GetWindowDrawList();
@@ -144,6 +160,18 @@ public class TopTabMenu
}
UiSharedService.AttachToolTip("Your User Menu");
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
}
ImGui.SameLine();
}
UiSharedService.AttachToolTip("Open Lightless Settings");
ImGui.NewLine();
btncolor.Dispose();
@@ -169,6 +197,22 @@ public class TopTabMenu
}
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
#if DEBUG
if (ImGui.Button("Add Test Pair Request"))
{
var fakeCid = Guid.NewGuid().ToString("N");
var display = _pairRequestService.RegisterIncomingRequest(fakeCid, "Debug pair request");
_lightlessMediator.Publish(new NotificationMessage(
"Pair request received (debug)",
display.Message,
NotificationType.Info,
TimeSpan.FromSeconds(5)));
}
#endif
DrawIncomingPairRequests(availableWidth);
ImGui.Separator();
DrawFilter(availableWidth, spacing.X);
@@ -192,6 +236,207 @@ public class TopTabMenu
UiSharedService.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd));
}
private void DrawIncomingPairRequests(float availableWidth)
{
var requests = _pairRequestService.GetActiveRequests();
var count = requests.Count;
if (count == 0)
{
_pairRequestsExpanded = false;
_lastRequestCount = 0;
return;
}
if (count > _lastRequestCount)
{
_pairRequestsExpanded = true;
}
_lastRequestCount = count;
var label = $"Incoming Pair Requests - {count}##IncomingPairRequestsHeader";
using (ImRaii.PushColor(ImGuiCol.Header, UIColors.Get("LightlessPurple")))
using (ImRaii.PushColor(ImGuiCol.HeaderHovered, UIColors.Get("LightlessPurpleActive")))
using (ImRaii.PushColor(ImGuiCol.HeaderActive, UIColors.Get("LightlessPurple")))
{
bool open = ImGui.CollapsingHeader(label, ImGuiTreeNodeFlags.DefaultOpen);
_pairRequestsExpanded = open;
if (ImGui.IsItemHovered())
UiSharedService.AttachToolTip("Expand to view incoming pair requests.");
if (open)
{
var lineHeight = ImGui.GetTextLineHeightWithSpacing();
//var desiredHeight = Math.Clamp(count * lineHeight * 2f, 130f * ImGuiHelpers.GlobalScale, 185f * ImGuiHelpers.GlobalScale); we use resize bar instead
ImGui.SetCursorPosX(ImGui.GetCursorPosX() - 2f);
using (ImRaii.PushColor(ImGuiCol.ChildBg, UIColors.Get("LightlessPurple")))
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f))
{
if (ImGui.BeginChild("##IncomingPairRequestsOuter", new Vector2(availableWidth + 5f, _pairRequestsHeight), true))
{
var defaultChildBg = ImGui.GetStyle().Colors[(int)ImGuiCol.WindowBg];
using (ImRaii.PushColor(ImGuiCol.ChildBg, defaultChildBg))
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 5f))
using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(6, 6)))
{
if (ImGui.BeginChild("##IncomingPairRequestsInner", new Vector2(0, 0), true))
{
using (ImRaii.PushColor(ImGuiCol.TableBorderStrong, ImGui.GetStyle().Colors[(int)ImGuiCol.Border]))
using (ImRaii.PushColor(ImGuiCol.TableBorderLight, ImGui.GetStyle().Colors[(int)ImGuiCol.Border]))
{
DrawPairRequestList(requests);
}
}
ImGui.EndChild();
}
}
ImGui.EndChild();
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("ButtonDefault"));
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple"));
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive"));
ImGui.Button("##resizeHandle", new Vector2(availableWidth, 4f));
ImGui.PopStyleColor(3);
if (ImGui.IsItemActive())
{
_pairRequestsHeight += ImGui.GetIO().MouseDelta.Y;
_pairRequestsHeight = Math.Clamp(_pairRequestsHeight, 100f, 300f);
}
}
}
}
}
private void DrawPairRequestList(IReadOnlyList<PairRequestService.PairRequestDisplay> requests)
{
float playerColWidth = 207f * ImGuiHelpers.GlobalScale;
float receivedColWidth = 73f * ImGuiHelpers.GlobalScale;
float actionsColWidth = 50f * ImGuiHelpers.GlobalScale;
ImGui.Separator();
ImGui.TextUnformatted("Player");
ImGui.SameLine(playerColWidth + 2f);
ImGui.TextUnformatted("Received");
ImGui.SameLine(playerColWidth + receivedColWidth + 12f);
ImGui.TextUnformatted("Actions");
ImGui.Separator();
foreach (var request in requests)
{
ImGui.BeginGroup();
var label = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName;
ImGui.TextUnformatted(label.Truncate(26));
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.TextUnformatted(label);
ImGui.EndTooltip();
}
ImGui.SameLine(playerColWidth);
ImGui.TextUnformatted(GetRelativeTime(request.ReceivedAt));
ImGui.SameLine(playerColWidth + receivedColWidth);
DrawPairRequestActions(request);
ImGui.EndGroup();
}
}
private void DrawPairRequestActions(PairRequestService.PairRequestDisplay request)
{
using var id = ImRaii.PushId(request.HashedCid);
var label = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName;
var inFlight = _pendingPairRequestActions.Contains(request.HashedCid);
using (ImRaii.Disabled(inFlight))
{
if (_uiSharedService.IconButton(FontAwesomeIcon.Check))
{
_ = AcceptPairRequestAsync(request);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Accept request");
ImGui.SameLine();
if (_uiSharedService.IconButton(FontAwesomeIcon.Times))
{
RejectPairRequest(request.HashedCid, label);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Decline request");
}
}
private static string GetRelativeTime(DateTime receivedAt)
{
var delta = DateTime.UtcNow - receivedAt;
if (delta <= TimeSpan.FromSeconds(10))
{
return "Just now";
}
if (delta.TotalMinutes >= 1)
{
return $"{Math.Floor(delta.TotalMinutes)}m {delta.Seconds:D2}s ago";
}
return $"{delta.Seconds}s ago";
}
private async Task AcceptPairRequestAsync(PairRequestService.PairRequestDisplay request)
{
if (!_pendingPairRequestActions.Add(request.HashedCid))
{
return;
}
try
{
var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
await _apiController.TryPairWithContentId(request.HashedCid, myCidHash).ConfigureAwait(false);
_pairRequestService.RemoveRequest(request.HashedCid);
var display = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName;
_lightlessMediator.Publish(new NotificationMessage(
"Pair request accepted",
$"Sent a pair request back to {display}.",
NotificationType.Info,
TimeSpan.FromSeconds(3)));
}
catch (Exception ex)
{
_lightlessMediator.Publish(new NotificationMessage(
"Failed to accept pair request",
ex.Message,
NotificationType.Error,
TimeSpan.FromSeconds(5)));
}
finally
{
_pendingPairRequestActions.Remove(request.HashedCid);
}
}
private void RejectPairRequest(string hashedCid, string playerName)
{
if (!_pairRequestService.RemoveRequest(hashedCid))
{
return;
}
_lightlessMediator.Publish(new NotificationMessage("Pair request declined", "Declined " + playerName + "'s pending pair request.",
NotificationType.Info,
TimeSpan.FromSeconds(3)));
}
private void DrawFilter(float availableWidth, float spacingX)
{
var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.Ban, "Clear");

View File

@@ -1,4 +1,4 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.GameFonts;
@@ -7,6 +7,7 @@ using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using System;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
@@ -173,12 +174,14 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
public static string ByteToString(long bytes, bool addSuffix = true)
{
string[] suffix = ["B", "KiB", "MiB", "GiB", "TiB"];
int i;
string[] suffix = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
int i = 0;
double dblSByte = bytes;
for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024)
while (dblSByte >= 1000 && i < suffix.Length - 1)
{
dblSByte = bytes / 1024.0;
dblSByte /= 1000.0;
i++;
}
return addSuffix ? $"{dblSByte:0.00} {suffix[i]}" : $"{dblSByte:0.00}";
@@ -510,10 +513,71 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
ImGui.Dummy(new Vector2(0, thickness * scale));
}
public static bool CheckboxWithBorder(string label, ref bool value, Vector4? borderColor = null, float borderThickness = 1.0f, float rounding = 3.0f)
{
var pos = ImGui.GetCursorScreenPos();
bool changed = ImGui.Checkbox(label, ref value);
var min = pos;
var max = ImGui.GetItemRectMax();
var col = ImGui.GetColorU32(borderColor ?? ImGuiColors.DalamudGrey);
ImGui.GetWindowDrawList().AddRect(min, max, col, rounding, ImDrawFlags.None, borderThickness);
return changed;
}
public void MediumText(string text, Vector4? color = null)
{
FontText(text, MediumFont, color);
}
public void DrawNoteLine(string icon, Vector4 color, string text)
{
MediumText(icon, color);
var iconHeight = ImGui.GetItemRectSize().Y;
ImGui.SameLine();
float textHeight = ImGui.GetTextLineHeight();
float offset = (iconHeight - textHeight) * 0.5f;
if (offset > 0)
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + offset);
ImGui.BeginGroup();
ImGui.TextWrapped(text);
ImGui.EndGroup();
}
public void DrawNoteLine(string icon, Vector4 color, ReadOnlySpan<SeStringUtils.RichTextEntry> fragments)
{
if (fragments.Length == 0)
{
DrawNoteLine(icon, color, string.Empty);
return;
}
MediumText(icon, color);
var iconHeight = ImGui.GetItemRectSize().Y;
ImGui.SameLine();
float textHeight = ImGui.GetTextLineHeight();
float offset = (iconHeight - textHeight) * 0.5f;
if (offset > 0)
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + offset);
var wrapWidth = ImGui.GetContentRegionAvail().X;
ImGui.BeginGroup();
var richText = SeStringUtils.BuildRichText(fragments);
SeStringUtils.RenderSeStringWrapped(richText, wrapWidth);
ImGui.EndGroup();
}
public void DrawNoteLine(string icon, Vector4 color, params SeStringUtils.RichTextEntry[] fragments)
{
DrawNoteLine(icon, color, fragments.AsSpan());
}
public bool MediumTreeNode(string label, Vector4? textColor = null, float lineWidth = 2f, ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.SpanAvailWidth)
{

View File

@@ -1,17 +1,23 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.Utility;
using Lumina.Text;
using System;
using System.Numerics;
using DalamudSeString = Dalamud.Game.Text.SeStringHandling.SeString;
using DalamudSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder;
using LuminaSeStringBuilder = Lumina.Text.SeStringBuilder;
namespace LightlessSync.Utils;
public static class SeStringUtils
{
public static SeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor)
public static DalamudSeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor)
{
var b = new SeStringBuilder();
var b = new DalamudSeStringBuilder();
if (glowColor is Vector4 glow)
b.Add(new GlowPayload(glow));
@@ -30,14 +36,47 @@ public static class SeStringUtils
return b.Build();
}
public static SeString BuildPlain(string text)
public static DalamudSeString BuildPlain(string text)
{
var b = new SeStringBuilder();
var b = new DalamudSeStringBuilder();
b.AddText(text ?? string.Empty);
return b.Build();
}
public static void RenderSeString(SeString seString, Vector2 position, ImFontPtr? font = null, ImDrawListPtr? drawList = null)
public static DalamudSeString BuildRichText(ReadOnlySpan<RichTextEntry> fragments)
{
var builder = new LuminaSeStringBuilder();
foreach (var fragment in fragments)
{
if (string.IsNullOrEmpty(fragment.Text))
continue;
var hasColor = fragment.Color.HasValue;
Vector4 color = default;
if (hasColor)
{
color = fragment.Color!.Value;
builder.PushColorRgba(color);
}
if (fragment.Bold)
builder.AppendSetBold(true);
builder.Append(fragment.Text.AsSpan());
if (fragment.Bold)
builder.AppendSetBold(false);
if (hasColor)
builder.PopColor();
}
return DalamudSeString.Parse(builder.ToArray());
}
public static DalamudSeString BuildRichText(params RichTextEntry[] fragments) => BuildRichText(fragments.AsSpan());
public static void RenderSeString(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, ImDrawListPtr? drawList = null)
{
drawList ??= ImGui.GetWindowDrawList();
@@ -51,9 +90,36 @@ public static class SeStringUtils
ImGui.SetCursorScreenPos(position);
ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);
var textSize = ImGui.CalcTextSize(seString.TextValue);
if (textSize.Y <= 0f)
textSize.Y = ImGui.GetTextLineHeight();
ImGui.Dummy(new Vector2(0f, textSize.Y));
}
public static Vector2 RenderSeStringWithHitbox(SeString seString, Vector2 position, ImFontPtr? font = null)
public static void RenderSeStringWrapped(DalamudSeString seString, float wrapWidth, ImFontPtr? font = null, ImDrawListPtr? drawList = null)
{
drawList ??= ImGui.GetWindowDrawList();
var drawParams = new SeStringDrawParams
{
Font = font ?? ImGui.GetFont(),
Color = ImGui.GetColorU32(ImGuiCol.Text),
WrapWidth = wrapWidth,
TargetDrawList = drawList
};
ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);
var calcWrapWidth = wrapWidth > 0f ? wrapWidth : -1f;
var textSize = ImGui.CalcTextSize(seString.TextValue, wrapWidth: calcWrapWidth);
if (textSize.Y <= 0f)
textSize.Y = ImGui.GetTextLineHeight();
ImGui.Dummy(new Vector2(0f, textSize.Y));
}
public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, ImFontPtr? font = null)
{
var drawList = ImGui.GetWindowDrawList();
@@ -99,6 +165,8 @@ public static class SeStringUtils
#region Internal Payloads
public readonly record struct RichTextEntry(string Text, Vector4? Color = null, bool Bold = false);
private abstract class AbstractColorPayload : Payload
{
protected byte Red { get; init; }

View File

@@ -134,6 +134,12 @@ public partial class ApiController
await _lightlessHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false);
}
public async Task UserUpdateVanityColors(UserVanityColorsDto dto)
{
if (!IsConnected) return;
await _lightlessHub!.InvokeAsync(nameof(UserUpdateVanityColors), dto).ConfigureAwait(false);
}
public async Task UserUpdateDefaultPermissions(DefaultPermissionsDto defaultPermissionsDto)
{
CheckConnection();

View File

@@ -105,6 +105,21 @@ public partial class ApiController
return Task.CompletedTask;
}
public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto)
{
if (dto == null)
return Task.CompletedTask;
var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty);
Mediator.Publish(new NotificationMessage(
"Pair request received",
request.Message,
NotificationType.Info,
TimeSpan.FromSeconds(5)));
return Task.CompletedTask;
}
public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo)
{
SystemInfoDto = systemInfo;
@@ -277,12 +292,25 @@ public partial class ApiController
_lightlessHub!.On(nameof(Client_GroupSendInfo), act);
}
public void OnGroupUpdateProfile(Action<GroupProfileDto> act)
{
if (_initialized) return;
_lightlessHub!.On(nameof(Client_GroupSendProfile), act);
}
public void OnReceiveServerMessage(Action<MessageSeverity, string> act)
{
if (_initialized) return;
_lightlessHub!.On(nameof(Client_ReceiveServerMessage), act);
}
public void OnReceiveBroadcastPairRequest(Action<UserPairNotificationDto> act)
{
if (_initialized) return;
_lightlessHub!.On(nameof(Client_ReceiveBroadcastPairRequest), act);
}
public void OnUpdateSystemInfo(Action<SystemInfoDto> act)
{
if (_initialized) return;

View File

@@ -45,6 +45,11 @@ public partial class ApiController
CheckConnection();
await _lightlessHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false);
}
public async Task GroupClearFinder(GroupDto group)
{
CheckConnection();
await _lightlessHub!.SendAsync(nameof(GroupClearFinder), group).ConfigureAwait(false);
}
public async Task<GroupJoinDto> GroupCreate()
{

View File

@@ -28,6 +28,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
private readonly DalamudUtilService _dalamudUtil;
private readonly HubFactory _hubFactory;
private readonly PairManager _pairManager;
private readonly PairRequestService _pairRequestService;
private readonly ServerConfigurationManager _serverManager;
private readonly TokenProvider _tokenProvider;
private readonly LightlessConfigService _lightlessConfigService;
@@ -42,12 +43,13 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
private CensusUpdateMessage? _lastCensus;
public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, DalamudUtilService dalamudUtil,
PairManager pairManager, ServerConfigurationManager serverManager, LightlessMediator mediator,
PairManager pairManager, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator,
TokenProvider tokenProvider, LightlessConfigService lightlessConfigService) : base(logger, mediator)
{
_hubFactory = hubFactory;
_dalamudUtil = dalamudUtil;
_pairManager = pairManager;
_pairRequestService = pairRequestService;
_serverManager = serverManager;
_tokenProvider = tokenProvider;
_lightlessConfigService = lightlessConfigService;
@@ -77,6 +79,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
public DefaultPermissionsDto? DefaultPermissions => _connectionDto?.DefaultPreferredPermissions ?? null;
public string DisplayName => _connectionDto?.User.AliasOrUID ?? string.Empty;
public bool HasVanity => _connectionDto?.HasVanity ?? false;
public string TextColorHex => _connectionDto?.TextColorHex ?? string.Empty;
public string TextGlowColorHex => _connectionDto?.TextGlowColorHex ?? string.Empty;
public bool IsConnected => ServerState == ServerState.Connected;
public bool IsCurrentVersion => (Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0, 0)) >= (_connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0, 0));
@@ -424,6 +430,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Logger.LogDebug("Initializing data");
OnDownloadReady((guid) => _ = Client_DownloadReady(guid));
OnReceiveServerMessage((sev, msg) => _ = Client_ReceiveServerMessage(sev, msg));
OnReceiveBroadcastPairRequest(dto => _ = Client_ReceiveBroadcastPairRequest(dto));
OnUpdateSystemInfo((dto) => _ = Client_UpdateSystemInfo(dto));
OnUserSendOffline((dto) => _ = Client_UserSendOffline(dto));
@@ -445,6 +452,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
OnGroupPairLeft((dto) => _ = Client_GroupPairLeft(dto));
OnGroupSendFullInfo((dto) => _ = Client_GroupSendFullInfo(dto));
OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto));
OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto));
OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto));
OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto));