Compare commits
85 Commits
1.12.2.5-D
...
1.12.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1364ac32bd | ||
|
|
e4931ded9f | ||
| a3b5a5cb2e | |||
|
|
0e274f04f1 | ||
|
|
dfb2e948c5 | ||
|
|
f037e7587d | ||
|
|
5bf443cf0e | ||
|
|
78455f7523 | ||
| 8dd13479fc | |||
|
|
87e6c0b3f6 | ||
| 25d39c0ad1 | |||
| 458aa5f933 | |||
|
|
3d2650cc5f | ||
|
|
931009607b | ||
|
|
36bf17aee2 | ||
|
|
012547056a | ||
|
|
9242f23787 | ||
|
|
e8a3d87ff0 | ||
|
|
4862921b03 | ||
|
|
f2b7b0c4e3 | ||
|
|
05872c285c | ||
|
|
da59105bab | ||
|
|
54ea1e9d4c | ||
|
|
f6e1832d62 | ||
| afc42d97d1 | |||
|
|
714aeef468 | ||
|
|
3a627f2d3e | ||
|
|
bf3770025b | ||
|
|
49d138049e | ||
|
|
e91d163763 | ||
| dea4ef4832 | |||
| 76520878bf | |||
|
|
068c8cb180 | ||
|
|
c34e379a6e | ||
| de007e2b04 | |||
| e52d4c6165 | |||
| 34250a18b4 | |||
| 390a0c2d61 | |||
|
|
571decd33b | ||
|
|
36c1611486 | ||
|
|
597a0beb91 | ||
|
|
1fa2f063e8 | ||
|
|
1fae47b171 | ||
| e85b900754 | |||
|
|
9696ac60f1 | ||
|
|
0b7b543dd7 | ||
|
|
8a9d4b8daa | ||
|
|
7d6d500e6a | ||
|
|
dc1fdd4a3e | ||
|
|
f74cd01a32 | ||
|
|
8f7f07311f | ||
|
|
7b415b4e47 | ||
| f90b9c3f8e | |||
|
|
0cc7181e98 | ||
|
|
914553d5ab | ||
|
|
08e3c8678f | ||
|
|
6af2013a03 | ||
|
|
3831dd24f1 | ||
|
|
73f130a95a | ||
|
|
7c4269b011 | ||
|
|
c6f8d6843e | ||
| fd9bd3975b | |||
|
|
3e15dd643e | ||
|
|
f6784fdf28 | ||
|
|
c182d10a0a | ||
| 8e7cdce310 | |||
|
|
e3a3f16d14 | ||
|
|
31b56ba58a | ||
| 9bc2ad24cd | |||
| 864a2e6677 | |||
|
|
ab3ca78c7f | ||
|
|
6bb379ebad | ||
|
|
b0b149d8bc | ||
|
|
777e6b9d27 | ||
|
|
37c11e9d73 | ||
|
|
4060ba96f1 | ||
| e8f8512cdd | |||
| 7569b15993 | |||
| d91f1a3356 | |||
| 0c38b9397a | |||
| 9d850f8fa6 | |||
| 9eb2309018 | |||
|
|
bd2c3a4a80 | ||
|
|
fe898cdc2b | ||
|
|
38a360bfee |
@@ -2,7 +2,7 @@ name: Tag and Release Lightless
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ master, dev ]
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: LightlessSync
|
||||
@@ -62,7 +62,8 @@ jobs:
|
||||
mkdir -p output
|
||||
(cd /workspace/Lightless-Sync/LightlessClient/LightlessSync/bin/x64/Release/ && zip -r $OLDPWD/output/LightlessClient.zip *)
|
||||
|
||||
- name: Create Git tag if not exists
|
||||
- name: Create Git tag if not exists (master)
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
tag="${{ steps.package_version.outputs.version }}"
|
||||
git fetch --tags
|
||||
@@ -76,7 +77,23 @@ jobs:
|
||||
echo "Tag $tag already exists. Skipping tag creation."
|
||||
fi
|
||||
|
||||
- name: Create Release
|
||||
- name: Create Git tag if not exists (dev)
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: |
|
||||
tag="${{ steps.package_version.outputs.version }}-Dev"
|
||||
git fetch --tags
|
||||
if ! git tag -l "$tag" | grep -q "$tag"; then
|
||||
echo "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
|
||||
echo "Tag $tag already exists. Skipping tag creation."
|
||||
fi
|
||||
|
||||
- name: Create Release (master)
|
||||
if: github.ref == 'refs/heads/master'
|
||||
id: create_release
|
||||
run: |
|
||||
echo "=== Searching for existing release ${{ steps.package_version.outputs.version }}==="
|
||||
@@ -107,6 +124,35 @@ jobs:
|
||||
release_id=$(echo "$response" | jq -r .id)
|
||||
echo "release_id=$release_id" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create Release (dev)
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
id: create_release
|
||||
run: |
|
||||
version="${{ steps.package_version.outputs.version }}-Dev"
|
||||
echo "=== Searching for existing release $version==="
|
||||
release_id=$(curl -s -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/$version" | jq -r .id)
|
||||
if [ "$release_id" != "null" ]; then
|
||||
echo "=== Deleting existing release $version==="
|
||||
curl -X DELETE -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/$release_id"
|
||||
fi
|
||||
echo "=== Creating new release $version==="
|
||||
response=$(
|
||||
curl --fail-with-body -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||
-d '{
|
||||
"tag_name": "'"$version"'",
|
||||
"name": "'"$version"'",
|
||||
"draft": false,
|
||||
"prerelease": false
|
||||
}' \
|
||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases"
|
||||
)
|
||||
release_id=$(echo "$response" | jq -r .id)
|
||||
echo "release_id=$release_id" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload Assets to release
|
||||
run: |
|
||||
curl --fail-with-body -s -X POST \
|
||||
@@ -122,7 +168,8 @@ jobs:
|
||||
env:
|
||||
GIT_TERMINAL_PROMPT: 0
|
||||
|
||||
- name: Update plogonmaster.json with version
|
||||
- name: Update plogonmaster.json with version (master)
|
||||
if: github.ref == 'refs/heads/master'
|
||||
env:
|
||||
VERSION: ${{ steps.package_version.outputs.version }}
|
||||
run: |
|
||||
@@ -159,7 +206,6 @@ jobs:
|
||||
.DalamudApiLevel = $dalamudApiLevel
|
||||
| .AssemblyVersion = $version
|
||||
| .DownloadLinkInstall = $downloadUrl
|
||||
| .DownloadLinkTesting = $downloadUrl
|
||||
| .DownloadLinkUpdate = $downloadUrl
|
||||
else
|
||||
.
|
||||
@@ -172,6 +218,47 @@ jobs:
|
||||
# Output the content of the file
|
||||
cat "$repoJsonPath"
|
||||
|
||||
- name: Update plogonmaster.json with version (dev)
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
env:
|
||||
VERSION: ${{ steps.package_version.outputs.version }}
|
||||
run: |
|
||||
set -e
|
||||
pluginJsonPath="${PLUGIN_NAME}/bin/x64/Release/${PLUGIN_NAME}.json"
|
||||
repoJsonPath="LightlessSyncRepo/LightlessSync/plogonmaster.json"
|
||||
assemblyVersion="${VERSION}"
|
||||
version="${VERSION}-Dev"
|
||||
downloadUrl="https://git.lightless-sync.org/${{ gitea.repository_owner }}/LightlessClient/releases/download/$version/LightlessClient.zip"
|
||||
pluginJson=$(cat "$pluginJsonPath")
|
||||
internalName=$(jq -r '.InternalName' <<< "$pluginJson")
|
||||
dalamudApiLevel=$(jq -r '.DalamudApiLevel' <<< "$pluginJson")
|
||||
repoJsonRaw=$(cat "$repoJsonPath")
|
||||
if echo "$repoJsonRaw" | jq 'type' | grep -q '"array"'; then
|
||||
repoJson="$repoJsonRaw"
|
||||
else
|
||||
repoJson="[$repoJsonRaw]"
|
||||
fi
|
||||
updatedRepoJson=$(jq \
|
||||
--arg internalName "$internalName" \
|
||||
--arg dalamudApiLevel "$dalamudApiLevel" \
|
||||
--arg assemblyVersion "$assemblyVersion" \
|
||||
--arg version "$version" \
|
||||
--arg downloadUrl "$downloadUrl" \
|
||||
'
|
||||
map(
|
||||
if .InternalName == $internalName
|
||||
then
|
||||
.DalamudApiLevel = $dalamudApiLevel
|
||||
| .TestingAssemblyVersion = $assemblyVersion
|
||||
| .DownloadLinkTesting = $downloadUrl
|
||||
else
|
||||
.
|
||||
end
|
||||
)
|
||||
' <<< "$repoJson")
|
||||
echo "$updatedRepoJson" > "$repoJsonPath"
|
||||
cat "$repoJsonPath"
|
||||
|
||||
- name: Commit and push to LightlessSync
|
||||
run: |
|
||||
cd LightlessSyncRepo/LightlessSync
|
||||
|
||||
140
.github/workflows/lightless-tag-and-release.yml
vendored
140
.github/workflows/lightless-tag-and-release.yml
vendored
@@ -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
|
||||
Submodule LightlessAPI updated: a337481243...167508d27b
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -20,6 +20,7 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
||||
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
||||
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u);
|
||||
public bool UseLightlessRedesign { get; set; } = true;
|
||||
public bool EnableRightClickMenus { get; set; } = true;
|
||||
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
||||
public string ExportFolder { get; set; } = string.Empty;
|
||||
@@ -66,4 +67,10 @@ 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;
|
||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors></Authors>
|
||||
<Company></Company>
|
||||
<Version>1.11.12</Version>
|
||||
<Version>1.12.0</Version>
|
||||
<Description></Description>
|
||||
<Copyright></Copyright>
|
||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.Windowing;
|
||||
@@ -41,7 +41,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui,
|
||||
IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager,
|
||||
ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, IGameConfig gameConfig,
|
||||
ISigScanner sigScanner, INamePlateGui namePlateGui)
|
||||
ISigScanner sigScanner, INamePlateGui namePlateGui, IAddonLifecycle addonLifecycle)
|
||||
{
|
||||
if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName))
|
||||
Directory.CreateDirectory(pluginInterface.ConfigDirectory.FullName);
|
||||
@@ -90,6 +90,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton(new WindowSystem("LightlessSync"));
|
||||
collection.AddSingleton<FileDialogManager>();
|
||||
collection.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", "", useEmbedded: true));
|
||||
collection.AddSingleton(gameGui);
|
||||
|
||||
// add lightless related singletons
|
||||
collection.AddSingleton<LightlessMediator>();
|
||||
@@ -112,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>();
|
||||
@@ -144,6 +146,11 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
|
||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu));
|
||||
collection.AddSingleton<RedrawManager>();
|
||||
collection.AddSingleton<BroadcastService>();
|
||||
collection.AddSingleton(addonLifecycle);
|
||||
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,
|
||||
@@ -174,7 +181,12 @@ public sealed class Plugin : IDalamudPlugin
|
||||
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
||||
return httpClient;
|
||||
});
|
||||
collection.AddSingleton((s) => new LightlessConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) =>
|
||||
{
|
||||
var cfg = new LightlessConfigService(pluginInterface.ConfigDirectory.FullName);
|
||||
LightlessSync.UI.Style.MainStyle.Init(cfg);
|
||||
return cfg;
|
||||
});
|
||||
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new PairTagConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
@@ -194,8 +206,10 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
|
||||
collection.AddSingleton<ConfigurationMigrator>();
|
||||
collection.AddSingleton<ConfigurationSaveService>();
|
||||
|
||||
collection.AddSingleton<HubFactory>();
|
||||
collection.AddSingleton<NameplateHandler>();
|
||||
collection.AddSingleton(s => new BroadcastScannerService( s.GetRequiredService<ILogger<BroadcastScannerService>>(), clientState, objectTable, framework, s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<NameplateHandler>(), s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessConfigService>()));
|
||||
|
||||
|
||||
// add scoped services
|
||||
collection.AddScoped<DrawEntityFactory>();
|
||||
@@ -218,6 +232,8 @@ public sealed class Plugin : IDalamudPlugin
|
||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<FileDialogManager>(),
|
||||
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<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<DalamudUtilService>()));
|
||||
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
||||
collection.AddScoped<IPopupHandler, CensusPopupHandler>();
|
||||
collection.AddScoped<CacheCreationService>();
|
||||
@@ -248,6 +264,8 @@ 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<ContextMenuService>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<BroadcastService>());
|
||||
})
|
||||
.Build();
|
||||
|
||||
@@ -259,4 +277,4 @@ public sealed class Plugin : IDalamudPlugin
|
||||
_host.StopAsync().GetAwaiter().GetResult();
|
||||
_host.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
222
LightlessSync/Services/BroadcastScanningService.cs
Normal file
222
LightlessSync/Services/BroadcastScanningService.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDisposable
|
||||
{
|
||||
private readonly ILogger<BroadcastScannerService> _logger;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly IFramework _framework;
|
||||
|
||||
private readonly BroadcastService _broadcastService;
|
||||
private readonly NameplateHandler _nameplateHandler;
|
||||
|
||||
private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new();
|
||||
private readonly Queue<string> _lookupQueue = new();
|
||||
private readonly HashSet<string> _lookupQueuedCids = new();
|
||||
private readonly HashSet<string> _syncshellCids = new();
|
||||
|
||||
private static readonly TimeSpan MaxAllowedTtl = TimeSpan.FromMinutes(4);
|
||||
private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1);
|
||||
|
||||
private readonly CancellationTokenSource _cleanupCts = new();
|
||||
private Task? _cleanupTask;
|
||||
|
||||
private int _checkEveryFrames = 20;
|
||||
private int _frameCounter = 0;
|
||||
private int _lookupsThisFrame = 0;
|
||||
private const int MaxLookupsPerFrame = 30;
|
||||
private const int MaxQueueSize = 100;
|
||||
|
||||
private volatile bool _batchRunning = false;
|
||||
|
||||
public IReadOnlyDictionary<string, BroadcastEntry> BroadcastCache => _broadcastCache;
|
||||
public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID);
|
||||
|
||||
public BroadcastScannerService(ILogger<BroadcastScannerService> logger,
|
||||
IClientState clientState,
|
||||
IObjectTable objectTable,
|
||||
IFramework framework,
|
||||
BroadcastService broadcastService,
|
||||
LightlessMediator mediator,
|
||||
NameplateHandler nameplateHandler,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessConfigService configService) : base(logger, mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_objectTable = objectTable;
|
||||
_broadcastService = broadcastService;
|
||||
_nameplateHandler = nameplateHandler;
|
||||
|
||||
_logger = logger;
|
||||
_framework = framework;
|
||||
_framework.Update += OnFrameworkUpdate;
|
||||
|
||||
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
||||
_cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop);
|
||||
|
||||
_nameplateHandler.Init();
|
||||
}
|
||||
|
||||
private void OnFrameworkUpdate(IFramework framework) => Update();
|
||||
|
||||
public void Update()
|
||||
{
|
||||
_frameCounter++;
|
||||
_lookupsThisFrame = 0;
|
||||
|
||||
if (!_broadcastService.IsBroadcasting)
|
||||
return;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj is not IPlayerCharacter player || player.Address == IntPtr.Zero)
|
||||
continue;
|
||||
|
||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(player.Address);
|
||||
var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now;
|
||||
|
||||
if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < MaxQueueSize)
|
||||
_lookupQueue.Enqueue(cid);
|
||||
}
|
||||
|
||||
if (_frameCounter % _checkEveryFrames == 0 && _lookupQueue.Count > 0)
|
||||
{
|
||||
var cidsToLookup = new List<string>();
|
||||
while (_lookupQueue.Count > 0 && _lookupsThisFrame < MaxLookupsPerFrame)
|
||||
{
|
||||
var cid = _lookupQueue.Dequeue();
|
||||
_lookupQueuedCids.Remove(cid);
|
||||
cidsToLookup.Add(cid);
|
||||
_lookupsThisFrame++;
|
||||
}
|
||||
|
||||
if (cidsToLookup.Count > 0 && !_batchRunning)
|
||||
{
|
||||
_batchRunning = true;
|
||||
_ = BatchUpdateBroadcastCacheAsync(cidsToLookup).ContinueWith(_ => _batchRunning = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BatchUpdateBroadcastCacheAsync(List<string> cids)
|
||||
{
|
||||
var results = await _broadcastService.AreUsersBroadcastingAsync(cids).ConfigureAwait(false);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
foreach (var (cid, info) in results)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cid) || info == null)
|
||||
continue;
|
||||
|
||||
var ttl = info.IsBroadcasting && info.TTL.HasValue
|
||||
? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, MaxAllowedTtl.Ticks))
|
||||
: RetryDelay;
|
||||
|
||||
var expiry = now + ttl;
|
||||
|
||||
_broadcastCache.AddOrUpdate(cid,
|
||||
new BroadcastEntry(info.IsBroadcasting, expiry, info.GID),
|
||||
(_, old) => new BroadcastEntry(info.IsBroadcasting, expiry, info.GID));
|
||||
}
|
||||
|
||||
var activeCids = _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
|
||||
.Select(e => e.Key)
|
||||
.ToList();
|
||||
|
||||
_nameplateHandler.UpdateBroadcastingCids(activeCids);
|
||||
UpdateSyncshellBroadcasts();
|
||||
}
|
||||
|
||||
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
|
||||
{
|
||||
if (!msg.Enabled)
|
||||
{
|
||||
_broadcastCache.Clear();
|
||||
_lookupQueue.Clear();
|
||||
_lookupQueuedCids.Clear();
|
||||
_syncshellCids.Clear();
|
||||
|
||||
_nameplateHandler.UpdateBroadcastingCids(Enumerable.Empty<string>());
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSyncshellBroadcasts()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var newSet = _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||
.Select(e => e.Key)
|
||||
.ToHashSet();
|
||||
|
||||
if (!_syncshellCids.SetEquals(newSet))
|
||||
{
|
||||
_syncshellCids.Clear();
|
||||
foreach (var cid in newSet)
|
||||
_syncshellCids.Add(cid);
|
||||
|
||||
Mediator.Publish(new SyncshellBroadcastsUpdatedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
return _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||
.Select(e => new BroadcastStatusInfoDto
|
||||
{
|
||||
HashedCID = e.Key,
|
||||
IsBroadcasting = true,
|
||||
TTL = e.Value.ExpiryTime - now,
|
||||
GID = e.Value.GID
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task ExpiredBroadcastCleanupLoop()
|
||||
{
|
||||
var token = _cleanupCts.Token;
|
||||
|
||||
try
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), token);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var (cid, entry) in _broadcastCache.ToArray())
|
||||
{
|
||||
if (entry.ExpiryTime <= now)
|
||||
_broadcastCache.TryRemove(cid, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Broadcast cleanup loop crashed");
|
||||
}
|
||||
|
||||
UpdateSyncshellBroadcasts();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
_framework.Update -= OnFrameworkUpdate;
|
||||
_cleanupCts.Cancel();
|
||||
_cleanupTask?.Wait(100);
|
||||
_nameplateHandler.Uninit();
|
||||
}
|
||||
}
|
||||
377
LightlessSync/Services/BroadcastService.cs
Normal file
377
LightlessSync/Services/BroadcastService.cs
Normal file
@@ -0,0 +1,377 @@
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
private readonly ILogger<BroadcastService> _logger;
|
||||
private readonly ApiController _apiController;
|
||||
private readonly LightlessMediator _mediator;
|
||||
private readonly LightlessConfigService _config;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
public LightlessMediator Mediator => _mediator;
|
||||
|
||||
public bool IsLightFinderAvailable { get; private set; } = true;
|
||||
|
||||
public bool IsBroadcasting => _config.Current.BroadcastEnabled;
|
||||
private bool _syncedOnStartup = false;
|
||||
private bool _waitingForTtlFetch = false;
|
||||
private TimeSpan? _remainingTtl = null;
|
||||
private DateTime _lastTtlCheck = DateTime.MinValue;
|
||||
private DateTime _lastForcedDisableTime = DateTime.MinValue;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public BroadcastService(ILogger<BroadcastService> logger, LightlessMediator mediator, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
_config = config;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_apiController = apiController;
|
||||
}
|
||||
|
||||
private async Task RequireConnectionAsync(string context, Func<Task> action)
|
||||
{
|
||||
if (!_apiController.IsConnected)
|
||||
{
|
||||
_logger.LogDebug(context + " skipped, not connected");
|
||||
return;
|
||||
}
|
||||
await action().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_mediator.Subscribe<EnableBroadcastMessage>(this, OnEnableBroadcast);
|
||||
_mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
||||
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
||||
|
||||
_apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken);
|
||||
//_ = CheckLightfinderSupportAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_mediator.UnsubscribeAll(this);
|
||||
_apiController.OnConnected -= () => _ = CheckLightfinderSupportAsync(cancellationToken);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// need to rework this, this is cooked
|
||||
private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!_apiController.IsConnected && !cancellationToken.IsCancellationRequested)
|
||||
await Task.Delay(250, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var dummy = "0".PadLeft(64, '0');
|
||||
|
||||
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.");
|
||||
}
|
||||
catch (HubException ex) when (ex.Message.Contains("Method does not exist"))
|
||||
{
|
||||
_logger.LogWarning("Lightfinder unavailable: required method missing.");
|
||||
IsLightFinderAvailable = false;
|
||||
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Lightfinder check was canceled.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Lightfinder check failed.");
|
||||
IsLightFinderAvailable = false;
|
||||
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnableBroadcast(EnableBroadcastMessage msg)
|
||||
{
|
||||
_ = RequireConnectionAsync(nameof(OnEnableBroadcast), async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
GroupBroadcastRequestDto? groupDto = null;
|
||||
if (_config.Current.SyncshellFinderEnabled && _config.Current.SelectedFinderSyncshell != null)
|
||||
{
|
||||
groupDto = new GroupBroadcastRequestDto
|
||||
{
|
||||
HashedCID = msg.HashedCid,
|
||||
GID = _config.Current.SelectedFinderSyncshell,
|
||||
Enabled = msg.Enabled,
|
||||
};
|
||||
}
|
||||
|
||||
await _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid);
|
||||
|
||||
if (!msg.Enabled)
|
||||
{
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
return;
|
||||
}
|
||||
|
||||
_waitingForTtlFetch = true;
|
||||
|
||||
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
|
||||
|
||||
if (ttl is { } remaining && remaining > TimeSpan.Zero)
|
||||
{
|
||||
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
|
||||
_config.Current.BroadcastEnabled = true;
|
||||
_config.Save();
|
||||
|
||||
_logger.LogDebug("Fetched TTL from server: {TTL}", remaining);
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(true, remaining));
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
}
|
||||
|
||||
_waitingForTtlFetch = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to toggle broadcast for {Cid}", msg.HashedCid);
|
||||
_waitingForTtlFetch = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
|
||||
{
|
||||
_config.Current.BroadcastEnabled = msg.Enabled;
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
public async Task<bool> CheckIfBroadcastingAsync(string targetCid)
|
||||
{
|
||||
bool result = false;
|
||||
await RequireConnectionAsync(nameof(CheckIfBroadcastingAsync), async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("[BroadcastCheck] Checking CID: {cid}", targetCid);
|
||||
|
||||
var info = await _apiController.IsUserBroadcasting(targetCid).ConfigureAwait(false);
|
||||
result = info?.TTL > TimeSpan.Zero;
|
||||
|
||||
|
||||
_logger.LogDebug("[BroadcastCheck] Result for {cid}: {result} (TTL: {ttl}, GID: {gid})", targetCid, result, info?.TTL, info?.GID);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to check broadcast status for {cid}", targetCid);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<TimeSpan?> GetBroadcastTtlAsync(string cid)
|
||||
{
|
||||
TimeSpan? ttl = null;
|
||||
await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => {
|
||||
try
|
||||
{
|
||||
ttl = await _apiController.GetBroadcastTtl(cid).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch broadcast TTL for {cid}", cid);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
return ttl;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, BroadcastStatusInfoDto?>> AreUsersBroadcastingAsync(List<string> hashedCids)
|
||||
{
|
||||
Dictionary<string, BroadcastStatusInfoDto?> result = new();
|
||||
|
||||
await RequireConnectionAsync(nameof(AreUsersBroadcastingAsync), async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var batch = await _apiController.AreUsersBroadcasting(hashedCids).ConfigureAwait(false);
|
||||
|
||||
if (batch?.Results != null)
|
||||
{
|
||||
foreach (var kv in batch.Results)
|
||||
result[kv.Key] = kv.Value;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Batch broadcast status check complete for {Count} CIDs", hashedCids.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to batch check broadcast status");
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async void ToggleBroadcast()
|
||||
{
|
||||
if (!IsLightFinderAvailable)
|
||||
{
|
||||
_logger.LogWarning("ToggleBroadcast - Lightfinder is not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
await RequireConnectionAsync(nameof(ToggleBroadcast), async () =>
|
||||
{
|
||||
var cooldown = RemainingCooldown;
|
||||
if (!_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero)
|
||||
{
|
||||
_logger.LogWarning("Cooldown active. Must wait {Remaining}s before re-enabling.", cd.TotalSeconds);
|
||||
return;
|
||||
}
|
||||
|
||||
var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
||||
|
||||
try
|
||||
{
|
||||
var isCurrentlyBroadcasting = await CheckIfBroadcastingAsync(hashedCid).ConfigureAwait(false);
|
||||
var newStatus = !isCurrentlyBroadcasting;
|
||||
|
||||
if (!newStatus)
|
||||
{
|
||||
_lastForcedDisableTime = DateTime.UtcNow;
|
||||
_logger.LogDebug("Manual disable: cooldown timer started.");
|
||||
}
|
||||
|
||||
_logger.LogDebug("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus);
|
||||
|
||||
_mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to determine current broadcast status for toggle");
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnTick(PriorityFrameworkUpdateMessage _)
|
||||
{
|
||||
if (!IsLightFinderAvailable)
|
||||
return;
|
||||
|
||||
if (_config?.Current == null)
|
||||
return;
|
||||
|
||||
if ((DateTime.UtcNow - _lastTtlCheck).TotalSeconds < 1)
|
||||
return;
|
||||
|
||||
_lastTtlCheck = DateTime.UtcNow;
|
||||
|
||||
await RequireConnectionAsync(nameof(OnTick), async () => {
|
||||
if (!_syncedOnStartup && _config.Current.BroadcastEnabled)
|
||||
{
|
||||
_syncedOnStartup = true;
|
||||
try
|
||||
{
|
||||
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.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state.");
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh TTL in OnTick");
|
||||
}
|
||||
}
|
||||
if (_config.Current.BroadcastEnabled)
|
||||
{
|
||||
if (_waitingForTtlFetch)
|
||||
{
|
||||
_logger.LogDebug("OnTick skipped: waiting for TTL fetch");
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime expiry = _config.Current.BroadcastTtl;
|
||||
TimeSpan remaining = expiry - DateTime.UtcNow;
|
||||
_remainingTtl = remaining > TimeSpan.Zero ? remaining : null;
|
||||
if (_remainingTtl == null)
|
||||
{
|
||||
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_remainingTtl = null;
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
204
LightlessSync/Services/ContextMenuService.cs
Normal file
204
LightlessSync/Services/ContextMenuService.cs
Normal 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]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -313,7 +313,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return await RunOnFrameworkThread(() => _cid.Value.ToString().GetHash256()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private unsafe static string GetHashedCIDFromPlayerPointer(nint ptr)
|
||||
public unsafe static string GetHashedCIDFromPlayerPointer(nint ptr)
|
||||
{
|
||||
return ((BattleChara*)ptr)->Character.ContentId.ToString().GetHash256();
|
||||
}
|
||||
@@ -421,6 +421,16 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return await RunOnFrameworkThread(() => IsObjectPresent(obj)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public IPlayerCharacter? GetPlayerByNameAndWorld(string name, ushort homeWorldId)
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
return _objectTable
|
||||
.OfType<IPlayerCharacter>()
|
||||
.FirstOrDefault(p =>
|
||||
string.Equals(p.Name.TextValue, name, StringComparison.Ordinal) &&
|
||||
p.HomeWorld.RowId == homeWorldId);
|
||||
}
|
||||
|
||||
public async Task RunOnFrameworkThread(System.Action act, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
|
||||
@@ -531,7 +541,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
curWaitTime += tick;
|
||||
Thread.Sleep(tick);
|
||||
}
|
||||
|
||||
Thread.Sleep(tick * 2);
|
||||
}
|
||||
|
||||
@@ -547,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;
|
||||
|
||||
@@ -97,7 +97,10 @@ public record GPoseLobbyReceiveCharaData(CharaDataDownloadDto CharaDataDownloadD
|
||||
public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase;
|
||||
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
||||
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
|
||||
@@ -1,4 +1,5 @@
|
||||
using Dalamud.Interface.Windowing;
|
||||
using LightlessSync.UI.Style;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services.Mediator;
|
||||
@@ -33,6 +34,18 @@ public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public override void PreDraw()
|
||||
{
|
||||
base.PreDraw();
|
||||
MainStyle.PushStyle(); // internally checks ShouldUseTheme
|
||||
}
|
||||
|
||||
public override void PostDraw()
|
||||
{
|
||||
MainStyle.PopStyle(); // always attempts to pop if pushed
|
||||
base.PostDraw();
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
_performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal);
|
||||
|
||||
301
LightlessSync/Services/NameplateHandler.cs
Normal file
301
LightlessSync/Services/NameplateHandler.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.Utils;
|
||||
// Created using https://github.com/PunishedPineapple/Distance as a reference, thank you!
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
{
|
||||
private readonly ILogger<NameplateHandler> _logger;
|
||||
private readonly IAddonLifecycle _addonLifecycle;
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly LightlessMediator _mediator;
|
||||
public LightlessMediator Mediator => _mediator;
|
||||
|
||||
private bool mEnabled = false;
|
||||
private bool _needsLabelRefresh = false;
|
||||
private AddonNamePlate* mpNameplateAddon = null;
|
||||
private readonly AtkTextNode*[] mTextNodes = new AtkTextNode*[AddonNamePlate.NumNamePlateObjects];
|
||||
|
||||
internal const uint mNameplateNodeIDBase = 0x7D99D500;
|
||||
|
||||
private volatile HashSet<string> _activeBroadcastingCids = new();
|
||||
|
||||
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessMediator mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_addonLifecycle = addonLifecycle;
|
||||
_gameGui = gameGui;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
internal void Init()
|
||||
{
|
||||
EnableNameplate();
|
||||
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
||||
}
|
||||
|
||||
internal void Uninit()
|
||||
{
|
||||
DisableNameplate();
|
||||
DestroyNameplateNodes();
|
||||
_mediator.Unsubscribe<PriorityFrameworkUpdateMessage>(this);
|
||||
mpNameplateAddon = null;
|
||||
}
|
||||
|
||||
internal void EnableNameplate()
|
||||
{
|
||||
if (!mEnabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
_addonLifecycle.RegisterListener(AddonEvent.PostDraw, "NamePlate", NameplateDrawDetour);
|
||||
mEnabled = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError($"Unknown error while trying to enable nameplate distances:\n{e}");
|
||||
DisableNameplate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void DisableNameplate()
|
||||
{
|
||||
if (mEnabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
_addonLifecycle.UnregisterListener(NameplateDrawDetour);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError($"Unknown error while unregistering nameplate listener:\n{e}");
|
||||
}
|
||||
|
||||
mEnabled = false;
|
||||
HideAllNameplateNodes();
|
||||
}
|
||||
}
|
||||
|
||||
private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
|
||||
{
|
||||
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
|
||||
|
||||
if (mpNameplateAddon != pNameplateAddon)
|
||||
{
|
||||
for (int i = 0; i < mTextNodes.Length; ++i) mTextNodes[i] = null;
|
||||
mpNameplateAddon = pNameplateAddon;
|
||||
if (mpNameplateAddon != null) CreateNameplateNodes();
|
||||
}
|
||||
|
||||
UpdateNameplateNodes();
|
||||
}
|
||||
|
||||
private void CreateNameplateNodes()
|
||||
{
|
||||
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
|
||||
{
|
||||
var nameplateObject = GetNameplateObject(i);
|
||||
if (nameplateObject == null)
|
||||
continue;
|
||||
|
||||
var pNameplateResNode = nameplateObject.Value.NameContainer;
|
||||
var pNewNode = AtkNodeHelpers.CreateOrphanTextNode(mNameplateNodeIDBase + (uint)i, TextFlags.Edge | TextFlags.Glare);
|
||||
|
||||
if (pNewNode != null)
|
||||
{
|
||||
var pLastChild = pNameplateResNode->ChildNode;
|
||||
while (pLastChild->PrevSiblingNode != null) pLastChild = pLastChild->PrevSiblingNode;
|
||||
pNewNode->AtkResNode.NextSiblingNode = pLastChild;
|
||||
pNewNode->AtkResNode.ParentNode = pNameplateResNode;
|
||||
pLastChild->PrevSiblingNode = (AtkResNode*)pNewNode;
|
||||
nameplateObject.Value.RootComponentNode->Component->UldManager.UpdateDrawNodeList();
|
||||
pNewNode->AtkResNode.SetUseDepthBasedPriority(true);
|
||||
mTextNodes[i] = pNewNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyNameplateNodes()
|
||||
{
|
||||
var pCurrentNameplateAddon = (AddonNamePlate*)_gameGui.GetAddonByName("NamePlate", 1).Address;
|
||||
if (mpNameplateAddon == null || mpNameplateAddon != pCurrentNameplateAddon)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
|
||||
{
|
||||
var pTextNode = mTextNodes[i];
|
||||
var pNameplateNode = GetNameplateComponentNode(i);
|
||||
if (pTextNode != null && pNameplateNode != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (pTextNode->AtkResNode.PrevSiblingNode != null)
|
||||
pTextNode->AtkResNode.PrevSiblingNode->NextSiblingNode = pTextNode->AtkResNode.NextSiblingNode;
|
||||
if (pTextNode->AtkResNode.NextSiblingNode != null)
|
||||
pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode;
|
||||
pNameplateNode->Component->UldManager.UpdateDrawNodeList();
|
||||
pTextNode->AtkResNode.Destroy(true);
|
||||
mTextNodes[i] = null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError($"Unknown error while removing text node 0x{(IntPtr)pTextNode:X} for nameplate {i} on component node 0x{(IntPtr)pNameplateNode:X}:\n{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HideAllNameplateNodes()
|
||||
{
|
||||
for (int i = 0; i < mTextNodes.Length; ++i)
|
||||
{
|
||||
HideNameplateTextNode(i);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateNameplateNodes()
|
||||
{
|
||||
var framework = Framework.Instance();
|
||||
var ui3DModule = framework->GetUIModule()->GetUI3DModule();
|
||||
|
||||
if (ui3DModule == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
|
||||
{
|
||||
var objectInfo = ui3DModule->NamePlateObjectInfoPointers[i].Value;
|
||||
if (objectInfo == null || objectInfo->GameObject == null)
|
||||
continue;
|
||||
|
||||
var nameplateIndex = objectInfo->NamePlateIndex;
|
||||
if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects)
|
||||
continue;
|
||||
|
||||
var pNode = mTextNodes[nameplateIndex];
|
||||
if (pNode == null)
|
||||
continue;
|
||||
|
||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject);
|
||||
|
||||
//_logger.LogInformation($"checking cid: {cid}", cid);
|
||||
|
||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
pNode->AtkResNode.ToggleVisibility(true);
|
||||
|
||||
var nameplateObject = mpNameplateAddon->NamePlateObjectArray[nameplateIndex];
|
||||
nameplateObject.RootComponentNode->Component->UldManager.UpdateDrawNodeList();
|
||||
|
||||
var nameContainer = nameplateObject.NameContainer;
|
||||
var nameText = nameplateObject.NameText;
|
||||
|
||||
var labelColor = UIColors.Get("LightlessPurple");
|
||||
var edgeColor = UIColors.Get("FullBlack");
|
||||
|
||||
var labelY = nameContainer->Height - nameplateObject.TextH - (int)(24 * nameText->AtkResNode.ScaleY);
|
||||
|
||||
pNode->AtkResNode.SetPositionShort(58, (short)labelY);
|
||||
pNode->AtkResNode.SetUseDepthBasedPriority(true);
|
||||
pNode->AtkResNode.SetScale(0.5f, 0.5f);
|
||||
|
||||
pNode->AtkResNode.Color.A = 255;
|
||||
|
||||
pNode->TextColor.R = (byte)(labelColor.X * 255);
|
||||
pNode->TextColor.G = (byte)(labelColor.Y * 255);
|
||||
pNode->TextColor.B = (byte)(labelColor.Z * 255);
|
||||
pNode->TextColor.A = (byte)(labelColor.W * 255);
|
||||
|
||||
pNode->EdgeColor.R = (byte)(edgeColor.X * 255);
|
||||
pNode->EdgeColor.G = (byte)(edgeColor.Y * 255);
|
||||
pNode->EdgeColor.B = (byte)(edgeColor.Z * 255);
|
||||
pNode->EdgeColor.A = (byte)(edgeColor.W * 255);
|
||||
|
||||
pNode->FontSize = 24;
|
||||
pNode->AlignmentType = AlignmentType.Center;
|
||||
pNode->FontType = FontType.MiedingerMed;
|
||||
pNode->LineSpacing = 24;
|
||||
pNode->CharSpacing = 1;
|
||||
|
||||
pNode->TextFlags = TextFlags.Edge | TextFlags.Glare;
|
||||
|
||||
pNode->SetText("Lightfinder");
|
||||
}
|
||||
}
|
||||
|
||||
private void HideNameplateTextNode(int i)
|
||||
{
|
||||
var pNode = mTextNodes[i];
|
||||
if (pNode != null)
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
private AddonNamePlate.NamePlateObject? GetNameplateObject(int i)
|
||||
{
|
||||
if (i < AddonNamePlate.NumNamePlateObjects &&
|
||||
mpNameplateAddon != null &&
|
||||
mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null)
|
||||
{
|
||||
return mpNameplateAddon->NamePlateObjectArray[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AtkComponentNode* GetNameplateComponentNode(int i)
|
||||
{
|
||||
var nameplateObject = GetNameplateObject(i);
|
||||
return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null;
|
||||
}
|
||||
|
||||
public void FlagRefresh()
|
||||
{
|
||||
_needsLabelRefresh = true;
|
||||
}
|
||||
|
||||
public void OnTick(PriorityFrameworkUpdateMessage _)
|
||||
{
|
||||
if (_needsLabelRefresh)
|
||||
{
|
||||
UpdateNameplateNodes();
|
||||
_needsLabelRefresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
||||
{
|
||||
var newSet = cids.ToHashSet();
|
||||
|
||||
var changed = !_activeBroadcastingCids.SetEquals(newSet);
|
||||
if (!changed)
|
||||
return;
|
||||
|
||||
_activeBroadcastingCids.Clear();
|
||||
foreach (var cid in newSet)
|
||||
_activeBroadcastingCids.Add(cid);
|
||||
|
||||
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(",", _activeBroadcastingCids));
|
||||
|
||||
FlagRefresh();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -8,16 +8,17 @@ using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public class NameplateService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly ILogger<NameplateService> _logger;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly INamePlateGui _namePlateGui;
|
||||
private readonly PairManager _pairManager;
|
||||
|
||||
|
||||
public NameplateService(ILogger<NameplateService> logger,
|
||||
LightlessConfigService configService,
|
||||
INamePlateGui namePlateGui,
|
||||
@@ -25,10 +26,12 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
||||
PairManager pairManager,
|
||||
LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_namePlateGui = namePlateGui;
|
||||
_clientState = clientState;
|
||||
_pairManager = pairManager;
|
||||
|
||||
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
|
||||
_namePlateGui.RequestRedraw();
|
||||
Mediator.Subscribe<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw());
|
||||
@@ -37,13 +40,23 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
||||
|
||||
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||
{
|
||||
if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen)) return;
|
||||
var visibleUsersIds = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue).Select(u => (ulong)u.PlayerCharacterId).ToHashSet();
|
||||
if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen))
|
||||
return;
|
||||
|
||||
var visibleUsersIds = _pairManager.GetOnlineUserPairs()
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)
|
||||
.ToHashSet();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var colors = _configService.Current.NameplateColors;
|
||||
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
var playerCharacter = handler.PlayerCharacter;
|
||||
if (playerCharacter == null) { continue; }
|
||||
if (playerCharacter == null)
|
||||
continue;
|
||||
|
||||
var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember);
|
||||
var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend);
|
||||
bool partyColorAllowed = (_configService.Current.overridePartyColor && isInParty);
|
||||
@@ -56,13 +69,25 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
||||
))
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -80,12 +105,11 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
||||
return (left.ToReadOnlySeString().ToDalamudString(), right.ToReadOnlySeString().ToDalamudString());
|
||||
}
|
||||
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
_namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate;
|
||||
_namePlateGui.RequestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
189
LightlessSync/Services/PairRequestService.cs
Normal file
189
LightlessSync/Services/PairRequestService.cs
Normal 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);
|
||||
}
|
||||
421
LightlessSync/UI/BroadcastUI.cs
Normal file
421
LightlessSync/UI/BroadcastUI.cs
Normal file
@@ -0,0 +1,421 @@
|
||||
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;
|
||||
|
||||
namespace LightlessSync.UI
|
||||
{
|
||||
public class BroadcastUI : WindowMediatorSubscriberBase
|
||||
{
|
||||
private readonly ApiController _apiController;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly BroadcastService _broadcastService;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly BroadcastScannerService _broadcastScannerService;
|
||||
|
||||
private IReadOnlyList<GroupFullInfoDto> _allSyncshells;
|
||||
private string _userUid = string.Empty;
|
||||
|
||||
private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new();
|
||||
|
||||
public BroadcastUI(
|
||||
ILogger<BroadcastUI> logger,
|
||||
LightlessMediator mediator,
|
||||
PerformanceCollectorService performanceCollectorService,
|
||||
BroadcastService broadcastService,
|
||||
LightlessConfigService configService,
|
||||
UiSharedService uiShared,
|
||||
ApiController apiController,
|
||||
BroadcastScannerService broadcastScannerService
|
||||
) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
|
||||
{
|
||||
_broadcastService = broadcastService;
|
||||
_uiSharedService = uiShared;
|
||||
_configService = configService;
|
||||
_apiController = apiController;
|
||||
_broadcastScannerService = broadcastScannerService;
|
||||
|
||||
IsOpen = false;
|
||||
this.SizeConstraints = new()
|
||||
{
|
||||
MinimumSize = new(600, 465),
|
||||
MaximumSize = new(750, 525)
|
||||
};
|
||||
|
||||
mediator.Subscribe<RefreshUiMessage>(this, async _ => await RefreshSyncshells().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
private void RebuildSyncshellDropdownOptions()
|
||||
{
|
||||
var selectedGid = _configService.Current.SelectedFinderSyncshell;
|
||||
var allSyncshells = _allSyncshells ?? Array.Empty<GroupFullInfoDto>();
|
||||
var ownedSyncshells = allSyncshells
|
||||
.Where(g => string.Equals(g.OwnerUID, _userUid, StringComparison.Ordinal))
|
||||
.ToList();
|
||||
|
||||
_syncshellOptions.Clear();
|
||||
_syncshellOptions.Add(("None", null, true));
|
||||
|
||||
var addedGids = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var shell in ownedSyncshells)
|
||||
{
|
||||
var label = shell.GroupAliasOrGID ?? shell.GID;
|
||||
_syncshellOptions.Add((label, shell.GID, true));
|
||||
addedGids.Add(shell.GID);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid))
|
||||
{
|
||||
var matching = allSyncshells.FirstOrDefault(g => string.Equals(g.GID, selectedGid, StringComparison.Ordinal));
|
||||
if (matching != null)
|
||||
{
|
||||
var label = matching.GroupAliasOrGID ?? matching.GID;
|
||||
_syncshellOptions.Add((label, matching.GID, true));
|
||||
addedGids.Add(matching.GID);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid))
|
||||
{
|
||||
_syncshellOptions.Add(($"[Unavailable] {selectedGid}", selectedGid, false));
|
||||
}
|
||||
}
|
||||
|
||||
public Task RefreshSyncshells()
|
||||
{
|
||||
return RefreshSyncshellsInternal();
|
||||
}
|
||||
|
||||
private async Task RefreshSyncshellsInternal()
|
||||
{
|
||||
if (!_apiController.IsConnected)
|
||||
{
|
||||
_allSyncshells = [];
|
||||
RebuildSyncshellDropdownOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_allSyncshells = await _apiController.GroupsGetAll().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch Syncshells.");
|
||||
_allSyncshells = [];
|
||||
}
|
||||
|
||||
RebuildSyncshellDropdownOptions();
|
||||
}
|
||||
|
||||
|
||||
public override void OnOpen()
|
||||
{
|
||||
_userUid = _apiController.UID;
|
||||
_ = RefreshSyncshellsInternal();
|
||||
}
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
if (!_broadcastService.IsLightFinderAvailable)
|
||||
{
|
||||
_uiSharedService.MediumText("This server doesn't support Lightfinder.", UIColors.Get("LightlessYellow"));
|
||||
|
||||
ImGuiHelpers.ScaledDummy(0.25f);
|
||||
}
|
||||
|
||||
if (ImGui.BeginTabBar("##BroadcastTabs"))
|
||||
{
|
||||
if (ImGui.BeginTabItem("Lightfinder"))
|
||||
{
|
||||
_uiSharedService.MediumText("Lightfinder", UIColors.Get("PairBlue"));
|
||||
|
||||
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.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();
|
||||
|
||||
ImGui.PopStyleVar();
|
||||
|
||||
ImGuiHelpers.ScaledDummy(3f);
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
||||
|
||||
if (_configService.Current.BroadcastEnabled)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGreen"));
|
||||
ImGui.Text("The Lightfinder calls, and somewhere, a soul may answer."); // cringe..
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
var ttl = _broadcastService.RemainingTtl;
|
||||
if (ttl is { } remaining && remaining > TimeSpan.Zero)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
|
||||
ImGui.Text($"Still shining, for {remaining:hh\\:mm\\:ss}");
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
ImGui.Text("The Lightfinder<65>s light wanes, but not in vain."); // cringe..
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
ImGui.Text("The Lightfinder rests, waiting to shine again."); // cringe..
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
||||
var cooldown = _broadcastService.RemainingCooldown;
|
||||
if (cooldown is { } cd)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
ImGui.Text($"The Lightfinder gathers its strength... ({Math.Ceiling(cd.TotalSeconds)}s)");
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(0.5f);
|
||||
|
||||
bool isBroadcasting = _broadcastService.IsBroadcasting;
|
||||
bool isOnCooldown = cooldown.HasValue && cooldown.Value.TotalSeconds > 0;
|
||||
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f);
|
||||
|
||||
if (isOnCooldown)
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("DimRed"));
|
||||
else if (isBroadcasting)
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
|
||||
else
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("PairBlue"));
|
||||
|
||||
if (isOnCooldown || !_broadcastService.IsLightFinderAvailable)
|
||||
ImGui.BeginDisabled();
|
||||
|
||||
string buttonText = isBroadcasting ? "Disable Lightfinder" : "Enable Lightfinder";
|
||||
|
||||
if (ImGui.Button(buttonText, new Vector2(200 * ImGuiHelpers.GlobalScale, 0)))
|
||||
{
|
||||
_broadcastService.ToggleBroadcast();
|
||||
}
|
||||
|
||||
if (isOnCooldown || !_broadcastService.IsLightFinderAvailable)
|
||||
ImGui.EndDisabled();
|
||||
|
||||
ImGui.PopStyleColor();
|
||||
ImGui.PopStyleVar();
|
||||
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui.BeginTabItem("Syncshell Finder"))
|
||||
{
|
||||
if (_allSyncshells == null)
|
||||
{
|
||||
ImGui.Text("Loading Syncshells...");
|
||||
return;
|
||||
}
|
||||
|
||||
_uiSharedService.MediumText("Syncshell Finder", UIColors.Get("PairBlue"));
|
||||
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
||||
|
||||
ImGui.PushTextWrapPos();
|
||||
ImGui.Text("Allow your owned Syncshell to be indexed by the Nearby Syncshell Finder.");
|
||||
ImGui.Text("To enable this, select one of your owned Syncshells from the dropdown menu below and ensure that \"Toggle Syncshell Finder\" is enabled. Your Syncshell will be visible in the Nearby Syncshell Finder as long as Lightfinder is active.");
|
||||
ImGui.PopTextWrapPos();
|
||||
|
||||
ImGuiHelpers.ScaledDummy(0.2f);
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
||||
|
||||
bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled;
|
||||
bool isBroadcasting = _broadcastService.IsBroadcasting;
|
||||
|
||||
if (isBroadcasting)
|
||||
ImGui.BeginDisabled();
|
||||
|
||||
if (ImGui.Checkbox("Toggle Syncshell Finder", ref ShellFinderEnabled))
|
||||
{
|
||||
_configService.Current.SyncshellFinderEnabled = ShellFinderEnabled;
|
||||
_configService.Save();
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.Text("Toggle to broadcast specified Syncshell.");
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
var selectedGid = _configService.Current.SelectedFinderSyncshell;
|
||||
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 = string.Equals(gid, selectedGid, StringComparison.Ordinal);
|
||||
|
||||
if (!available)
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
|
||||
if (ImGui.Selectable(label, isSelected))
|
||||
{
|
||||
_configService.Current.SelectedFinderSyncshell = gid;
|
||||
_configService.Save();
|
||||
}
|
||||
|
||||
if (!available && ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.Text("This Syncshell is not available on the current service.");
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
if (!available)
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
if (isSelected)
|
||||
ImGui.SetItemDefaultFocus();
|
||||
}
|
||||
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.Text("Choose one of the available options.");
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
|
||||
if (isBroadcasting)
|
||||
ImGui.EndDisabled();
|
||||
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
if (ImGui.BeginTabItem("Debug"))
|
||||
{
|
||||
ImGui.Text("Broadcast Cache");
|
||||
|
||||
if (ImGui.BeginTable("##BroadcastCacheTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 225f)))
|
||||
{
|
||||
ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn("Expires In", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn("Syncshell GID", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
foreach (var (cid, entry) in _broadcastScannerService.BroadcastCache)
|
||||
{
|
||||
ImGui.TableNextRow();
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(cid.Truncate(12));
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.TextUnformatted(cid);
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
var colorBroadcast = entry.IsBroadcasting
|
||||
? UIColors.Get("LightlessGreen")
|
||||
: UIColors.Get("DimRed");
|
||||
|
||||
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorBroadcast));
|
||||
ImGui.TextUnformatted(entry.IsBroadcasting.ToString());
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
var remaining = entry.ExpiryTime - now;
|
||||
var colorTtl =
|
||||
remaining <= TimeSpan.Zero ? UIColors.Get("DimRed") :
|
||||
remaining < TimeSpan.FromSeconds(10) ? UIColors.Get("LightlessYellow") :
|
||||
(Vector4?)null;
|
||||
|
||||
if (colorTtl != null)
|
||||
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorTtl.Value));
|
||||
|
||||
ImGui.TextUnformatted(remaining > TimeSpan.Zero
|
||||
? remaining.ToString("hh\\:mm\\:ss")
|
||||
: "Expired");
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(entry.GID ?? "-");
|
||||
}
|
||||
|
||||
ImGui.EndTable();
|
||||
}
|
||||
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
#endif
|
||||
|
||||
ImGui.EndTabBar();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -51,6 +52,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
private readonly TopTabMenu _tabMenu;
|
||||
private readonly TagHandler _tagHandler;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly BroadcastService _broadcastService;
|
||||
|
||||
private List<IDrawFolder> _drawFolders;
|
||||
private Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>? _cachedAnalysis;
|
||||
private Pair? _lastAddedUser;
|
||||
@@ -62,13 +65,28 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
private bool _wasOpen;
|
||||
private float _windowContentWidth;
|
||||
|
||||
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, LightlessConfigService configService, ApiController apiController, PairManager pairManager,
|
||||
ServerConfigurationManager serverManager, LightlessMediator mediator, FileUploadManager fileTransferManager,
|
||||
TagHandler tagHandler, DrawEntityFactory drawEntityFactory,
|
||||
SelectTagForPairUi selectTagForPairUi, SelectPairForTagUi selectPairForTagUi, RenamePairTagUi renameTagUi,
|
||||
SelectTagForSyncshellUi selectTagForSyncshellUi, SelectSyncshellForTagUi selectSyncshellForTagUi, RenameSyncshellTagUi renameSyncshellTagUi,
|
||||
PerformanceCollectorService performanceCollectorService, IpcManager ipcManager, CharacterAnalyzer characterAnalyzer, PlayerPerformanceConfigService playerPerformanceConfig, LightlessMediator lightlessMediator)
|
||||
: base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
|
||||
public CompactUi(
|
||||
ILogger<CompactUi> logger,
|
||||
UiSharedService uiShared,
|
||||
LightlessConfigService configService,
|
||||
ApiController apiController,
|
||||
PairManager pairManager,
|
||||
ServerConfigurationManager serverManager,
|
||||
LightlessMediator mediator,
|
||||
FileUploadManager fileTransferManager,
|
||||
TagHandler tagHandler,
|
||||
DrawEntityFactory drawEntityFactory,
|
||||
SelectTagForPairUi selectTagForPairUi,
|
||||
SelectPairForTagUi selectPairForTagUi,
|
||||
RenamePairTagUi renameTagUi,
|
||||
SelectTagForSyncshellUi selectTagForSyncshellUi,
|
||||
SelectSyncshellForTagUi selectSyncshellForTagUi,
|
||||
RenameSyncshellTagUi renameSyncshellTagUi,
|
||||
PerformanceCollectorService performanceCollectorService,
|
||||
IpcManager ipcManager,
|
||||
BroadcastService broadcastService,
|
||||
CharacterAnalyzer characterAnalyzer,
|
||||
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
|
||||
{
|
||||
_uiSharedService = uiShared;
|
||||
_configService = configService;
|
||||
@@ -85,7 +103,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
_selectPairsForGroupUi = selectPairForTagUi;
|
||||
_renamePairTagUi = renameTagUi;
|
||||
_ipcManager = ipcManager;
|
||||
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService);
|
||||
_broadcastService = broadcastService;
|
||||
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService);
|
||||
|
||||
AllowPinning = true;
|
||||
AllowClickthrough = false;
|
||||
@@ -120,10 +139,10 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
ImGui.Text("Open Lightless Event Viewer");
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_drawFolders = [.. GetDrawFolders()];
|
||||
_drawFolders = [.. DrawFolders];
|
||||
|
||||
#if DEBUG
|
||||
string dev = "Dev Build";
|
||||
@@ -140,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;
|
||||
|
||||
@@ -151,7 +170,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
};
|
||||
_characterAnalyzer = characterAnalyzer;
|
||||
_playerPerformanceConfig = playerPerformanceConfig;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_lightlessMediator = mediator;
|
||||
}
|
||||
|
||||
protected override void DrawInternal()
|
||||
@@ -202,7 +221,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
|
||||
using (ImRaii.PushId("header")) DrawUIDHeader();
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
||||
_uiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f);
|
||||
using (ImRaii.PushId("serverstatus")) DrawServerStatus();
|
||||
ImGui.Separator();
|
||||
|
||||
@@ -383,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);
|
||||
@@ -410,26 +429,165 @@ 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();
|
||||
|
||||
Vector2 uidTextSize, iconSize;
|
||||
using (_uiSharedService.UidFont.Push())
|
||||
uidTextSize = ImGui.CalcTextSize(uidText);
|
||||
|
||||
using (_uiSharedService.IconFont.Push())
|
||||
iconSize = ImGui.CalcTextSize(FontAwesomeIcon.PersonCirclePlus.ToIconString());
|
||||
|
||||
float contentWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
||||
float uidStartX = (contentWidth - uidTextSize.X) / 2f;
|
||||
float cursorY = ImGui.GetCursorPosY();
|
||||
|
||||
if (_configService.Current.BroadcastEnabled && _apiController.IsConnected)
|
||||
{
|
||||
var uidTextSize = ImGui.CalcTextSize(uidText);
|
||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (uidTextSize.X / 2));
|
||||
ImGui.TextColored(GetUidColor(), uidText);
|
||||
float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f;
|
||||
var buttonSize = new Vector2(iconSize.X, uidTextSize.Y);
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(ImGui.GetStyle().ItemSpacing.X + 5f, cursorY));
|
||||
ImGui.InvisibleButton("BroadcastIcon", buttonSize);
|
||||
|
||||
var iconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset);
|
||||
using (_uiSharedService.IconFont.Push())
|
||||
ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.PersonCirclePlus.ToIconString());
|
||||
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("PairBlue"));
|
||||
ImGui.Text("Lightfinder");
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
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);
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
||||
|
||||
if (_configService.Current.BroadcastEnabled)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGreen"));
|
||||
ImGui.Text("The Lightfinder calls, and somewhere, a soul may answer."); // cringe..
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
var ttl = _broadcastService.RemainingTtl;
|
||||
if (ttl is { } remaining && remaining > TimeSpan.Zero)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
|
||||
ImGui.Text($"Still shining, for {remaining:hh\\:mm\\:ss}");
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
ImGui.Text("The Lightfinder's light wanes, but not in vain."); // cringe..
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
ImGui.Text("The Lightfinder rests, waiting to shine again."); // cringe..
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
||||
var cooldown = _broadcastService.RemainingCooldown;
|
||||
if (cooldown is { } cd)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
ImGui.Text($"The Lightfinder gathers its strength... ({Math.Ceiling(cd.TotalSeconds)}s)");
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
if (ImGui.IsItemClicked())
|
||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip("Click to copy");
|
||||
if (ImGui.IsItemClicked())
|
||||
ImGui.SetCursorPosY(cursorY);
|
||||
ImGui.SetCursorPosX(uidStartX);
|
||||
|
||||
bool headerItemClicked;
|
||||
using (_uiSharedService.UidFont.Push())
|
||||
{
|
||||
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)
|
||||
{
|
||||
var firstEntry = _cachedAnalysis.FirstOrDefault();
|
||||
@@ -459,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 = "";
|
||||
@@ -486,7 +645,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
|
||||
if (_apiController.ServerState is ServerState.Connected)
|
||||
{
|
||||
if (ImGui.IsItemClicked())
|
||||
if (headerItemClicked)
|
||||
{
|
||||
ImGui.SetClipboardText(_apiController.DisplayName);
|
||||
}
|
||||
@@ -495,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)));
|
||||
}
|
||||
@@ -509,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
|
||||
|
||||
@@ -34,8 +34,6 @@ public class DrawGroupedGroupFolder : IDrawFolder
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
if (!_groups.Any()) return;
|
||||
|
||||
string _id = "__folder_syncshells";
|
||||
if (_tag != "")
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
@@ -12,6 +11,7 @@ using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI.Handlers;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
|
||||
namespace LightlessSync.UI.Components;
|
||||
@@ -295,6 +295,31 @@ public class DrawUserPair
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (_pair.UserData.IsAdmin || _pair.UserData.IsModerator)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
|
||||
var iconId = _pair.UserData.IsAdmin ? 67 : 68;
|
||||
var colorKey = _pair.UserData.IsAdmin ? "LightlessAdminText" : "LightlessModeratorText";
|
||||
var roleColor = UIColors.Get(colorKey);
|
||||
|
||||
var iconPos = ImGui.GetCursorScreenPos();
|
||||
SeStringUtils.RenderIconWithHitbox(iconId, iconPos);
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, roleColor))
|
||||
{
|
||||
ImGui.TextUnformatted(_pair.UserData.IsAdmin
|
||||
? "Official Lightless Admin"
|
||||
: "Official Lightless Moderator");
|
||||
}
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void DrawName(float leftSide, float rightSide)
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
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;
|
||||
|
||||
@@ -22,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;
|
||||
@@ -89,11 +97,108 @@ public class IdDisplayHandler
|
||||
{
|
||||
ImGui.SameLine(textPosX);
|
||||
(bool textIsUid, string playerText) = GetPlayerText(pair);
|
||||
|
||||
if (!string.Equals(_editEntry, pair.UserData.UID, StringComparison.Ordinal))
|
||||
{
|
||||
ImGui.AlignTextToFramePadding();
|
||||
|
||||
using (ImRaii.PushFont(UiBuilder.MonoFont, textIsUid)) ImGui.TextUnformatted(playerText);
|
||||
var font = textIsUid ? UiBuilder.MonoFont : ImGui.GetFont();
|
||||
|
||||
Vector4? textColor = null;
|
||||
Vector4? glowColor = null;
|
||||
|
||||
if (pair.UserData.HasVanity)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(pair.UserData.TextColorHex))
|
||||
{
|
||||
textColor = UIColors.HexToRgba(pair.UserData.TextColorHex);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(pair.UserData.TextGlowColorHex))
|
||||
{
|
||||
glowColor = UIColors.HexToRgba(pair.UserData.TextGlowColorHex);
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
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())
|
||||
{
|
||||
@@ -173,10 +278,12 @@ public class IdDisplayHandler
|
||||
{
|
||||
_editEntry = string.Empty;
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public (bool isGid, string text) GetGroupText(GroupFullInfoDto group)
|
||||
{
|
||||
var textIsGid = true;
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
@@ -921,6 +921,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
var showUidInDtrTooltip = _configService.Current.ShowUidInDtrTooltip;
|
||||
var preferNoteInDtrTooltip = _configService.Current.PreferNoteInDtrTooltip;
|
||||
var useColorsInDtr = _configService.Current.UseColorsInDtr;
|
||||
var useLightlessRedesign = _configService.Current.UseLightlessRedesign;
|
||||
var dtrColorsDefault = _configService.Current.DtrColorsDefault;
|
||||
var dtrColorsNotConnected = _configService.Current.DtrColorsNotConnected;
|
||||
var dtrColorsPairsInRange = _configService.Current.DtrColorsPairsInRange;
|
||||
@@ -977,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();
|
||||
@@ -1019,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))
|
||||
@@ -1053,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))
|
||||
{
|
||||
@@ -1089,8 +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();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -1145,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))
|
||||
@@ -2228,4 +2293,4 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
_wasOpen = IsOpen;
|
||||
IsOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
64
LightlessSync/UI/Style/Luminance.cs
Normal file
64
LightlessSync/UI/Style/Luminance.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
169
LightlessSync/UI/Style/MainStyle.cs
Normal file
169
LightlessSync/UI/Style/MainStyle.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
// inspiration: brio because it's style is fucking amazing
|
||||
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using System.Numerics;
|
||||
|
||||
namespace LightlessSync.UI.Style
|
||||
{
|
||||
internal static class MainStyle
|
||||
{
|
||||
private static LightlessConfigService? _config;
|
||||
public static void Init(LightlessConfigService config) => _config = config;
|
||||
public static bool ShouldUseTheme => _config?.Current.UseLightlessRedesign ?? false;
|
||||
|
||||
private static bool _hasPushed;
|
||||
private static int _pushedColorCount;
|
||||
private static int _pushedStyleVarCount;
|
||||
|
||||
public static void PushStyle()
|
||||
{
|
||||
if (_hasPushed)
|
||||
PopStyle();
|
||||
|
||||
if (!ShouldUseTheme)
|
||||
{
|
||||
_hasPushed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_hasPushed = true;
|
||||
_pushedColorCount = 0;
|
||||
_pushedStyleVarCount = 0;
|
||||
|
||||
Push(ImGuiCol.Text, new Vector4(255, 255, 255, 255));
|
||||
Push(ImGuiCol.TextDisabled, new Vector4(128, 128, 128, 255));
|
||||
|
||||
Push(ImGuiCol.WindowBg, new Vector4(23, 23, 23, 248));
|
||||
Push(ImGuiCol.ChildBg, new Vector4(23, 23, 23, 66));
|
||||
Push(ImGuiCol.PopupBg, new Vector4(23, 23, 23, 248));
|
||||
|
||||
Push(ImGuiCol.Border, new Vector4(65, 65, 65, 255));
|
||||
Push(ImGuiCol.BorderShadow, new Vector4(0, 0, 0, 150));
|
||||
|
||||
Push(ImGuiCol.FrameBg, new Vector4(40, 40, 40, 255));
|
||||
Push(ImGuiCol.FrameBgHovered, new Vector4(50, 50, 50, 255));
|
||||
Push(ImGuiCol.FrameBgActive, new Vector4(30, 30, 30, 255));
|
||||
|
||||
Push(ImGuiCol.TitleBg, new Vector4(24, 24, 24, 232));
|
||||
Push(ImGuiCol.TitleBgActive, new Vector4(30, 30, 30, 255));
|
||||
Push(ImGuiCol.TitleBgCollapsed, new Vector4(27, 27, 27, 255));
|
||||
|
||||
Push(ImGuiCol.MenuBarBg, new Vector4(36, 36, 36, 255));
|
||||
Push(ImGuiCol.ScrollbarBg, new Vector4(0, 0, 0, 0));
|
||||
Push(ImGuiCol.ScrollbarGrab, new Vector4(62, 62, 62, 255));
|
||||
Push(ImGuiCol.ScrollbarGrabHovered, new Vector4(70, 70, 70, 255));
|
||||
Push(ImGuiCol.ScrollbarGrabActive, new Vector4(70, 70, 70, 255));
|
||||
|
||||
Push(ImGuiCol.CheckMark, UIColors.Get("LightlessPurple"));
|
||||
|
||||
Push(ImGuiCol.SliderGrab, new Vector4(101, 101, 101, 255));
|
||||
Push(ImGuiCol.SliderGrabActive, new Vector4(123, 123, 123, 255));
|
||||
|
||||
Push(ImGuiCol.Button, UIColors.Get("ButtonDefault"));
|
||||
Push(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple"));
|
||||
Push(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive"));
|
||||
|
||||
Push(ImGuiCol.Header, new Vector4(0, 0, 0, 60));
|
||||
Push(ImGuiCol.HeaderHovered, new Vector4(0, 0, 0, 90));
|
||||
Push(ImGuiCol.HeaderActive, new Vector4(0, 0, 0, 120));
|
||||
|
||||
Push(ImGuiCol.Separator, new Vector4(75, 75, 75, 121));
|
||||
Push(ImGuiCol.SeparatorHovered, UIColors.Get("LightlessPurple"));
|
||||
Push(ImGuiCol.SeparatorActive, UIColors.Get("LightlessPurpleActive"));
|
||||
|
||||
Push(ImGuiCol.ResizeGrip, new Vector4(0, 0, 0, 0));
|
||||
Push(ImGuiCol.ResizeGripHovered, new Vector4(0, 0, 0, 0));
|
||||
Push(ImGuiCol.ResizeGripActive, UIColors.Get("LightlessPurpleActive"));
|
||||
|
||||
Push(ImGuiCol.Tab, new Vector4(40, 40, 40, 255));
|
||||
Push(ImGuiCol.TabHovered, UIColors.Get("LightlessPurple"));
|
||||
Push(ImGuiCol.TabActive, UIColors.Get("LightlessPurpleActive"));
|
||||
Push(ImGuiCol.TabUnfocused, new Vector4(40, 40, 40, 255));
|
||||
Push(ImGuiCol.TabUnfocusedActive, UIColors.Get("LightlessPurpleActive"));
|
||||
|
||||
Push(ImGuiCol.DockingPreview, UIColors.Get("LightlessPurpleActive"));
|
||||
Push(ImGuiCol.DockingEmptyBg, new Vector4(50, 50, 50, 255));
|
||||
|
||||
Push(ImGuiCol.PlotLines, new Vector4(150, 150, 150, 255));
|
||||
|
||||
Push(ImGuiCol.TableHeaderBg, new Vector4(48, 48, 48, 255));
|
||||
Push(ImGuiCol.TableBorderStrong, new Vector4(79, 79, 89, 255));
|
||||
Push(ImGuiCol.TableBorderLight, new Vector4(59, 59, 64, 255));
|
||||
Push(ImGuiCol.TableRowBg, new Vector4(0, 0, 0, 0));
|
||||
Push(ImGuiCol.TableRowBgAlt, new Vector4(255, 255, 255, 15));
|
||||
|
||||
Push(ImGuiCol.TextSelectedBg, new Vector4(98, 75, 224, 255));
|
||||
Push(ImGuiCol.DragDropTarget, new Vector4(98, 75, 224, 255));
|
||||
|
||||
Push(ImGuiCol.NavHighlight, new Vector4(98, 75, 224, 179));
|
||||
Push(ImGuiCol.NavWindowingDimBg, new Vector4(204, 204, 204, 51));
|
||||
Push(ImGuiCol.NavWindowingHighlight, new Vector4(204, 204, 204, 89));
|
||||
|
||||
PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(6, 6));
|
||||
PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(4, 3));
|
||||
PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(4, 4));
|
||||
PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(4, 4));
|
||||
PushStyleVar(ImGuiStyleVar.ItemInnerSpacing, new Vector2(4, 4));
|
||||
|
||||
PushStyleVar(ImGuiStyleVar.IndentSpacing, 21.0f);
|
||||
PushStyleVar(ImGuiStyleVar.ScrollbarSize, 10.0f);
|
||||
PushStyleVar(ImGuiStyleVar.GrabMinSize, 20.0f);
|
||||
|
||||
PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1.5f);
|
||||
PushStyleVar(ImGuiStyleVar.ChildBorderSize, 1.5f);
|
||||
PushStyleVar(ImGuiStyleVar.PopupBorderSize, 1.5f);
|
||||
PushStyleVar(ImGuiStyleVar.FrameBorderSize, 0f);
|
||||
|
||||
PushStyleVar(ImGuiStyleVar.WindowRounding, 7f);
|
||||
PushStyleVar(ImGuiStyleVar.ChildRounding, 4f);
|
||||
PushStyleVar(ImGuiStyleVar.FrameRounding, 4f);
|
||||
PushStyleVar(ImGuiStyleVar.PopupRounding, 4f);
|
||||
PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 4f);
|
||||
PushStyleVar(ImGuiStyleVar.GrabRounding, 4f);
|
||||
PushStyleVar(ImGuiStyleVar.TabRounding, 4f);
|
||||
}
|
||||
|
||||
public static void PopStyle()
|
||||
{
|
||||
if (!_hasPushed)
|
||||
return;
|
||||
|
||||
if (_pushedStyleVarCount > 0)
|
||||
ImGui.PopStyleVar(_pushedStyleVarCount);
|
||||
if (_pushedColorCount > 0)
|
||||
ImGui.PopStyleColor(_pushedColorCount);
|
||||
|
||||
_hasPushed = false;
|
||||
_pushedColorCount = 0;
|
||||
_pushedStyleVarCount = 0;
|
||||
}
|
||||
|
||||
private static void Push(ImGuiCol col, Vector4 rgba)
|
||||
{
|
||||
if (rgba.X > 1f || rgba.Y > 1f || rgba.Z > 1f || rgba.W > 1f)
|
||||
rgba /= 255f;
|
||||
|
||||
ImGui.PushStyleColor(col, rgba);
|
||||
_pushedColorCount++;
|
||||
}
|
||||
|
||||
private static void Push(ImGuiCol col, uint packedRgba)
|
||||
{
|
||||
ImGui.PushStyleColor(col, packedRgba);
|
||||
_pushedColorCount++;
|
||||
}
|
||||
|
||||
private static void PushStyleVar(ImGuiStyleVar var, float value)
|
||||
{
|
||||
ImGui.PushStyleVar(var, value);
|
||||
_pushedStyleVarCount++;
|
||||
}
|
||||
|
||||
private static void PushStyleVar(ImGuiStyleVar var, Vector2 value)
|
||||
{
|
||||
ImGui.PushStyleVar(var, value);
|
||||
_pushedStyleVarCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
335
LightlessSync/UI/SyncshellFinderUI.cs
Normal file
335
LightlessSync/UI/SyncshellFinderUI.cs
Normal file
@@ -0,0 +1,335 @@
|
||||
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.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
{
|
||||
private readonly ApiController _apiController;
|
||||
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 = [];
|
||||
private List<GroupFullInfoDto> _currentSyncshells = [];
|
||||
private int _selectedNearbyIndex = -1;
|
||||
|
||||
private GroupJoinDto? _joinDto;
|
||||
private GroupJoinInfoDto? _joinInfo;
|
||||
private DefaultPermissionsDto _ownPermissions = null!;
|
||||
|
||||
public SyncshellFinderUI(
|
||||
ILogger<SyncshellFinderUI> logger,
|
||||
LightlessMediator mediator,
|
||||
PerformanceCollectorService performanceCollectorService,
|
||||
BroadcastService broadcastService,
|
||||
UiSharedService uiShared,
|
||||
ApiController apiController,
|
||||
BroadcastScannerService broadcastScannerService,
|
||||
PairManager pairManager,
|
||||
DalamudUtilService dalamudUtilService) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService)
|
||||
{
|
||||
_broadcastService = broadcastService;
|
||||
_uiSharedService = uiShared;
|
||||
_apiController = apiController;
|
||||
_broadcastScannerService = broadcastScannerService;
|
||||
_pairManager = pairManager;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
|
||||
IsOpen = false;
|
||||
SizeConstraints = new()
|
||||
{
|
||||
MinimumSize = new(600, 400),
|
||||
MaximumSize = new(600, 550)
|
||||
};
|
||||
|
||||
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().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
_uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("PairBlue"));
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("PairBlue"));
|
||||
|
||||
if (_nearbySyncshells.Count == 0)
|
||||
{
|
||||
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted.");
|
||||
|
||||
if (!_broadcastService.IsBroadcasting)
|
||||
{
|
||||
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"));
|
||||
|
||||
ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder is currently disabled, to locate nearby syncshells, Lightfinder must be active.");
|
||||
ImGuiHelpers.ScaledDummy(0.5f);
|
||||
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f);
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessYellow2"));
|
||||
|
||||
if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0)))
|
||||
{
|
||||
Mediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
|
||||
}
|
||||
|
||||
ImGui.PopStyleColor();
|
||||
ImGui.PopStyleVar();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DrawSyncshellTable();
|
||||
|
||||
if (_joinDto != null && _joinInfo != null && _joinInfo.Success)
|
||||
DrawConfirmation();
|
||||
}
|
||||
|
||||
private void DrawSyncshellTable()
|
||||
{
|
||||
if (ImGui.BeginTable("##NearbySyncshellsTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg))
|
||||
{
|
||||
ImGui.TableSetupColumn("Syncshell", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn("Broadcaster", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn("Join", ImGuiTableColumnFlags.WidthFixed, 80f * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
foreach (var shell in _nearbySyncshells)
|
||||
{
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID;
|
||||
ImGui.TextUnformatted(displayName);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
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}";
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f));
|
||||
|
||||
if (!_currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal)))
|
||||
{
|
||||
if (ImGui.Button(label))
|
||||
{
|
||||
_logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})");
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto(
|
||||
shell.Group,
|
||||
shell.Password,
|
||||
shell.GroupUserPreferredPermissions
|
||||
)).ConfigureAwait(false);
|
||||
|
||||
if (info != null && info.Success)
|
||||
{
|
||||
_joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions);
|
||||
_joinInfo = info;
|
||||
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
|
||||
|
||||
_logger.LogInformation($"Fetched join info for {shell.Group.GID}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Join failed for {shell.Group.GID}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (ImRaii.Disabled())
|
||||
{
|
||||
ImGui.Button(label);
|
||||
}
|
||||
UiSharedService.AttachToolTip("Already a member or owner of this Syncshell.");
|
||||
}
|
||||
ImGui.PopStyleColor(3);
|
||||
}
|
||||
|
||||
ImGui.EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawConfirmation()
|
||||
{
|
||||
if (_joinDto != null && _joinInfo != null)
|
||||
{
|
||||
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}"))
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPermissionRow(string label, bool suggested, bool current, Action<bool> apply)
|
||||
{
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted($"- {label}");
|
||||
|
||||
ImGui.SameLine(150 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted("Current:");
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.BooleanToColoredIcon(!current);
|
||||
|
||||
ImGui.SameLine(300 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TextUnformatted("Suggested:");
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.BooleanToColoredIcon(!suggested);
|
||||
|
||||
ImGui.SameLine(450 * ImGuiHelpers.GlobalScale);
|
||||
using var id = ImRaii.PushId(label);
|
||||
if (current != suggested)
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply"))
|
||||
apply(suggested);
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
}
|
||||
|
||||
private async Task RefreshSyncshellsAsync()
|
||||
{
|
||||
var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
|
||||
_currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)];
|
||||
|
||||
if (syncshellBroadcasts.Count == 0)
|
||||
{
|
||||
ClearSyncshells();
|
||||
return;
|
||||
}
|
||||
|
||||
List<GroupJoinDto>? updatedList = [];
|
||||
try
|
||||
{
|
||||
var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts).ConfigureAwait(false);
|
||||
updatedList = groups?.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh broadcasted syncshells.");
|
||||
return;
|
||||
}
|
||||
|
||||
var currentGids = _nearbySyncshells.Select(s => s.Group.GID).ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
if (updatedList != null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClearSelection();
|
||||
}
|
||||
|
||||
private void ClearSyncshells()
|
||||
{
|
||||
if (_nearbySyncshells.Count == 0)
|
||||
return;
|
||||
|
||||
_nearbySyncshells.Clear();
|
||||
ClearSelection();
|
||||
}
|
||||
|
||||
private void ClearSelection()
|
||||
{
|
||||
_selectedNearbyIndex = -1;
|
||||
_joinDto = null;
|
||||
_joinInfo = null;
|
||||
}
|
||||
|
||||
private string? GetSelectedGid()
|
||||
{
|
||||
if (_selectedNearbyIndex < 0 || _selectedNearbyIndex >= _nearbySyncshells.Count)
|
||||
return null;
|
||||
|
||||
return _nearbySyncshells[_selectedNearbyIndex].Group.GID;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -39,8 +54,9 @@ public class TopTabMenu
|
||||
None,
|
||||
Individual,
|
||||
Syncshell,
|
||||
Filter,
|
||||
UserConfig
|
||||
Lightfinder,
|
||||
UserConfig,
|
||||
Settings
|
||||
}
|
||||
|
||||
public string Filter
|
||||
@@ -60,11 +76,6 @@ public class TopTabMenu
|
||||
{
|
||||
get => _selectedTab; set
|
||||
{
|
||||
if (_selectedTab == SelectedTab.Filter && value != SelectedTab.Filter)
|
||||
{
|
||||
Filter = string.Empty;
|
||||
}
|
||||
|
||||
_selectedTab = value;
|
||||
}
|
||||
}
|
||||
@@ -72,11 +83,11 @@ 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();
|
||||
var underlineColor = ImGui.GetColorU32(ImGuiCol.Separator);
|
||||
var underlineColor = ImGui.GetColorU32(UIColors.Get("LightlessPurpleActive")); // ImGui.GetColorU32(ImGuiCol.Separator);
|
||||
var btncolor = ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0)));
|
||||
|
||||
ImGuiHelpers.ScaledDummy(spacing.Y / 2f);
|
||||
@@ -117,19 +128,19 @@ public class TopTabMenu
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
var x = ImGui.GetCursorScreenPos();
|
||||
if (ImGui.Button(FontAwesomeIcon.Filter.ToIconString(), buttonSize))
|
||||
if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize))
|
||||
{
|
||||
TabSelection = TabSelection == SelectedTab.Filter ? SelectedTab.None : SelectedTab.Filter;
|
||||
TabSelection = TabSelection == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
var xAfter = ImGui.GetCursorScreenPos();
|
||||
if (TabSelection == SelectedTab.Filter)
|
||||
if (TabSelection == SelectedTab.Lightfinder)
|
||||
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y },
|
||||
xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
|
||||
underlineColor, 2);
|
||||
}
|
||||
UiSharedService.AttachToolTip("Filter");
|
||||
UiSharedService.AttachToolTip("Lightfinder");
|
||||
|
||||
ImGui.SameLine();
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
@@ -149,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();
|
||||
|
||||
@@ -164,9 +187,9 @@ public class TopTabMenu
|
||||
DrawSyncshellMenu(availableWidth, spacing.X);
|
||||
DrawGlobalSyncshellButtons(availableWidth, spacing.X);
|
||||
}
|
||||
else if (TabSelection == SelectedTab.Filter)
|
||||
else if (TabSelection == SelectedTab.Lightfinder)
|
||||
{
|
||||
DrawFilter(availableWidth, spacing.X);
|
||||
DrawLightfinderMenu(availableWidth, spacing.X);
|
||||
}
|
||||
else if (TabSelection == SelectedTab.UserConfig)
|
||||
{
|
||||
@@ -174,7 +197,25 @@ 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);
|
||||
}
|
||||
|
||||
private void DrawAddPair(float availableXWidth, float spacingX)
|
||||
@@ -195,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");
|
||||
@@ -483,6 +725,23 @@ public class TopTabMenu
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawLightfinderMenu(float availableWidth, float spacingX)
|
||||
{
|
||||
var buttonX = (availableWidth - (spacingX)) / 2f;
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, "Lightfinder", buttonX, center: true))
|
||||
{
|
||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Globe, "Syncshell Finder", buttonX, center: true))
|
||||
{
|
||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(SyncshellFinderUI)));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawUserConfig(float availableWidth, float spacingX)
|
||||
{
|
||||
var buttonX = (availableWidth - spacingX) / 2f;
|
||||
|
||||
@@ -9,10 +9,23 @@ namespace LightlessSync.UI
|
||||
private static readonly Dictionary<string, string> DefaultHexColors = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "LightlessPurple", "#ad8af5" },
|
||||
{ "LightlessPurpleActive", "#be9eff" },
|
||||
{ "LightlessPurpleDefault", "#9375d1" },
|
||||
|
||||
{ "ButtonDefault", "#323232" },
|
||||
{ "FullBlack", "#000000" },
|
||||
|
||||
{ "LightlessBlue", "#a6c2ff" },
|
||||
{ "LightlessYellow", "#ffe97a" },
|
||||
{ "LightlessYellow2", "#cfbd63" },
|
||||
{ "LightlessGreen", "#7cd68a" },
|
||||
{ "PairBlue", "#88a2db" },
|
||||
{ "DimRed", "#d44444" },
|
||||
|
||||
{ "LightlessAdminText", "#ffd663" },
|
||||
{ "LightlessAdminGlow", "#b09343" },
|
||||
{ "LightlessModeratorText", "#94ffda" },
|
||||
{ "LightlessModeratorGlow", "#599c84" },
|
||||
};
|
||||
|
||||
private static LightlessConfigService? _configService;
|
||||
|
||||
@@ -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}";
|
||||
@@ -491,10 +494,90 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
ImGui.Dummy(new Vector2(0, thickness * ImGuiHelpers.GlobalScale));
|
||||
}
|
||||
|
||||
public void RoundedSeparator(Vector4? color = null, float thickness = 2f, float indent = 0f, float rounding = 4f)
|
||||
{
|
||||
float scale = ImGuiHelpers.GlobalScale;
|
||||
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var min = ImGui.GetCursorScreenPos();
|
||||
var contentWidth = ImGui.GetContentRegionAvail().X;
|
||||
|
||||
min.X += indent * scale;
|
||||
var max = new Vector2(min.X + (contentWidth - indent * 2f) * scale, min.Y + thickness * scale);
|
||||
|
||||
var col = ImGui.GetColorU32(color ?? ImGuiColors.DalamudGrey);
|
||||
|
||||
|
||||
drawList.AddRectFilled(min, max, col, rounding);
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -1125,11 +1208,11 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
IconText(icon, color == null ? ImGui.GetColorU32(ImGuiCol.Text) : ImGui.GetColorU32(color.Value));
|
||||
}
|
||||
|
||||
public bool IconTextButton(FontAwesomeIcon icon, string text, float? width = null, bool isInPopup = false)
|
||||
public bool IconTextButton(FontAwesomeIcon icon, string text, float? width = null, bool isInPopup = false, bool? center = null)
|
||||
{
|
||||
return IconTextButtonInternal(icon, text,
|
||||
isInPopup ? ColorHelpers.RgbaUintToVector4(ImGui.GetColorU32(ImGuiCol.PopupBg)) : null,
|
||||
width <= 0 ? null : width);
|
||||
width <= 0 ? null : width, center);
|
||||
}
|
||||
|
||||
public IDalamudTextureWrap LoadImage(byte[] imageData)
|
||||
@@ -1193,7 +1276,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
ImGui.TextUnformatted(text);
|
||||
}
|
||||
|
||||
private bool IconTextButtonInternal(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, float? width = null)
|
||||
private bool IconTextButtonInternal(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, float? width = null, bool? center = null)
|
||||
{
|
||||
int num = 0;
|
||||
if (defaultColor.HasValue)
|
||||
@@ -1203,19 +1286,35 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
|
||||
ImGui.PushID(text);
|
||||
|
||||
Vector2 vector;
|
||||
using (IconFont.Push())
|
||||
vector = ImGui.CalcTextSize(icon.ToIconString());
|
||||
|
||||
Vector2 vector2 = ImGui.CalcTextSize(text);
|
||||
ImDrawListPtr windowDrawList = ImGui.GetWindowDrawList();
|
||||
Vector2 cursorScreenPos = ImGui.GetCursorScreenPos();
|
||||
float num2 = 3f * ImGuiHelpers.GlobalScale;
|
||||
float x = width ?? vector.X + vector2.X + ImGui.GetStyle().FramePadding.X * 2f + num2;
|
||||
|
||||
float totalTextWidth = vector.X + num2 + vector2.X;
|
||||
float x = width ?? totalTextWidth + ImGui.GetStyle().FramePadding.X * 2f;
|
||||
float frameHeight = ImGui.GetFrameHeight();
|
||||
|
||||
bool result = ImGui.Button(string.Empty, new Vector2(x, frameHeight));
|
||||
Vector2 pos = new Vector2(cursorScreenPos.X + ImGui.GetStyle().FramePadding.X, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y);
|
||||
|
||||
bool shouldCenter = center == true;
|
||||
|
||||
Vector2 pos = shouldCenter
|
||||
? new Vector2(
|
||||
cursorScreenPos.X + (x - totalTextWidth) / 2f,
|
||||
cursorScreenPos.Y + (frameHeight - vector.Y) / 2f)
|
||||
: new Vector2(
|
||||
cursorScreenPos.X + ImGui.GetStyle().FramePadding.X,
|
||||
cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y);
|
||||
|
||||
using (IconFont.Push())
|
||||
windowDrawList.AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString());
|
||||
|
||||
Vector2 pos2 = new Vector2(pos.X + vector.X + num2, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y);
|
||||
windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), text);
|
||||
ImGui.PopID();
|
||||
|
||||
99
LightlessSync/Utils/AtkNodeHelpers.cs
Normal file
99
LightlessSync/Utils/AtkNodeHelpers.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace LightlessSync.Utils;
|
||||
|
||||
internal static unsafe class AtkNodeHelpers
|
||||
{
|
||||
internal const ushort DefaultTextNodeWidth = 200;
|
||||
internal const ushort DefaultTextNodeHeight = 14;
|
||||
|
||||
internal static AtkTextNode* CreateNewTextNode(AtkUnitBase* pAddon, uint nodeID)
|
||||
{
|
||||
if (pAddon == null) return null;
|
||||
var pNewNode = CreateOrphanTextNode(nodeID);
|
||||
if (pNewNode != null) AttachTextNode(pAddon, pNewNode);
|
||||
return pNewNode;
|
||||
}
|
||||
|
||||
internal static void HideNode(AtkUnitBase* pAddon, uint nodeID)
|
||||
{
|
||||
var pNode = GetTextNodeByID(pAddon, nodeID);
|
||||
if (pNode != null) ((AtkResNode*)pNode)->ToggleVisibility(false);
|
||||
}
|
||||
|
||||
internal static AtkTextNode* GetTextNodeByID(AtkUnitBase* pAddon, uint nodeID)
|
||||
{
|
||||
if (pAddon == null) return null;
|
||||
for (var i = 0; i < pAddon->UldManager.NodeListCount; ++i)
|
||||
{
|
||||
if (pAddon->UldManager.NodeList[i] == null) continue;
|
||||
if (pAddon->UldManager.NodeList[i]->NodeId == nodeID)
|
||||
{
|
||||
return (AtkTextNode*)pAddon->UldManager.NodeList[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static void AttachTextNode(AtkUnitBase* pAddon, AtkTextNode* pNode)
|
||||
{
|
||||
if (pAddon == null) return;
|
||||
|
||||
if (pNode != null)
|
||||
{
|
||||
var lastNode = pAddon->RootNode;
|
||||
if (lastNode->ChildNode != null)
|
||||
{
|
||||
lastNode = lastNode->ChildNode;
|
||||
while (lastNode->PrevSiblingNode != null)
|
||||
{
|
||||
lastNode = lastNode->PrevSiblingNode;
|
||||
}
|
||||
|
||||
pNode->AtkResNode.NextSiblingNode = lastNode;
|
||||
pNode->AtkResNode.ParentNode = pAddon->RootNode;
|
||||
lastNode->PrevSiblingNode = (AtkResNode*)pNode;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastNode->ChildNode = (AtkResNode*)pNode;
|
||||
pNode->AtkResNode.ParentNode = lastNode;
|
||||
}
|
||||
|
||||
pAddon->UldManager.UpdateDrawNodeList();
|
||||
}
|
||||
}
|
||||
|
||||
internal static AtkTextNode* CreateOrphanTextNode(uint nodeID, TextFlags textFlags = TextFlags.Edge)
|
||||
{
|
||||
var pNewNode = (AtkTextNode*)IMemorySpace.GetUISpace()->Malloc((ulong)sizeof(AtkTextNode), 8);
|
||||
if (pNewNode != null)
|
||||
{
|
||||
IMemorySpace.Memset(pNewNode, 0, (ulong)sizeof(AtkTextNode));
|
||||
pNewNode->Ctor();
|
||||
|
||||
pNewNode->AtkResNode.Type = NodeType.Text;
|
||||
pNewNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop;
|
||||
pNewNode->AtkResNode.DrawFlags = 0;
|
||||
pNewNode->AtkResNode.SetPositionShort(0, 0);
|
||||
pNewNode->AtkResNode.SetWidth(DefaultTextNodeWidth);
|
||||
pNewNode->AtkResNode.SetHeight(DefaultTextNodeHeight);
|
||||
|
||||
pNewNode->LineSpacing = 24;
|
||||
pNewNode->CharSpacing = 1;
|
||||
pNewNode->AlignmentFontType = (byte)AlignmentType.BottomLeft;
|
||||
pNewNode->FontSize = 12;
|
||||
pNewNode->TextFlags = textFlags;
|
||||
|
||||
pNewNode->AtkResNode.NodeId = nodeID;
|
||||
|
||||
pNewNode->AtkResNode.Color.A = 0xFF;
|
||||
pNewNode->AtkResNode.Color.R = 0xFF;
|
||||
pNewNode->AtkResNode.Color.G = 0xFF;
|
||||
pNewNode->AtkResNode.Color.B = 0xFF;
|
||||
}
|
||||
|
||||
return pNewNode;
|
||||
}
|
||||
}
|
||||
239
LightlessSync/Utils/SeStringUtils.cs
Normal file
239
LightlessSync/Utils/SeStringUtils.cs
Normal file
@@ -0,0 +1,239 @@
|
||||
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 DalamudSeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor)
|
||||
{
|
||||
var b = new DalamudSeStringBuilder();
|
||||
|
||||
if (glowColor is Vector4 glow)
|
||||
b.Add(new GlowPayload(glow));
|
||||
|
||||
if (textColor is Vector4 color)
|
||||
b.Add(new ColorPayload(color));
|
||||
|
||||
b.AddText(text ?? string.Empty);
|
||||
|
||||
if (textColor is not null)
|
||||
b.Add(new ColorEndPayload());
|
||||
|
||||
if (glowColor is not null)
|
||||
b.Add(new GlowEndPayload());
|
||||
|
||||
return b.Build();
|
||||
}
|
||||
|
||||
public static DalamudSeString BuildPlain(string text)
|
||||
{
|
||||
var b = new DalamudSeStringBuilder();
|
||||
b.AddText(text ?? string.Empty);
|
||||
return b.Build();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
var drawParams = new SeStringDrawParams
|
||||
{
|
||||
Font = font ?? UiBuilder.MonoFont,
|
||||
Color = 0xFFFFFFFF,
|
||||
WrapWidth = float.MaxValue,
|
||||
TargetDrawList = drawList
|
||||
};
|
||||
|
||||
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 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();
|
||||
|
||||
var drawParams = new SeStringDrawParams
|
||||
{
|
||||
Font = font ?? UiBuilder.MonoFont,
|
||||
Color = 0xFFFFFFFF,
|
||||
WrapWidth = float.MaxValue,
|
||||
TargetDrawList = drawList
|
||||
};
|
||||
|
||||
ImGui.SetCursorScreenPos(position);
|
||||
ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);
|
||||
|
||||
var textSize = ImGui.CalcTextSize(seString.TextValue);
|
||||
|
||||
ImGui.SetCursorScreenPos(position);
|
||||
ImGui.InvisibleButton($"##hitbox_{Guid.NewGuid()}", textSize);
|
||||
|
||||
return textSize;
|
||||
}
|
||||
|
||||
public static Vector2 RenderIconWithHitbox(int iconId, Vector2 position, ImFontPtr? font = null)
|
||||
{
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
|
||||
var drawParams = new SeStringDrawParams
|
||||
{
|
||||
Font = font ?? UiBuilder.MonoFont,
|
||||
Color = 0xFFFFFFFF,
|
||||
WrapWidth = float.MaxValue,
|
||||
TargetDrawList = drawList
|
||||
};
|
||||
|
||||
var iconMacro = $"<icon({iconId})>";
|
||||
var drawResult = ImGuiHelpers.CompileSeStringWrapped(iconMacro, drawParams);
|
||||
|
||||
ImGui.SetCursorScreenPos(position);
|
||||
ImGui.InvisibleButton($"##iconHitbox_{Guid.NewGuid()}", drawResult.Size);
|
||||
|
||||
return drawResult.Size;
|
||||
}
|
||||
|
||||
#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; }
|
||||
protected byte Green { get; init; }
|
||||
protected byte Blue { get; init; }
|
||||
|
||||
protected override byte[] EncodeImpl()
|
||||
{
|
||||
return new byte[] { 0x02, ChunkType, 0x05, 0xF6, Red, Green, Blue, 0x03 };
|
||||
}
|
||||
|
||||
protected override void DecodeImpl(BinaryReader reader, long endOfStream) { }
|
||||
|
||||
public override PayloadType Type => PayloadType.Unknown;
|
||||
protected abstract byte ChunkType { get; }
|
||||
}
|
||||
|
||||
private abstract class AbstractColorEndPayload : Payload
|
||||
{
|
||||
protected override byte[] EncodeImpl()
|
||||
{
|
||||
return new byte[] { 0x02, ChunkType, 0x02, 0xEC, 0x03 };
|
||||
}
|
||||
|
||||
protected override void DecodeImpl(BinaryReader reader, long endOfStream) { }
|
||||
|
||||
public override PayloadType Type => PayloadType.Unknown;
|
||||
protected abstract byte ChunkType { get; }
|
||||
}
|
||||
|
||||
private class ColorPayload : AbstractColorPayload
|
||||
{
|
||||
protected override byte ChunkType => 0x13;
|
||||
|
||||
public ColorPayload(Vector3 color)
|
||||
{
|
||||
Red = Math.Max((byte)1, (byte)(color.X * 255f));
|
||||
Green = Math.Max((byte)1, (byte)(color.Y * 255f));
|
||||
Blue = Math.Max((byte)1, (byte)(color.Z * 255f));
|
||||
}
|
||||
|
||||
public ColorPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { }
|
||||
}
|
||||
|
||||
private class ColorEndPayload : AbstractColorEndPayload
|
||||
{
|
||||
protected override byte ChunkType => 0x13;
|
||||
}
|
||||
|
||||
private class GlowPayload : AbstractColorPayload
|
||||
{
|
||||
protected override byte ChunkType => 0x14;
|
||||
|
||||
public GlowPayload(Vector3 color)
|
||||
{
|
||||
Red = Math.Max((byte)1, (byte)(color.X * 255f));
|
||||
Green = Math.Max((byte)1, (byte)(color.Y * 255f));
|
||||
Blue = Math.Max((byte)1, (byte)(color.Z * 255f));
|
||||
}
|
||||
|
||||
public GlowPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { }
|
||||
}
|
||||
|
||||
private class GlowEndPayload : AbstractColorEndPayload
|
||||
{
|
||||
protected override byte ChunkType => 0x14;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -34,6 +35,36 @@ public partial class ApiController
|
||||
await _lightlessHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task TryPairWithContentId(string otherCid, string myCid)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid, myCid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SetBroadcastStatus(string hashedCid, bool enabled, GroupBroadcastRequestDto? groupDto = null)
|
||||
{
|
||||
CheckConnection();
|
||||
await _lightlessHub!.InvokeAsync(nameof(SetBroadcastStatus), hashedCid, enabled, groupDto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<BroadcastStatusInfoDto?> IsUserBroadcasting(string hashedCid)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _lightlessHub!.InvokeAsync<BroadcastStatusInfoDto?>(nameof(IsUserBroadcasting), hashedCid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<BroadcastStatusBatchDto> AreUsersBroadcasting(List<string> hashedCids)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _lightlessHub!.InvokeAsync<BroadcastStatusBatchDto>(nameof(AreUsersBroadcasting), hashedCids).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<TimeSpan?> GetBroadcastTtl(string hashedCid)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _lightlessHub!.InvokeAsync<TimeSpan?>(nameof(GetBroadcastTtl), hashedCid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserDelete()
|
||||
{
|
||||
CheckConnection();
|
||||
@@ -103,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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.WebAPI.SignalR.Utils;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
@@ -44,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()
|
||||
{
|
||||
@@ -80,6 +86,11 @@ public partial class ApiController
|
||||
CheckConnection();
|
||||
return await _lightlessHub!.InvokeAsync<bool>(nameof(GroupJoinFinalize), passwordedGroup).ConfigureAwait(false);
|
||||
}
|
||||
public async Task<GroupJoinInfoDto> GroupJoinHashed(GroupJoinHashedDto dto)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _lightlessHub!.InvokeAsync<GroupJoinInfoDto>("GroupJoinHashed", dto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task GroupLeave(GroupDto group)
|
||||
{
|
||||
@@ -116,6 +127,18 @@ public partial class ApiController
|
||||
CheckConnection();
|
||||
await _lightlessHub!.SendAsync(nameof(GroupUnbanUser), groupPair).ConfigureAwait(false);
|
||||
}
|
||||
public async Task<bool> SetGroupBroadcastStatus(GroupBroadcastRequestDto dto)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _lightlessHub!.InvokeAsync<bool>(nameof(SetGroupBroadcastStatus), dto).ConfigureAwait(false);
|
||||
}
|
||||
public async Task<List<GroupJoinDto>> GetBroadcastedGroups(List<BroadcastStatusInfoDto> broadcastEntries)
|
||||
{
|
||||
CheckConnection();
|
||||
return await _lightlessHub!.InvokeAsync<List<GroupJoinDto>>(nameof(GetBroadcastedGroups), broadcastEntries)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
private void CheckConnection()
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.API.SignalR;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
@@ -27,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;
|
||||
@@ -41,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;
|
||||
@@ -76,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));
|
||||
@@ -100,6 +107,8 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
|
||||
public string UID => _connectionDto?.User.UID ?? string.Empty;
|
||||
|
||||
public event Action? OnConnected;
|
||||
|
||||
public async Task<bool> CheckClientHealth()
|
||||
{
|
||||
return await _lightlessHub!.InvokeAsync<bool>(nameof(CheckClientHealth)).ConfigureAwait(false);
|
||||
@@ -230,6 +239,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
_connectionDto = await GetConnectionDto().ConfigureAwait(false);
|
||||
|
||||
ServerState = ServerState.Connected;
|
||||
OnConnected?.Invoke();
|
||||
|
||||
var currentClientVer = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||
|
||||
@@ -420,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));
|
||||
@@ -441,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));
|
||||
@@ -517,6 +529,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
return;
|
||||
}
|
||||
ServerState = ServerState.Connected;
|
||||
OnConnected?.Invoke();
|
||||
await LoadIninitialPairsAsync().ConfigureAwait(false);
|
||||
await LoadOnlinePairsAsync().ConfigureAwait(false);
|
||||
Mediator.Publish(new ConnectedMessage(_connectionDto));
|
||||
@@ -592,5 +605,20 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
|
||||
ServerState = state;
|
||||
}
|
||||
|
||||
public Task Client_GroupSendProfile(GroupProfileDto groupInfo)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<GroupProfileDto> GroupGetProfile(GroupDto dto)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task GroupSetProfile(GroupProfileDto dto)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
#pragma warning restore MA0040
|
||||
Submodule PenumbraAPI updated: dd14131793...648b6fc2ce
Reference in New Issue
Block a user