Compare commits
48 Commits
1.11.7
...
1.12.1-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2faec601c4 | ||
|
|
57b7b8cee9 | ||
|
|
b492d12e19 | ||
|
|
6bd59d01aa | ||
| a772ee4705 | |||
|
|
1d88c04235 | ||
|
|
a7378652c4 | ||
| 61267d1b03 | |||
|
|
ca70c622bc | ||
|
|
49e5fb9d8d | ||
|
|
371b1c2fb1 | ||
| a2bb1d7336 | |||
| 4f50028517 | |||
| 9f87a6a8fc | |||
|
|
4a391f2392 | ||
| a7c4b8f356 | |||
| c35650438c | |||
| b87185bc33 | |||
| 67da22fe9f | |||
|
|
15798e6753 | ||
| ca68e63c7d | |||
| eb10a27c6e | |||
| 39784a1fea | |||
|
|
fec2e4d380 | ||
|
|
afc3b4534c | ||
|
|
98b9cc7fe7 | ||
|
|
fd26d776a5 | ||
|
|
fdfd5722c7 | ||
|
|
19e42d34ff | ||
|
|
173e0aa7ae | ||
|
|
55d979b7c0 | ||
|
|
f31a139a3e | ||
| c82c633513 | |||
|
|
f6ea1eddc0 | ||
|
|
4ca3b6da48 | ||
| e75dd86fdb | |||
| 716d7a54d9 | |||
| 701ccaffe4 | |||
| ee8d05ca7a | |||
| fd5522b90a | |||
| 7672f147f5 | |||
|
|
5dce1977c7 | ||
|
|
e396d2cf46 | ||
| c5e6c06005 | |||
| 14ec282f21 | |||
| c7316e4f55 | |||
| 8c308ab488 | |||
| a8512e2a86 |
@@ -2,7 +2,7 @@ name: Tag and Release Lightless
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [ master, dev ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PLUGIN_NAME: LightlessSync
|
PLUGIN_NAME: LightlessSync
|
||||||
@@ -41,9 +41,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Get version
|
- name: Get version
|
||||||
id: package_version
|
id: package_version
|
||||||
uses: KageKirin/get-csproj-version@v0
|
run: |
|
||||||
with:
|
version=$(grep -oPm1 "(?<=<Version>)[^<]+" LightlessSync/LightlessSync.csproj)
|
||||||
file: LightlessSync/LightlessSync.csproj
|
echo "version=$version" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Display version
|
- name: Display version
|
||||||
run: |
|
run: |
|
||||||
@@ -62,7 +62,8 @@ jobs:
|
|||||||
mkdir -p output
|
mkdir -p output
|
||||||
(cd /workspace/Lightless-Sync/LightlessClient/LightlessSync/bin/x64/Release/ && zip -r $OLDPWD/output/LightlessClient.zip *)
|
(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: |
|
run: |
|
||||||
tag="${{ steps.package_version.outputs.version }}"
|
tag="${{ steps.package_version.outputs.version }}"
|
||||||
git fetch --tags
|
git fetch --tags
|
||||||
@@ -76,7 +77,23 @@ jobs:
|
|||||||
echo "Tag $tag already exists. Skipping tag creation."
|
echo "Tag $tag already exists. Skipping tag creation."
|
||||||
fi
|
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
|
id: create_release
|
||||||
run: |
|
run: |
|
||||||
echo "=== Searching for existing release ${{ steps.package_version.outputs.version }}==="
|
echo "=== Searching for existing release ${{ steps.package_version.outputs.version }}==="
|
||||||
@@ -104,15 +121,60 @@ jobs:
|
|||||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases"
|
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
echo "API response: $response"
|
||||||
release_id=$(echo "$response" | jq -r .id)
|
release_id=$(echo "$response" | jq -r .id)
|
||||||
echo "release_id=$release_id" >> "$GITHUB_OUTPUT"
|
echo "release_id=$release_id"
|
||||||
|
echo "release_id=$release_id" >> $GITHUB_OUTPUT || echo "::set-output name=release_id::$release_id"
|
||||||
|
echo "RELEASE_ID=$release_id" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Upload Assets to release
|
- name: Create Release (dev)
|
||||||
|
if: github.ref == 'refs/heads/dev'
|
||||||
|
id: create_release
|
||||||
run: |
|
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"
|
||||||
|
)
|
||||||
|
echo "API response: $response"
|
||||||
|
release_id=$(echo "$response" | jq -r .id)
|
||||||
|
echo "release_id=$release_id"
|
||||||
|
echo "release_id=$release_id" >> $GITHUB_OUTPUT || echo "::set-output name=release_id::$release_id"
|
||||||
|
echo "RELEASE_ID=$release_id" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Check asset exists
|
||||||
|
run: |
|
||||||
|
if [ ! -f output/LightlessClient.zip ]; then
|
||||||
|
echo "output/LightlessClient.zip does not exist!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload Assets to release
|
||||||
|
env:
|
||||||
|
RELEASE_ID: ${{ env.RELEASE_ID }}
|
||||||
|
run: |
|
||||||
|
echo "Uploading to release ID: $RELEASE_ID"
|
||||||
curl --fail-with-body -s -X POST \
|
curl --fail-with-body -s -X POST \
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||||
-F "attachment=@output/LightlessClient.zip" \
|
-F "attachment=@output/LightlessClient.zip" \
|
||||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/${{ steps.create_release.outputs.release_id }}/assets"
|
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/$RELEASE_ID/assets"
|
||||||
|
|
||||||
- name: Clone plugin hosting repo
|
- name: Clone plugin hosting repo
|
||||||
run: |
|
run: |
|
||||||
@@ -122,7 +184,8 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GIT_TERMINAL_PROMPT: 0
|
GIT_TERMINAL_PROMPT: 0
|
||||||
|
|
||||||
- name: Update plogonmaster.json with version
|
- name: Update plogonmaster.json with version (master)
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ steps.package_version.outputs.version }}
|
VERSION: ${{ steps.package_version.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
@@ -159,7 +222,6 @@ jobs:
|
|||||||
.DalamudApiLevel = $dalamudApiLevel
|
.DalamudApiLevel = $dalamudApiLevel
|
||||||
| .AssemblyVersion = $version
|
| .AssemblyVersion = $version
|
||||||
| .DownloadLinkInstall = $downloadUrl
|
| .DownloadLinkInstall = $downloadUrl
|
||||||
| .DownloadLinkTesting = $downloadUrl
|
|
||||||
| .DownloadLinkUpdate = $downloadUrl
|
| .DownloadLinkUpdate = $downloadUrl
|
||||||
else
|
else
|
||||||
.
|
.
|
||||||
@@ -172,6 +234,47 @@ jobs:
|
|||||||
# Output the content of the file
|
# Output the content of the file
|
||||||
cat "$repoJsonPath"
|
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
|
- name: Commit and push to LightlessSync
|
||||||
run: |
|
run: |
|
||||||
cd LightlessSyncRepo/LightlessSync
|
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...6c542c0ccc
@@ -20,7 +20,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
private long _currentFileProgress = 0;
|
private long _currentFileProgress = 0;
|
||||||
private CancellationTokenSource _scanCancellationTokenSource = new();
|
private CancellationTokenSource _scanCancellationTokenSource = new();
|
||||||
private readonly CancellationTokenSource _periodicCalculationTokenSource = 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,
|
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
|
||||||
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
|
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
|
||||||
@@ -383,7 +383,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
scanThread.Start();
|
scanThread.Start();
|
||||||
while (scanThread.IsAlive)
|
while (scanThread.IsAlive)
|
||||||
{
|
{
|
||||||
await Task.Delay(250).ConfigureAwait(false);
|
await Task.Delay(250, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
TotalFiles = 0;
|
TotalFiles = 0;
|
||||||
_currentFileProgress = 0;
|
_currentFileProgress = 0;
|
||||||
@@ -583,7 +583,14 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogWarning(ex, "Failed validating {path}", workload.ResolvedFilepath);
|
if (workload != null)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed validating {path}", workload.ResolvedFilepath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed validating unknown workload");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Interlocked.Increment(ref _currentFileProgress);
|
Interlocked.Increment(ref _currentFileProgress);
|
||||||
}
|
}
|
||||||
@@ -612,7 +619,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entitiesToUpdate.Any() || entitiesToRemove.Any())
|
if (entitiesToUpdate.Count != 0 || entitiesToRemove.Count != 0)
|
||||||
{
|
{
|
||||||
foreach (var entity in entitiesToUpdate)
|
foreach (var entity in entitiesToUpdate)
|
||||||
{
|
{
|
||||||
@@ -647,6 +654,12 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
CancellationToken = ct
|
CancellationToken = ct
|
||||||
}, (cachePath) =>
|
}, (cachePath) =>
|
||||||
{
|
{
|
||||||
|
if (_fileDbManager == null || _ipcManager?.Penumbra == null || cachePath == null)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Potential null in db: {isDbNull} penumbra: {isPenumbraNull} cachepath: {isPathNull}", _fileDbManager == null, _ipcManager?.Penumbra == null, cachePath == null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ct.IsCancellationRequested) return;
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
if (!_ipcManager.Penumbra.APIAvailable)
|
if (!_ipcManager.Penumbra.APIAvailable)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using K4os.Compression.LZ4.Legacy;
|
using K4os.Compression.LZ4.Legacy;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
@@ -19,9 +19,10 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
private readonly string _csvPath;
|
private readonly string _csvPath;
|
||||||
private readonly ConcurrentDictionary<string, List<FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
|
||||||
|
private readonly ConcurrentDictionary<string, FileCacheEntity> _fileCachesByPrefixedPath = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1);
|
||||||
private readonly object _fileWriteLock = new();
|
private readonly Lock _fileWriteLock = new();
|
||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly ILogger<FileCacheManager> _logger;
|
private readonly ILogger<FileCacheManager> _logger;
|
||||||
public string CacheFolder => _configService.Current.CacheFolder;
|
public string CacheFolder => _configService.Current.CacheFolder;
|
||||||
@@ -37,15 +38,63 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
private string CsvBakPath => _csvPath + ".bak";
|
private string CsvBakPath => _csvPath + ".bak";
|
||||||
|
|
||||||
|
private static string NormalizeSeparators(string path)
|
||||||
|
{
|
||||||
|
return path.Replace("/", "\\", StringComparison.Ordinal)
|
||||||
|
.Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizePrefixedPathKey(string prefixedPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(prefixedPath))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NormalizeSeparators(prefixedPath).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NormalizeToPrefixedPath(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path)) return string.Empty;
|
||||||
|
|
||||||
|
var normalized = NormalizeSeparators(path);
|
||||||
|
|
||||||
|
if (normalized.StartsWith(CachePrefix, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
normalized.StartsWith(PenumbraPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return NormalizePrefixedPathKey(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
var penumbraDir = _ipcManager.Penumbra.ModDirectory;
|
||||||
|
if (!string.IsNullOrEmpty(penumbraDir))
|
||||||
|
{
|
||||||
|
var normalizedPenumbra = NormalizeSeparators(penumbraDir);
|
||||||
|
var replacement = normalizedPenumbra.EndsWith("\\", StringComparison.Ordinal)
|
||||||
|
? PenumbraPrefix + "\\"
|
||||||
|
: PenumbraPrefix;
|
||||||
|
normalized = normalized.Replace(normalizedPenumbra, replacement, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheFolder = _configService.Current.CacheFolder;
|
||||||
|
if (!string.IsNullOrEmpty(cacheFolder))
|
||||||
|
{
|
||||||
|
var normalizedCache = NormalizeSeparators(cacheFolder);
|
||||||
|
var replacement = normalizedCache.EndsWith("\\", StringComparison.Ordinal)
|
||||||
|
? CachePrefix + "\\"
|
||||||
|
: CachePrefix;
|
||||||
|
normalized = normalized.Replace(normalizedCache, replacement, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NormalizePrefixedPathKey(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
public FileCacheEntity? CreateCacheEntry(string path)
|
public FileCacheEntity? CreateCacheEntry(string path)
|
||||||
{
|
{
|
||||||
FileInfo fi = new(path);
|
FileInfo fi = new(path);
|
||||||
if (!fi.Exists) return null;
|
if (!fi.Exists) return null;
|
||||||
_logger.LogTrace("Creating cache entry for {path}", path);
|
_logger.LogTrace("Creating cache entry for {path}", path);
|
||||||
var fullName = fi.FullName.ToLowerInvariant();
|
return CreateFileEntity(_configService.Current.CacheFolder.ToLowerInvariant(), CachePrefix, fi);
|
||||||
if (!fullName.Contains(_configService.Current.CacheFolder.ToLowerInvariant(), StringComparison.Ordinal)) return null;
|
|
||||||
string prefixedPath = fullName.Replace(_configService.Current.CacheFolder.ToLowerInvariant(), CachePrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
|
|
||||||
return CreateFileCacheEntity(fi, prefixedPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileCacheEntity? CreateFileEntry(string path)
|
public FileCacheEntity? CreateFileEntry(string path)
|
||||||
@@ -53,26 +102,37 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
FileInfo fi = new(path);
|
FileInfo fi = new(path);
|
||||||
if (!fi.Exists) return null;
|
if (!fi.Exists) return null;
|
||||||
_logger.LogTrace("Creating file entry for {path}", path);
|
_logger.LogTrace("Creating file entry for {path}", path);
|
||||||
|
return CreateFileEntity(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix, fi);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileCacheEntity? CreateFileEntity(string directory, string prefix, FileInfo fi)
|
||||||
|
{
|
||||||
var fullName = fi.FullName.ToLowerInvariant();
|
var fullName = fi.FullName.ToLowerInvariant();
|
||||||
if (!fullName.Contains(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), StringComparison.Ordinal)) return null;
|
if (!fullName.Contains(directory, StringComparison.Ordinal)) return null;
|
||||||
string prefixedPath = fullName.Replace(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
|
string prefixedPath = fullName.Replace(directory, prefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||||
return CreateFileCacheEntity(fi, prefixedPath);
|
return CreateFileCacheEntity(fi, prefixedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v).ToList();
|
public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null)).ToList();
|
||||||
|
|
||||||
public List<FileCacheEntity> GetAllFileCachesByHash(string hash, bool ignoreCacheEntries = false, bool validate = true)
|
public List<FileCacheEntity> GetAllFileCachesByHash(string hash, bool ignoreCacheEntries = false, bool validate = true)
|
||||||
{
|
{
|
||||||
List<FileCacheEntity> output = [];
|
List<FileCacheEntity> output = [];
|
||||||
if (_fileCaches.TryGetValue(hash, out var fileCacheEntities))
|
if (_fileCaches.TryGetValue(hash, out var fileCacheEntities))
|
||||||
{
|
{
|
||||||
foreach (var fileCache in fileCacheEntities.Where(c => ignoreCacheEntries ? !c.IsCacheEntry : true).ToList())
|
foreach (var fileCache in fileCacheEntities.Values.Where(c => !ignoreCacheEntries || !c.IsCacheEntry).ToList())
|
||||||
{
|
{
|
||||||
if (!validate) output.Add(fileCache);
|
if (!validate)
|
||||||
|
{
|
||||||
|
output.Add(fileCache);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var validated = GetValidatedFileCache(fileCache);
|
var validated = GetValidatedFileCache(fileCache);
|
||||||
if (validated != null) output.Add(validated);
|
if (validated != null)
|
||||||
|
{
|
||||||
|
output.Add(validated);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +144,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
{
|
{
|
||||||
_lightlessMediator.Publish(new HaltScanMessage(nameof(ValidateLocalIntegrity)));
|
_lightlessMediator.Publish(new HaltScanMessage(nameof(ValidateLocalIntegrity)));
|
||||||
_logger.LogInformation("Validating local storage");
|
_logger.LogInformation("Validating local storage");
|
||||||
var cacheEntries = _fileCaches.SelectMany(v => v.Value).Where(v => v.IsCacheEntry).ToList();
|
var cacheEntries = _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null)).Where(v => v.IsCacheEntry).ToList();
|
||||||
List<FileCacheEntity> brokenEntities = [];
|
List<FileCacheEntity> brokenEntities = [];
|
||||||
int i = 0;
|
int i = 0;
|
||||||
foreach (var fileCache in cacheEntries)
|
foreach (var fileCache in cacheEntries)
|
||||||
@@ -106,7 +166,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
var computedHash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
|
var computedHash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
|
||||||
if (!string.Equals(computedHash, fileCache.Hash, StringComparison.Ordinal))
|
if (!string.Equals(computedHash, fileCache.Hash, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Failed to validate {file}, got hash {hash}, expected hash {hash}", fileCache.ResolvedFilepath, computedHash, fileCache.Hash);
|
_logger.LogInformation("Failed to validate {file}, got hash {computedHash}, expected hash {hash}", fileCache.ResolvedFilepath, computedHash, fileCache.Hash);
|
||||||
brokenEntities.Add(fileCache);
|
brokenEntities.Add(fileCache);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,29 +209,40 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
public FileCacheEntity? GetFileCacheByHash(string hash)
|
public FileCacheEntity? GetFileCacheByHash(string hash)
|
||||||
{
|
{
|
||||||
if (_fileCaches.TryGetValue(hash, out var hashes))
|
if (_fileCaches.TryGetValue(hash, out var entries))
|
||||||
{
|
{
|
||||||
var item = hashes.OrderBy(p => p.PrefixedFilePath.Contains(PenumbraPrefix) ? 0 : 1).FirstOrDefault();
|
var item = entries.Values
|
||||||
if (item != null) return GetValidatedFileCache(item);
|
.OrderBy(p => p.PrefixedFilePath.Contains(PenumbraPrefix, StringComparison.Ordinal) ? 0 : 1)
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (item != null)
|
||||||
|
{
|
||||||
|
return GetValidatedFileCache(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private FileCacheEntity? GetFileCacheByPath(string path)
|
private FileCacheEntity? GetFileCacheByPath(string path)
|
||||||
{
|
{
|
||||||
var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant()
|
var normalizedPrefixedPath = NormalizeToPrefixedPath(path);
|
||||||
.Replace(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase);
|
if (string.IsNullOrEmpty(normalizedPrefixedPath))
|
||||||
var entry = _fileCaches.SelectMany(v => v.Value).FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (entry == null)
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Found no entries for {path}", cleanedPath);
|
return null;
|
||||||
return CreateFileEntry(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var validatedCacheEntry = GetValidatedFileCache(entry);
|
if (_fileCachesByPrefixedPath.TryGetValue(normalizedPrefixedPath, out var entry))
|
||||||
|
{
|
||||||
|
return GetValidatedFileCache(entry);
|
||||||
|
}
|
||||||
|
|
||||||
return validatedCacheEntry;
|
_logger.LogDebug("Found no entries for {path}", normalizedPrefixedPath);
|
||||||
|
|
||||||
|
if (normalizedPrefixedPath.Contains(CachePrefix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return CreateCacheEntry(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateFileEntry(path) ?? CreateCacheEntry(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, FileCacheEntity?> GetFileCachesByPaths(string[] paths)
|
public Dictionary<string, FileCacheEntity?> GetFileCachesByPaths(string[] paths)
|
||||||
@@ -180,34 +251,52 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var cleanedPaths = paths.Distinct(StringComparer.OrdinalIgnoreCase).ToDictionary(p => p,
|
var result = new Dictionary<string, FileCacheEntity?>(StringComparer.OrdinalIgnoreCase);
|
||||||
p => p.Replace("/", "\\", StringComparison.OrdinalIgnoreCase)
|
var seenNormalized = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
.Replace(_ipcManager.Penumbra.ModDirectory!, _ipcManager.Penumbra.ModDirectory!.EndsWith('\\') ? PenumbraPrefix + '\\' : PenumbraPrefix, StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace(_configService.Current.CacheFolder, _configService.Current.CacheFolder.EndsWith('\\') ? CachePrefix + '\\' : CachePrefix, StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace("\\\\", "\\", StringComparison.Ordinal),
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
Dictionary<string, FileCacheEntity?> result = new(StringComparer.OrdinalIgnoreCase);
|
foreach (var originalPath in paths)
|
||||||
|
|
||||||
var dict = _fileCaches.SelectMany(f => f.Value)
|
|
||||||
.ToDictionary(d => d.PrefixedFilePath, d => d, StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
foreach (var entry in cleanedPaths)
|
|
||||||
{
|
{
|
||||||
//_logger.LogDebug("Checking {path}", entry.Value);
|
if (string.IsNullOrEmpty(originalPath))
|
||||||
|
|
||||||
if (dict.TryGetValue(entry.Value, out var entity))
|
|
||||||
{
|
{
|
||||||
var validatedCache = GetValidatedFileCache(entity);
|
result[originalPath] = null;
|
||||||
result.Add(entry.Key, validatedCache);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = NormalizeToPrefixedPath(originalPath);
|
||||||
|
if (seenNormalized.Add(normalized))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(normalized))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Normalized path {cleaned}", normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(normalized))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Duplicate normalized path detected: {cleaned}", normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_fileCachesByPrefixedPath.TryGetValue(normalized, out var entity))
|
||||||
|
{
|
||||||
|
result[originalPath] = GetValidatedFileCache(entity);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileCacheEntity? created = null;
|
||||||
|
|
||||||
|
if (normalized.Contains(CachePrefix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
created = CreateCacheEntry(originalPath);
|
||||||
|
}
|
||||||
|
else if (normalized.Contains(PenumbraPrefix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
created = CreateFileEntry(originalPath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!entry.Value.Contains(CachePrefix, StringComparison.Ordinal))
|
created = CreateFileEntry(originalPath) ?? CreateCacheEntry(originalPath);
|
||||||
result.Add(entry.Key, CreateFileEntry(entry.Key));
|
|
||||||
else
|
|
||||||
result.Add(entry.Key, CreateCacheEntry(entry.Key));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result[originalPath] = created;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -220,16 +309,24 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
public void RemoveHashedFile(string hash, string prefixedFilePath)
|
public void RemoveHashedFile(string hash, string prefixedFilePath)
|
||||||
{
|
{
|
||||||
|
var normalizedPath = NormalizePrefixedPathKey(prefixedFilePath);
|
||||||
|
|
||||||
if (_fileCaches.TryGetValue(hash, out var caches))
|
if (_fileCaches.TryGetValue(hash, out var caches))
|
||||||
{
|
{
|
||||||
var removedCount = caches?.RemoveAll(c => string.Equals(c.PrefixedFilePath, prefixedFilePath, StringComparison.Ordinal));
|
_logger.LogTrace("Removing from DB: {hash} => {path}", hash, prefixedFilePath);
|
||||||
_logger.LogTrace("Removed from DB: {count} file(s) with hash {hash} and file cache {path}", removedCount, hash, prefixedFilePath);
|
|
||||||
|
|
||||||
if (caches?.Count == 0)
|
if (caches.TryRemove(normalizedPath, out var removedEntity))
|
||||||
{
|
{
|
||||||
_fileCaches.Remove(hash, out var entity);
|
_logger.LogTrace("Removed from DB: {hash} => {path}", hash, removedEntity.PrefixedFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caches.IsEmpty)
|
||||||
|
{
|
||||||
|
_fileCaches.TryRemove(hash, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_fileCachesByPrefixedPath.TryRemove(normalizedPath, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true)
|
public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true)
|
||||||
@@ -270,7 +367,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
lock (_fileWriteLock)
|
lock (_fileWriteLock)
|
||||||
{
|
{
|
||||||
StringBuilder sb = new();
|
StringBuilder sb = new();
|
||||||
foreach (var entry in _fileCaches.SelectMany(k => k.Value).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
foreach (var entry in _fileCaches.Values.SelectMany(k => k.Values).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
sb.AppendLine(entry.CsvEntry);
|
sb.AppendLine(entry.CsvEntry);
|
||||||
}
|
}
|
||||||
@@ -315,16 +412,11 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
private void AddHashedFile(FileCacheEntity fileCache)
|
private void AddHashedFile(FileCacheEntity fileCache)
|
||||||
{
|
{
|
||||||
if (!_fileCaches.TryGetValue(fileCache.Hash, out var entries) || entries is null)
|
var normalizedPath = NormalizePrefixedPathKey(fileCache.PrefixedFilePath);
|
||||||
{
|
var entries = _fileCaches.GetOrAdd(fileCache.Hash, _ => new ConcurrentDictionary<string, FileCacheEntity>(StringComparer.OrdinalIgnoreCase));
|
||||||
_fileCaches[fileCache.Hash] = entries = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entries.Exists(u => string.Equals(u.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.OrdinalIgnoreCase)))
|
entries[normalizedPath] = fileCache;
|
||||||
{
|
_fileCachesByPrefixedPath[normalizedPath] = fileCache;
|
||||||
//_logger.LogTrace("Adding to DB: {hash} => {path}", fileCache.Hash, fileCache.PrefixedFilePath);
|
|
||||||
entries.Add(fileCache);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null)
|
private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null)
|
||||||
@@ -366,6 +458,12 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
private FileCacheEntity? Validate(FileCacheEntity fileCache)
|
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);
|
var file = new FileInfo(fileCache.ResolvedFilepath);
|
||||||
if (!file.Exists)
|
if (!file.Exists)
|
||||||
{
|
{
|
||||||
@@ -439,7 +537,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
{
|
{
|
||||||
attempts++;
|
attempts++;
|
||||||
_logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath);
|
_logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath);
|
||||||
Thread.Sleep(100);
|
Task.Delay(100, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
private readonly HashSet<string> _cachedHandledPaths = new(StringComparer.Ordinal);
|
private readonly HashSet<string> _cachedHandledPaths = new(StringComparer.Ordinal);
|
||||||
private readonly TransientConfigService _configurationService;
|
private readonly TransientConfigService _configurationService;
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
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 string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
|
||||||
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
|
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
|
||||||
private ConcurrentDictionary<IntPtr, ObjectKind> _cachedFrameAddresses = [];
|
private ConcurrentDictionary<IntPtr, ObjectKind> _cachedFrameAddresses = [];
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Dalamud.Game.Text;
|
||||||
|
using LightlessSync.UtilsEnum.Enum;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.UI;
|
using LightlessSync.UI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -20,6 +22,7 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
||||||
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
||||||
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u);
|
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u);
|
||||||
|
public bool UseLightlessRedesign { get; set; } = true;
|
||||||
public bool EnableRightClickMenus { get; set; } = true;
|
public bool EnableRightClickMenus { get; set; } = true;
|
||||||
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
||||||
public string ExportFolder { get; set; } = string.Empty;
|
public string ExportFolder { get; set; } = string.Empty;
|
||||||
@@ -32,6 +35,9 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool OpenGposeImportOnGposeStart { get; set; } = false;
|
public bool OpenGposeImportOnGposeStart { get; set; } = false;
|
||||||
public bool OpenPopupOnAdd { get; set; } = true;
|
public bool OpenPopupOnAdd { get; set; } = true;
|
||||||
public int ParallelDownloads { get; set; } = 10;
|
public int ParallelDownloads { get; set; } = 10;
|
||||||
|
public int ParallelUploads { get; set; } = 8;
|
||||||
|
public bool EnablePairProcessingLimiter { get; set; } = true;
|
||||||
|
public int MaxConcurrentPairApplications { get; set; } = 3;
|
||||||
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
||||||
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
||||||
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||||
@@ -43,6 +49,7 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool ShowCharacterNameInsteadOfNotesForVisible { get; set; } = false;
|
public bool ShowCharacterNameInsteadOfNotesForVisible { get; set; } = false;
|
||||||
public bool ShowOfflineUsersSeparately { get; set; } = true;
|
public bool ShowOfflineUsersSeparately { get; set; } = true;
|
||||||
public bool ShowSyncshellOfflineUsersSeparately { get; set; } = true;
|
public bool ShowSyncshellOfflineUsersSeparately { get; set; } = true;
|
||||||
|
public bool ShowGroupedSyncshellsInAll { get; set; } = true;
|
||||||
public bool GroupUpSyncshells { get; set; } = true;
|
public bool GroupUpSyncshells { get; set; } = true;
|
||||||
public bool ShowOnlineNotifications { get; set; } = false;
|
public bool ShowOnlineNotifications { get; set; } = false;
|
||||||
public bool ShowOnlineNotificationsOnlyForIndividualPairs { get; set; } = true;
|
public bool ShowOnlineNotificationsOnlyForIndividualPairs { get; set; } = true;
|
||||||
@@ -65,4 +72,19 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool UseFocusTarget { get; set; } = false;
|
public bool UseFocusTarget { get; set; } = false;
|
||||||
public bool overrideFriendColor { get; set; } = false;
|
public bool overrideFriendColor { get; set; } = false;
|
||||||
public bool overridePartyColor { get; set; } = false;
|
public bool overridePartyColor { get; set; } = false;
|
||||||
}
|
public bool overrideFcTagColor { get; set; } = false;
|
||||||
|
public bool useColoredUIDs { get; set; } = true;
|
||||||
|
public bool BroadcastEnabled { get; set; } = false;
|
||||||
|
public short LightfinderLabelOffsetX { get; set; } = 0;
|
||||||
|
public short LightfinderLabelOffsetY { get; set; } = 0;
|
||||||
|
public bool LightfinderLabelUseIcon { get; set; } = false;
|
||||||
|
public bool LightfinderLabelShowOwn { get; set; } = true;
|
||||||
|
public bool LightfinderLabelShowPaired { get; set; } = true;
|
||||||
|
public string LightfinderLabelIconGlyph { get; set; } = SeIconCharExtensions.ToIconString(SeIconChar.Hyadelyn);
|
||||||
|
public float LightfinderLabelScale { get; set; } = 1.0f;
|
||||||
|
public bool LightfinderAutoAlign { get; set; } = true;
|
||||||
|
public LabelAlignment LabelAlignment { get; set; } = LabelAlignment.Left;
|
||||||
|
public DateTime BroadcastTtl { get; set; } = DateTime.MinValue;
|
||||||
|
public bool SyncshellFinderEnabled { get; set; } = false;
|
||||||
|
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,4 +14,6 @@ public class PlayerPerformanceConfig : ILightlessConfiguration
|
|||||||
public int TrisAutoPauseThresholdThousands { get; set; } = 250;
|
public int TrisAutoPauseThresholdThousands { get; set; } = 250;
|
||||||
public List<string> UIDsToIgnore { get; set; } = new();
|
public List<string> UIDsToIgnore { get; set; } = new();
|
||||||
public bool PauseInInstanceDuty { get; set; } = false;
|
public bool PauseInInstanceDuty { get; set; } = false;
|
||||||
|
public bool PauseWhilePerforming { get; set; } = true;
|
||||||
|
public bool PauseInCombat { get; set; } = true;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
|
|
||||||
|
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
public class ServerTagConfig : ILightlessConfiguration
|
||||||
|
{
|
||||||
|
public Dictionary<string, ServerTagStorage> ServerTagStorage { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace LightlessSync.LightlessConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ServerTagStorage
|
||||||
|
{
|
||||||
|
public HashSet<string> OpenPairTags { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<string, List<string>> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace LightlessSync.LightlessConfiguration;
|
||||||
|
|
||||||
|
public class ServerTagConfigService : ConfigurationServiceBase<ServerTagConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "servertags.json";
|
||||||
|
|
||||||
|
public ServerTagConfigService(string configDir) : base(configDir)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors></Authors>
|
<Authors></Authors>
|
||||||
<Company></Company>
|
<Company></Company>
|
||||||
<Version>1.11.6</Version>
|
<Version>1.12.1-beta.1</Version>
|
||||||
<Description></Description>
|
<Description></Description>
|
||||||
<Copyright></Copyright>
|
<Copyright></Copyright>
|
||||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
@@ -21,6 +21,7 @@ public class PairHandlerFactory
|
|||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||||
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
private readonly ServerConfigurationManager _serverConfigManager;
|
private readonly ServerConfigurationManager _serverConfigManager;
|
||||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ public class PairHandlerFactory
|
|||||||
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
||||||
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
||||||
FileCacheManager fileCacheManager, LightlessMediator lightlessMediator, PlayerPerformanceService playerPerformanceService,
|
FileCacheManager fileCacheManager, LightlessMediator lightlessMediator, PlayerPerformanceService playerPerformanceService,
|
||||||
|
PairProcessingLimiter pairProcessingLimiter,
|
||||||
ServerConfigurationManager serverConfigManager)
|
ServerConfigurationManager serverConfigManager)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
@@ -40,6 +42,7 @@ public class PairHandlerFactory
|
|||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
_playerPerformanceService = playerPerformanceService;
|
_playerPerformanceService = playerPerformanceService;
|
||||||
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
_serverConfigManager = serverConfigManager;
|
_serverConfigManager = serverConfigManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +50,6 @@ public class PairHandlerFactory
|
|||||||
{
|
{
|
||||||
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _gameObjectHandlerFactory,
|
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _gameObjectHandlerFactory,
|
||||||
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
||||||
_fileCacheManager, _lightlessMediator, _playerPerformanceService, _serverConfigManager);
|
_fileCacheManager, _lightlessMediator, _playerPerformanceService, _pairProcessingLimiter, _serverConfigManager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
using LightlessSync.PlayerData.Factories;
|
using LightlessSync.PlayerData.Factories;
|
||||||
@@ -28,6 +28,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly IHostApplicationLifetime _lifetime;
|
private readonly IHostApplicationLifetime _lifetime;
|
||||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||||
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
private readonly ServerConfigurationManager _serverConfigManager;
|
private readonly ServerConfigurationManager _serverConfigManager;
|
||||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||||
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
||||||
@@ -50,6 +51,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
||||||
FileCacheManager fileDbManager, LightlessMediator mediator,
|
FileCacheManager fileDbManager, LightlessMediator mediator,
|
||||||
PlayerPerformanceService playerPerformanceService,
|
PlayerPerformanceService playerPerformanceService,
|
||||||
|
PairProcessingLimiter pairProcessingLimiter,
|
||||||
ServerConfigurationManager serverConfigManager) : base(logger, mediator)
|
ServerConfigurationManager serverConfigManager) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
Pair = pair;
|
Pair = pair;
|
||||||
@@ -61,6 +63,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
_lifetime = lifetime;
|
_lifetime = lifetime;
|
||||||
_fileDbManager = fileDbManager;
|
_fileDbManager = fileDbManager;
|
||||||
_playerPerformanceService = playerPerformanceService;
|
_playerPerformanceService = playerPerformanceService;
|
||||||
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
_serverConfigManager = serverConfigManager;
|
_serverConfigManager = serverConfigManager;
|
||||||
_penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, Pair.UserData.UID).ConfigureAwait(false).GetAwaiter().GetResult();
|
_penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, Pair.UserData.UID).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||||
|
|
||||||
@@ -88,11 +91,19 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
_redrawOnNextApplication = true;
|
_redrawOnNextApplication = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Mediator.Subscribe<CombatOrPerformanceEndMessage>(this, (msg) =>
|
Mediator.Subscribe<CombatEndMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
EnableSync();
|
EnableSync();
|
||||||
});
|
});
|
||||||
Mediator.Subscribe<CombatOrPerformanceStartMessage>(this, _ =>
|
Mediator.Subscribe<CombatStartMessage>(this, _ =>
|
||||||
|
{
|
||||||
|
DisableSync();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<PerformanceEndMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
EnableSync();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<PerformanceStartMessage>(this, _ =>
|
||||||
{
|
{
|
||||||
DisableSync();
|
DisableSync();
|
||||||
});
|
});
|
||||||
@@ -137,11 +148,21 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
||||||
{
|
{
|
||||||
if (_dalamudUtil.IsInCombatOrPerforming)
|
if (_dalamudUtil.IsInCombat)
|
||||||
{
|
{
|
||||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||||
"Cannot apply character data: you are in combat or performing music, deferring application")));
|
"Cannot apply character data: you are in combat, deferring application")));
|
||||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat or performing", applicationBase);
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
||||||
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||||
|
SetUploading(isUploading: false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_dalamudUtil.IsPerforming)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||||
|
"Cannot apply character data: you are performing music, deferring application")));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase);
|
||||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||||
SetUploading(isUploading: false);
|
SetUploading(isUploading: false);
|
||||||
return;
|
return;
|
||||||
@@ -402,6 +423,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
||||||
bool updateModdedPaths, bool updateManip, CancellationToken downloadToken)
|
bool updateModdedPaths, bool updateManip, CancellationToken downloadToken)
|
||||||
{
|
{
|
||||||
|
await using var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false);
|
||||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
|
Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
|
||||||
|
|
||||||
if (updateModdedPaths)
|
if (updateModdedPaths)
|
||||||
@@ -719,6 +741,11 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[BASE-{appBase}] Modded path calculation cancelled", applicationBase);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "[BASE-{appBase}] Something went wrong during calculation replacements", applicationBase);
|
Logger.LogError(ex, "[BASE-{appBase}] Something went wrong during calculation replacements", applicationBase);
|
||||||
@@ -745,4 +772,4 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
_dataReceivedInDowntime = null;
|
_dataReceivedInDowntime = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Comparer;
|
using LightlessSync.API.Data.Comparer;
|
||||||
using LightlessSync.API.Data.Extensions;
|
using LightlessSync.API.Data.Extensions;
|
||||||
@@ -7,10 +7,14 @@ using LightlessSync.API.Dto.User;
|
|||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.PlayerData.Factories;
|
using LightlessSync.PlayerData.Factories;
|
||||||
|
using LightlessSync.Services;
|
||||||
|
|
||||||
using LightlessSync.Services.Events;
|
using LightlessSync.Services.Events;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Pairs;
|
namespace LightlessSync.PlayerData.Pairs;
|
||||||
|
|
||||||
@@ -24,14 +28,19 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
private Lazy<List<Pair>> _directPairsInternal;
|
private Lazy<List<Pair>> _directPairsInternal;
|
||||||
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> _groupPairsInternal;
|
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> _groupPairsInternal;
|
||||||
private Lazy<Dictionary<Pair, List<GroupFullInfoDto>>> _pairsWithGroupsInternal;
|
private Lazy<Dictionary<Pair, List<GroupFullInfoDto>>> _pairsWithGroupsInternal;
|
||||||
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
|
private readonly ConcurrentQueue<(Pair Pair, OnlineUserIdentDto? Ident)> _pairCreationQueue = new();
|
||||||
|
private CancellationTokenSource _pairCreationCts = new();
|
||||||
|
private int _pairCreationProcessorRunning;
|
||||||
|
|
||||||
public PairManager(ILogger<PairManager> logger, PairFactory pairFactory,
|
public PairManager(ILogger<PairManager> logger, PairFactory pairFactory,
|
||||||
LightlessConfigService configurationService, LightlessMediator mediator,
|
LightlessConfigService configurationService, LightlessMediator mediator,
|
||||||
IContextMenu dalamudContextMenu) : base(logger, mediator)
|
IContextMenu dalamudContextMenu, PairProcessingLimiter pairProcessingLimiter) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_pairFactory = pairFactory;
|
_pairFactory = pairFactory;
|
||||||
_configurationService = configurationService;
|
_configurationService = configurationService;
|
||||||
_dalamudContextMenu = dalamudContextMenu;
|
_dalamudContextMenu = dalamudContextMenu;
|
||||||
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
Mediator.Subscribe<DisconnectedMessage>(this, (_) => ClearPairs());
|
Mediator.Subscribe<DisconnectedMessage>(this, (_) => ClearPairs());
|
||||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => ReapplyPairData());
|
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => ReapplyPairData());
|
||||||
_directPairsInternal = DirectPairsLazy();
|
_directPairsInternal = DirectPairsLazy();
|
||||||
@@ -112,6 +121,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
public void ClearPairs()
|
public void ClearPairs()
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Clearing all Pairs");
|
Logger.LogDebug("Clearing all Pairs");
|
||||||
|
ResetPairCreationQueue();
|
||||||
DisposePairs();
|
DisposePairs();
|
||||||
_allClientPairs.Clear();
|
_allClientPairs.Clear();
|
||||||
_allGroups.Clear();
|
_allGroups.Clear();
|
||||||
@@ -161,7 +171,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5)));
|
Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pair.CreateCachedPlayer(dto);
|
QueuePairCreation(pair, dto);
|
||||||
|
|
||||||
RecreateLazy();
|
RecreateLazy();
|
||||||
}
|
}
|
||||||
@@ -332,6 +342,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
ResetPairCreationQueue();
|
||||||
_dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu;
|
_dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu;
|
||||||
|
|
||||||
DisposePairs();
|
DisposePairs();
|
||||||
@@ -390,6 +401,84 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void QueuePairCreation(Pair pair, OnlineUserIdentDto? dto)
|
||||||
|
{
|
||||||
|
if (pair.HasCachedPlayer)
|
||||||
|
{
|
||||||
|
RecreateLazy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pairCreationQueue.Enqueue((pair, dto));
|
||||||
|
StartPairCreationProcessor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartPairCreationProcessor()
|
||||||
|
{
|
||||||
|
if (_pairCreationCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Interlocked.CompareExchange(ref _pairCreationProcessorRunning, 1, 0) == 0)
|
||||||
|
{
|
||||||
|
_ = Task.Run(ProcessPairCreationQueueAsync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessPairCreationQueueAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!_pairCreationCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (!_pairCreationQueue.TryDequeue(out var work))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var lease = await _pairProcessingLimiter.AcquireAsync(_pairCreationCts.Token).ConfigureAwait(false);
|
||||||
|
if (!work.Pair.HasCachedPlayer)
|
||||||
|
{
|
||||||
|
work.Pair.CreateCachedPlayer(work.Ident);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error creating cached player for {uid}", work.Pair.UserData.UID);
|
||||||
|
}
|
||||||
|
|
||||||
|
RecreateLazy();
|
||||||
|
await Task.Yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _pairCreationProcessorRunning, 0);
|
||||||
|
if (!_pairCreationQueue.IsEmpty && !_pairCreationCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
StartPairCreationProcessor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetPairCreationQueue()
|
||||||
|
{
|
||||||
|
_pairCreationCts.Cancel();
|
||||||
|
while (_pairCreationQueue.TryDequeue(out _))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
_pairCreationCts.Dispose();
|
||||||
|
_pairCreationCts = new CancellationTokenSource();
|
||||||
|
Interlocked.Exchange(ref _pairCreationProcessorRunning, 0);
|
||||||
|
}
|
||||||
|
|
||||||
private void ReapplyPairData()
|
private void ReapplyPairData()
|
||||||
{
|
{
|
||||||
foreach (var pair in _allClientPairs.Select(k => k.Value))
|
foreach (var pair in _allClientPairs.Select(k => k.Value))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
@@ -101,6 +101,8 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
forced |= _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash;
|
forced |= _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash;
|
||||||
|
|
||||||
if (_fileUploadTask == null || (_fileUploadTask?.IsCompleted ?? false) || forced)
|
if (_fileUploadTask == null || (_fileUploadTask?.IsCompleted ?? false) || forced)
|
||||||
@@ -127,6 +129,15 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
|||||||
_pushDataSemaphore.Release();
|
_pushDataSemaphore.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (_runtimeCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("PushCharacterData cancelled");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Failed to push character data");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Game;
|
using Dalamud.Game;
|
||||||
using Dalamud.Game.ClientState.Objects;
|
using Dalamud.Game.ClientState.Objects;
|
||||||
using Dalamud.Interface.ImGuiFileDialog;
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
@@ -41,7 +41,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui,
|
IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui,
|
||||||
IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager,
|
IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager,
|
||||||
ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, IGameConfig gameConfig,
|
ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, IGameConfig gameConfig,
|
||||||
ISigScanner sigScanner, INamePlateGui namePlateGui)
|
ISigScanner sigScanner, INamePlateGui namePlateGui, IAddonLifecycle addonLifecycle)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName))
|
if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName))
|
||||||
Directory.CreateDirectory(pluginInterface.ConfigDirectory.FullName);
|
Directory.CreateDirectory(pluginInterface.ConfigDirectory.FullName);
|
||||||
@@ -90,6 +90,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton(new WindowSystem("LightlessSync"));
|
collection.AddSingleton(new WindowSystem("LightlessSync"));
|
||||||
collection.AddSingleton<FileDialogManager>();
|
collection.AddSingleton<FileDialogManager>();
|
||||||
collection.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", "", useEmbedded: true));
|
collection.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", "", useEmbedded: true));
|
||||||
|
collection.AddSingleton(gameGui);
|
||||||
|
|
||||||
// add lightless related singletons
|
// add lightless related singletons
|
||||||
collection.AddSingleton<LightlessMediator>();
|
collection.AddSingleton<LightlessMediator>();
|
||||||
@@ -105,6 +106,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<GameObjectHandlerFactory>();
|
collection.AddSingleton<GameObjectHandlerFactory>();
|
||||||
collection.AddSingleton<FileDownloadManagerFactory>();
|
collection.AddSingleton<FileDownloadManagerFactory>();
|
||||||
collection.AddSingleton<PairHandlerFactory>();
|
collection.AddSingleton<PairHandlerFactory>();
|
||||||
|
collection.AddSingleton<PairProcessingLimiter>();
|
||||||
collection.AddSingleton<PairFactory>();
|
collection.AddSingleton<PairFactory>();
|
||||||
collection.AddSingleton<XivDataAnalyzer>();
|
collection.AddSingleton<XivDataAnalyzer>();
|
||||||
collection.AddSingleton<CharacterAnalyzer>();
|
collection.AddSingleton<CharacterAnalyzer>();
|
||||||
@@ -112,6 +114,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<PluginWarningNotificationService>();
|
collection.AddSingleton<PluginWarningNotificationService>();
|
||||||
collection.AddSingleton<FileCompactor>();
|
collection.AddSingleton<FileCompactor>();
|
||||||
collection.AddSingleton<TagHandler>();
|
collection.AddSingleton<TagHandler>();
|
||||||
|
collection.AddSingleton<PairRequestService>();
|
||||||
collection.AddSingleton<IdDisplayHandler>();
|
collection.AddSingleton<IdDisplayHandler>();
|
||||||
collection.AddSingleton<PlayerPerformanceService>();
|
collection.AddSingleton<PlayerPerformanceService>();
|
||||||
collection.AddSingleton<TransientResourceManager>();
|
collection.AddSingleton<TransientResourceManager>();
|
||||||
@@ -142,8 +145,13 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService<ILogger<DtrEntry>>(), dtrBar, s.GetRequiredService<LightlessConfigService>(),
|
collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService<ILogger<DtrEntry>>(), dtrBar, s.GetRequiredService<LightlessConfigService>(),
|
||||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>()));
|
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>()));
|
||||||
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
|
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
|
||||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu));
|
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu, s.GetRequiredService<PairProcessingLimiter>()));
|
||||||
collection.AddSingleton<RedrawManager>();
|
collection.AddSingleton<RedrawManager>();
|
||||||
|
collection.AddSingleton<BroadcastService>();
|
||||||
|
collection.AddSingleton(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,
|
collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService<ILogger<IpcCallerPenumbra>>(), pluginInterface,
|
||||||
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<RedrawManager>()));
|
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<RedrawManager>()));
|
||||||
collection.AddSingleton((s) => new IpcCallerGlamourer(s.GetRequiredService<ILogger<IpcCallerGlamourer>>(), pluginInterface,
|
collection.AddSingleton((s) => new IpcCallerGlamourer(s.GetRequiredService<ILogger<IpcCallerGlamourer>>(), pluginInterface,
|
||||||
@@ -174,7 +182,12 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
||||||
return httpClient;
|
return httpClient;
|
||||||
});
|
});
|
||||||
collection.AddSingleton((s) => new 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 ServerConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton((s) => new PairTagConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new PairTagConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
@@ -194,8 +207,9 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
|
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
|
||||||
collection.AddSingleton<ConfigurationMigrator>();
|
collection.AddSingleton<ConfigurationMigrator>();
|
||||||
collection.AddSingleton<ConfigurationSaveService>();
|
collection.AddSingleton<ConfigurationSaveService>();
|
||||||
|
|
||||||
collection.AddSingleton<HubFactory>();
|
collection.AddSingleton<HubFactory>();
|
||||||
|
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
|
// add scoped services
|
||||||
collection.AddScoped<DrawEntityFactory>();
|
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<LightlessMediator>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<FileDialogManager>(),
|
||||||
s.GetRequiredService<LightlessProfileManager>(), s.GetRequiredService<PerformanceCollectorService>()));
|
s.GetRequiredService<LightlessProfileManager>(), s.GetRequiredService<PerformanceCollectorService>()));
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
|
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, BanUserPopupHandler>();
|
||||||
collection.AddScoped<IPopupHandler, CensusPopupHandler>();
|
collection.AddScoped<IPopupHandler, CensusPopupHandler>();
|
||||||
collection.AddScoped<CacheCreationService>();
|
collection.AddScoped<CacheCreationService>();
|
||||||
@@ -236,6 +252,8 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
s.GetRequiredService<LightlessMediator>()));
|
s.GetRequiredService<LightlessMediator>()));
|
||||||
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), namePlateGui, clientState,
|
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), namePlateGui, clientState,
|
||||||
s.GetRequiredService<PairManager>(), s.GetRequiredService<LightlessMediator>()));
|
s.GetRequiredService<PairManager>(), s.GetRequiredService<LightlessMediator>()));
|
||||||
|
collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService<ILogger<NameplateHandler>>(), addonLifecycle, gameGui, s.GetRequiredService<DalamudUtilService>(),
|
||||||
|
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), clientState, s.GetRequiredService<PairManager>()));
|
||||||
|
|
||||||
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<LightlessMediator>());
|
collection.AddHostedService(p => p.GetRequiredService<LightlessMediator>());
|
||||||
@@ -248,6 +266,8 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());
|
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
|
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<LightlessPlugin>());
|
collection.AddHostedService(p => p.GetRequiredService<LightlessPlugin>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<ContextMenuService>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<BroadcastService>());
|
||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -944,9 +944,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
Logger.LogTrace("[{appId}] Computing local missing files", applicationId);
|
Logger.LogTrace("[{appId}] Computing local missing files", applicationId);
|
||||||
|
|
||||||
Dictionary<string, string> modPaths;
|
_fileHandler.ComputeMissingFiles(charaDataDownloadDto, out Dictionary<string, string> modPaths, out List<FileReplacementData> missingFiles);
|
||||||
List<FileReplacementData> missingFiles;
|
|
||||||
_fileHandler.ComputeMissingFiles(charaDataDownloadDto, out modPaths, out missingFiles);
|
|
||||||
|
|
||||||
Logger.LogTrace("[{appId}] Computing local missing files", applicationId);
|
Logger.LogTrace("[{appId}] Computing local missing files", applicationId);
|
||||||
|
|
||||||
@@ -990,7 +988,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_uploadCts = _uploadCts.CancelRecreate();
|
_uploadCts = _uploadCts.CancelRecreate();
|
||||||
var missingFiles = await _fileHandler.UploadFiles([.. missingFileList.Select(k => k.HashOrFileSwap)], UploadProgress, _uploadCts.Token).ConfigureAwait(false);
|
var missingFiles = await _fileHandler.UploadFiles([.. missingFileList.Select(k => k.HashOrFileSwap)], UploadProgress, _uploadCts.Token).ConfigureAwait(false);
|
||||||
if (missingFiles.Any())
|
if (missingFiles.Count != 0)
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Failed to upload {files}", string.Join(", ", missingFiles));
|
Logger.LogInformation("Failed to upload {files}", string.Join(", ", missingFiles));
|
||||||
return ($"Upload failed: {missingFiles.Count} missing or forbidden to upload local files.", false);
|
return ($"Upload failed: {missingFiles.Count} missing or forbidden to upload local files.", false);
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_charaDataConfigService.Current.NearbyDrawWisps && !_dalamudUtilService.IsInGpose && !_dalamudUtilService.IsInCombatOrPerforming)
|
if (_charaDataConfigService.Current.NearbyDrawWisps && !_dalamudUtilService.IsInGpose && !_dalamudUtilService.IsInCombat && !_dalamudUtilService.IsPerforming && !_dalamudUtilService.IsInInstance)
|
||||||
await _dalamudUtilService.RunOnFrameworkThread(() => ManageWispsNearby(previousPoses)).ConfigureAwait(false);
|
await _dalamudUtilService.RunOnFrameworkThread(() => ManageWispsNearby(previousPoses)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_charaDataConfigService.Current.NearbyDrawWisps || _dalamudUtilService.IsInGpose || _dalamudUtilService.IsInCombatOrPerforming)
|
if (!_charaDataConfigService.Current.NearbyDrawWisps || _dalamudUtilService.IsInGpose || _dalamudUtilService.IsInCombat || _dalamudUtilService.IsPerforming || _dalamudUtilService.IsInInstance)
|
||||||
ClearAllVfx();
|
ClearAllVfx();
|
||||||
|
|
||||||
var camera = CameraManager.Instance()->CurrentCamera;
|
var camera = CameraManager.Instance()->CurrentCamera;
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
"\t /light toggle on|off - Connects or disconnects to Lightless respectively" + Environment.NewLine +
|
"\t /light toggle on|off - Connects or disconnects to Lightless respectively" + Environment.NewLine +
|
||||||
"\t /light gpose - Opens the Lightless Character Data Hub window" + Environment.NewLine +
|
"\t /light gpose - Opens the Lightless Character Data Hub window" + Environment.NewLine +
|
||||||
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
||||||
"\t /light settings - Opens the Lightless Settings window"
|
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
|
||||||
|
"\t /light lightfinder - Opens the Lightfinder window"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,5 +123,9 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
{
|
{
|
||||||
_mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
_mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
||||||
}
|
}
|
||||||
|
else if (string.Equals(splitArgs[0], "lightfinder", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_mediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
210
LightlessSync/Services/ContextMenuService.cs
Normal file
210
LightlessSync/Services/ContextMenuService.cs
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
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;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
|
|
||||||
|
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;
|
||||||
|
_configService = configService;
|
||||||
|
_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;
|
||||||
|
|
||||||
|
if (!_configService.Current.EnableRightClickMenus)
|
||||||
|
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;
|
||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
@@ -162,7 +162,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
public bool IsLoggedIn { get; private set; }
|
public bool IsLoggedIn { get; private set; }
|
||||||
public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread;
|
public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread;
|
||||||
public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
||||||
public bool IsInCombatOrPerforming { get; private set; } = false;
|
public bool IsInCombat { get; private set; } = false;
|
||||||
|
public bool IsPerforming { get; private set; } = false;
|
||||||
public bool IsInInstance { get; private set; } = false;
|
public bool IsInInstance { get; private set; } = false;
|
||||||
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
|
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
|
||||||
public uint ClassJobId => _classJobId!.Value;
|
public uint ClassJobId => _classJobId!.Value;
|
||||||
@@ -312,7 +313,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return await RunOnFrameworkThread(() => _cid.Value.ToString().GetHash256()).ConfigureAwait(false);
|
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();
|
return ((BattleChara*)ptr)->Character.ContentId.ToString().GetHash256();
|
||||||
}
|
}
|
||||||
@@ -420,6 +421,16 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return await RunOnFrameworkThread(() => IsObjectPresent(obj)).ConfigureAwait(false);
|
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)
|
public async Task RunOnFrameworkThread(System.Action act, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)
|
||||||
{
|
{
|
||||||
var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
|
var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
|
||||||
@@ -530,7 +541,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
curWaitTime += tick;
|
curWaitTime += tick;
|
||||||
Thread.Sleep(tick);
|
Thread.Sleep(tick);
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread.Sleep(tick * 2);
|
Thread.Sleep(tick * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,6 +556,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return result;
|
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)
|
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
|
||||||
{
|
{
|
||||||
var gameObj = (GameObject*)address;
|
var gameObj = (GameObject*)address;
|
||||||
@@ -670,19 +692,33 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
Mediator.Publish(new GposeEndMessage());
|
Mediator.Publish(new GposeEndMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((_condition[ConditionFlag.Performing] || _condition[ConditionFlag.InCombat]) && !IsInCombatOrPerforming && (_condition[ConditionFlag.BoundByDuty] && !_playerPerformanceConfigService.Current.PauseInInstanceDuty))
|
if ((_condition[ConditionFlag.InCombat]) && !IsInCombat && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Combat/Performance start");
|
_logger.LogDebug("Combat start");
|
||||||
IsInCombatOrPerforming = true;
|
IsInCombat = true;
|
||||||
Mediator.Publish(new CombatOrPerformanceStartMessage());
|
Mediator.Publish(new CombatStartMessage());
|
||||||
Mediator.Publish(new HaltScanMessage(nameof(IsInCombatOrPerforming)));
|
Mediator.Publish(new HaltScanMessage(nameof(IsInCombat)));
|
||||||
}
|
}
|
||||||
else if ((!_condition[ConditionFlag.Performing] && !_condition[ConditionFlag.InCombat]) && IsInCombatOrPerforming && (_condition[ConditionFlag.BoundByDuty] && !_playerPerformanceConfigService.Current.PauseInInstanceDuty))
|
else if ((!_condition[ConditionFlag.InCombat]) && IsInCombat && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Combat/Performance end");
|
_logger.LogDebug("Combat end");
|
||||||
IsInCombatOrPerforming = false;
|
IsInCombat = false;
|
||||||
Mediator.Publish(new CombatOrPerformanceEndMessage());
|
Mediator.Publish(new CombatEndMessage());
|
||||||
Mediator.Publish(new ResumeScanMessage(nameof(IsInCombatOrPerforming)));
|
Mediator.Publish(new ResumeScanMessage(nameof(IsInCombat)));
|
||||||
|
}
|
||||||
|
if (_condition[ConditionFlag.Performing] && !IsPerforming && _playerPerformanceConfigService.Current.PauseWhilePerforming)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Performance start");
|
||||||
|
IsInCombat = true;
|
||||||
|
Mediator.Publish(new PerformanceStartMessage());
|
||||||
|
Mediator.Publish(new HaltScanMessage(nameof(IsPerforming)));
|
||||||
|
}
|
||||||
|
else if (!_condition[ConditionFlag.Performing] && IsPerforming && _playerPerformanceConfigService.Current.PauseWhilePerforming)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Performance end");
|
||||||
|
IsInCombat = false;
|
||||||
|
Mediator.Publish(new PerformanceEndMessage());
|
||||||
|
Mediator.Publish(new ResumeScanMessage(nameof(IsPerforming)));
|
||||||
}
|
}
|
||||||
if ((_condition[ConditionFlag.BoundByDuty]) && !IsInInstance && _playerPerformanceConfigService.Current.PauseInInstanceDuty)
|
if ((_condition[ConditionFlag.BoundByDuty]) && !IsInInstance && _playerPerformanceConfigService.Current.PauseInInstanceDuty)
|
||||||
{
|
{
|
||||||
@@ -752,7 +788,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
_classJobId = localPlayer.ClassJob.RowId;
|
_classJobId = localPlayer.ClassJob.RowId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsInCombatOrPerforming)
|
if (!IsInCombat || !IsPerforming || !IsInInstance)
|
||||||
Mediator.Publish(new FrameworkUpdateMessage());
|
Mediator.Publish(new FrameworkUpdateMessage());
|
||||||
|
|
||||||
Mediator.Publish(new PriorityFrameworkUpdateMessage());
|
Mediator.Publish(new PriorityFrameworkUpdateMessage());
|
||||||
@@ -781,7 +817,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
IsLodEnabled = lodEnabled;
|
IsLodEnabled = lodEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsInCombatOrPerforming)
|
if (IsInCombat || IsPerforming || IsInInstance)
|
||||||
Mediator.Publish(new FrameworkUpdateMessage());
|
Mediator.Publish(new FrameworkUpdateMessage());
|
||||||
|
|
||||||
Mediator.Publish(new DelayedFrameworkUpdateMessage());
|
Mediator.Publish(new DelayedFrameworkUpdateMessage());
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Dto;
|
using LightlessSync.API.Dto;
|
||||||
using LightlessSync.API.Dto.CharaData;
|
using LightlessSync.API.Dto.CharaData;
|
||||||
@@ -77,10 +77,13 @@ public record OpenCensusPopupMessage() : MessageBase;
|
|||||||
public record OpenSyncshellAdminPanel(GroupFullInfoDto GroupInfo) : MessageBase;
|
public record OpenSyncshellAdminPanel(GroupFullInfoDto GroupInfo) : MessageBase;
|
||||||
public record OpenPermissionWindow(Pair Pair) : MessageBase;
|
public record OpenPermissionWindow(Pair Pair) : MessageBase;
|
||||||
public record DownloadLimitChangedMessage() : SameThreadMessage;
|
public record DownloadLimitChangedMessage() : SameThreadMessage;
|
||||||
|
public record PairProcessingLimitChangedMessage : SameThreadMessage;
|
||||||
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
|
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
|
||||||
public record TargetPairMessage(Pair Pair) : MessageBase;
|
public record TargetPairMessage(Pair Pair) : MessageBase;
|
||||||
public record CombatOrPerformanceStartMessage : MessageBase;
|
public record CombatStartMessage : MessageBase;
|
||||||
public record CombatOrPerformanceEndMessage : MessageBase;
|
public record CombatEndMessage : MessageBase;
|
||||||
|
public record PerformanceStartMessage : MessageBase;
|
||||||
|
public record PerformanceEndMessage : MessageBase;
|
||||||
public record InstanceOrDutyStartMessage : MessageBase;
|
public record InstanceOrDutyStartMessage : MessageBase;
|
||||||
public record InstanceOrDutyEndMessage : MessageBase;
|
public record InstanceOrDutyEndMessage : MessageBase;
|
||||||
public record EventMessage(Event Event) : MessageBase;
|
public record EventMessage(Event Event) : MessageBase;
|
||||||
@@ -95,7 +98,10 @@ public record GPoseLobbyReceiveCharaData(CharaDataDownloadDto CharaDataDownloadD
|
|||||||
public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase;
|
public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase;
|
||||||
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
||||||
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : 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;
|
public record VisibilityChange : MessageBase;
|
||||||
#pragma warning restore S2094
|
#pragma warning restore S2094
|
||||||
#pragma warning restore MA0048 // File name must match type name
|
#pragma warning restore MA0048 // File name must match type name
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
|
using LightlessSync.UI.Style;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace LightlessSync.Services.Mediator;
|
namespace LightlessSync.Services.Mediator;
|
||||||
@@ -33,6 +34,18 @@ public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber
|
|||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void PreDraw()
|
||||||
|
{
|
||||||
|
base.PreDraw();
|
||||||
|
MainStyle.PushStyle(); // internally checks ShouldUseTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PostDraw()
|
||||||
|
{
|
||||||
|
MainStyle.PopStyle(); // always attempts to pop if pushed
|
||||||
|
base.PostDraw();
|
||||||
|
}
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
_performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal);
|
_performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal);
|
||||||
|
|||||||
597
LightlessSync/Services/NameplateHandler.cs
Normal file
597
LightlessSync/Services/NameplateHandler.cs
Normal file
@@ -0,0 +1,597 @@
|
|||||||
|
using Dalamud.Game.Addon.Lifecycle;
|
||||||
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.PlayerData.Pairs;
|
||||||
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.UI;
|
||||||
|
using LightlessSync.Utils;
|
||||||
|
using LightlessSync.UtilsEnum.Enum;
|
||||||
|
|
||||||
|
// Created using https://github.com/PunishedPineapple/Distance as a reference, thank you!
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
|
public unsafe class NameplateHandler : IMediatorSubscriber
|
||||||
|
{
|
||||||
|
private readonly ILogger<NameplateHandler> _logger;
|
||||||
|
private readonly IAddonLifecycle _addonLifecycle;
|
||||||
|
private readonly IGameGui _gameGui;
|
||||||
|
private readonly IClientState _clientState;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
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];
|
||||||
|
private readonly int[] _cachedNameplateTextWidths = new int[AddonNamePlate.NumNamePlateObjects];
|
||||||
|
private readonly int[] _cachedNameplateTextHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
||||||
|
private readonly int[] _cachedNameplateContainerHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
||||||
|
private readonly int[] _cachedNameplateTextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
|
||||||
|
|
||||||
|
internal const uint mNameplateNodeIDBase = 0x7D99D500;
|
||||||
|
private const string DefaultLabelText = "LightFinder";
|
||||||
|
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
|
||||||
|
private const int ContainerOffsetX = 50;
|
||||||
|
private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
|
||||||
|
|
||||||
|
private volatile HashSet<string> _activeBroadcastingCids = [];
|
||||||
|
|
||||||
|
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairManager pairManager)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_addonLifecycle = addonLifecycle;
|
||||||
|
_gameGui = gameGui;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_configService = configService;
|
||||||
|
_mediator = mediator;
|
||||||
|
_clientState = clientState;
|
||||||
|
_pairManager = pairManager;
|
||||||
|
|
||||||
|
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
||||||
|
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
||||||
|
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
||||||
|
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||||
|
mpNameplateAddon = pNameplateAddon;
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
||||||
|
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
||||||
|
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
||||||
|
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HideAllNameplateNodes()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
||||||
|
{
|
||||||
|
pNode->AtkResNode.ToggleVisibility(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_configService.Current.LightfinderLabelShowOwn && (objectInfo->GameObject->GetGameObjectId() == _clientState.LocalPlayer.GameObjectId))
|
||||||
|
{
|
||||||
|
pNode->AtkResNode.ToggleVisibility(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_configService.Current.LightfinderLabelShowPaired && VisibleUserIds.Any(u => u == objectInfo->GameObject->GetGameObjectId()))
|
||||||
|
{
|
||||||
|
pNode->AtkResNode.ToggleVisibility(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameplateObject = mpNameplateAddon->NamePlateObjectArray[nameplateIndex];
|
||||||
|
nameplateObject.RootComponentNode->Component->UldManager.UpdateDrawNodeList();
|
||||||
|
|
||||||
|
var pNameplateIconNode = nameplateObject.MarkerIcon;
|
||||||
|
var pNameplateResNode = nameplateObject.NameContainer;
|
||||||
|
var pNameplateTextNode = nameplateObject.NameText;
|
||||||
|
bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible());
|
||||||
|
pNode->AtkResNode.ToggleVisibility(IsVisible);
|
||||||
|
|
||||||
|
var nameContainer = nameplateObject.NameContainer;
|
||||||
|
var nameText = nameplateObject.NameText;
|
||||||
|
|
||||||
|
if (nameContainer == null || nameText == null)
|
||||||
|
{
|
||||||
|
pNode->AtkResNode.ToggleVisibility(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var labelColor = UIColors.Get("LightlessPurple");
|
||||||
|
var edgeColor = UIColors.Get("FullBlack");
|
||||||
|
var config = _configService.Current;
|
||||||
|
|
||||||
|
var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
|
||||||
|
var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
||||||
|
var effectiveScale = baseScale * scaleMultiplier;
|
||||||
|
var labelContent = config.LightfinderLabelUseIcon
|
||||||
|
? NormalizeIconGlyph(config.LightfinderLabelIconGlyph)
|
||||||
|
: DefaultLabelText;
|
||||||
|
|
||||||
|
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
||||||
|
pNode->AtkResNode.SetScale(effectiveScale, effectiveScale);
|
||||||
|
var nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||||
|
if (nodeWidth <= 0)
|
||||||
|
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||||
|
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
||||||
|
var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f;
|
||||||
|
var computedFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
||||||
|
pNode->FontSize = (byte)System.Math.Clamp(computedFontSize, 1, 255);
|
||||||
|
AlignmentType alignment;
|
||||||
|
|
||||||
|
var textScaleY = nameText->AtkResNode.ScaleY;
|
||||||
|
if (textScaleY <= 0f)
|
||||||
|
textScaleY = 1f;
|
||||||
|
|
||||||
|
var blockHeight = System.Math.Abs((int)nameplateObject.TextH);
|
||||||
|
if (blockHeight > 0)
|
||||||
|
{
|
||||||
|
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
blockHeight = _cachedNameplateTextHeights[nameplateIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockHeight <= 0)
|
||||||
|
{
|
||||||
|
blockHeight = GetScaledTextHeight(nameText);
|
||||||
|
if (blockHeight <= 0)
|
||||||
|
blockHeight = nodeHeight;
|
||||||
|
|
||||||
|
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
var containerHeight = (int)nameContainer->Height;
|
||||||
|
if (containerHeight > 0)
|
||||||
|
{
|
||||||
|
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
containerHeight = _cachedNameplateContainerHeights[nameplateIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containerHeight <= 0)
|
||||||
|
{
|
||||||
|
containerHeight = blockHeight + (int)System.Math.Round(8 * textScaleY);
|
||||||
|
if (containerHeight <= blockHeight)
|
||||||
|
containerHeight = blockHeight + 1;
|
||||||
|
|
||||||
|
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockTop = containerHeight - blockHeight;
|
||||||
|
if (blockTop < 0)
|
||||||
|
blockTop = 0;
|
||||||
|
var verticalPadding = (int)System.Math.Round(4 * effectiveScale);
|
||||||
|
|
||||||
|
var positionY = blockTop - verticalPadding - nodeHeight;
|
||||||
|
|
||||||
|
var textWidth = System.Math.Abs((int)nameplateObject.TextW);
|
||||||
|
if (textWidth <= 0)
|
||||||
|
{
|
||||||
|
textWidth = GetScaledTextWidth(nameText);
|
||||||
|
if (textWidth <= 0)
|
||||||
|
textWidth = nodeWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textWidth > 0)
|
||||||
|
{
|
||||||
|
_cachedNameplateTextWidths[nameplateIndex] = textWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
||||||
|
var hasValidOffset = true;
|
||||||
|
|
||||||
|
if (System.Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0)
|
||||||
|
{
|
||||||
|
_cachedNameplateTextOffsets[nameplateIndex] = textOffset;
|
||||||
|
}
|
||||||
|
else if (_cachedNameplateTextOffsets[nameplateIndex] != int.MinValue)
|
||||||
|
{
|
||||||
|
textOffset = _cachedNameplateTextOffsets[nameplateIndex];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hasValidOffset = false;
|
||||||
|
}
|
||||||
|
int positionX;
|
||||||
|
|
||||||
|
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
|
||||||
|
{
|
||||||
|
var nameplateWidth = (int)nameContainer->Width;
|
||||||
|
|
||||||
|
if (!config.LightfinderLabelUseIcon)
|
||||||
|
{
|
||||||
|
pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize;
|
||||||
|
pNode->AtkResNode.Width = 0;
|
||||||
|
pNode->SetText(labelContent);
|
||||||
|
|
||||||
|
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||||
|
if (nodeWidth <= 0)
|
||||||
|
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||||
|
|
||||||
|
if (nodeWidth > nameplateWidth)
|
||||||
|
nodeWidth = nameplateWidth;
|
||||||
|
|
||||||
|
pNode->AtkResNode.Width = (ushort)nodeWidth;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pNode->TextFlags |= TextFlags.AutoAdjustNodeSize;
|
||||||
|
pNode->AtkResNode.Width = 0;
|
||||||
|
pNode->SetText(labelContent);
|
||||||
|
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
int leftPos = nameplateWidth / 8;
|
||||||
|
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
|
||||||
|
int centrePos = (nameplateWidth - nodeWidth) / 2;
|
||||||
|
int staticMargin = 24;
|
||||||
|
int calcMargin = (int)(nameplateWidth * 0.08f);
|
||||||
|
|
||||||
|
switch (config.LabelAlignment)
|
||||||
|
{
|
||||||
|
case LabelAlignment.Left:
|
||||||
|
positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos;
|
||||||
|
alignment = AlignmentType.BottomLeft;
|
||||||
|
break;
|
||||||
|
case LabelAlignment.Right:
|
||||||
|
positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin;
|
||||||
|
alignment = AlignmentType.BottomRight;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin;
|
||||||
|
alignment = AlignmentType.Bottom;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
positionX = 58 + config.LightfinderLabelOffsetX;
|
||||||
|
alignment = AlignmentType.Bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
positionY += config.LightfinderLabelOffsetY;
|
||||||
|
|
||||||
|
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
||||||
|
pNode->AtkResNode.SetUseDepthBasedPriority(true);
|
||||||
|
|
||||||
|
pNode->AtkResNode.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);
|
||||||
|
|
||||||
|
|
||||||
|
if(!config.LightfinderLabelUseIcon)
|
||||||
|
{
|
||||||
|
pNode->AlignmentType = AlignmentType.Bottom;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pNode->AlignmentType = alignment;
|
||||||
|
}
|
||||||
|
pNode->AtkResNode.SetPositionShort(
|
||||||
|
(short)System.Math.Clamp(positionX, short.MinValue, short.MaxValue),
|
||||||
|
(short)System.Math.Clamp(positionY, short.MinValue, short.MaxValue)
|
||||||
|
);
|
||||||
|
var computedLineSpacing = (int)System.Math.Round(24 * scaleMultiplier);
|
||||||
|
pNode->LineSpacing = (byte)System.Math.Clamp(computedLineSpacing, 0, byte.MaxValue);
|
||||||
|
pNode->CharSpacing = 1;
|
||||||
|
pNode->TextFlags = config.LightfinderLabelUseIcon
|
||||||
|
? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize
|
||||||
|
: TextFlags.Edge | TextFlags.Glare;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe int GetScaledTextHeight(AtkTextNode* node)
|
||||||
|
{
|
||||||
|
if (node == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var resNode = &node->AtkResNode;
|
||||||
|
var rawHeight = (int)resNode->GetHeight();
|
||||||
|
if (rawHeight <= 0 && node->LineSpacing > 0)
|
||||||
|
rawHeight = node->LineSpacing;
|
||||||
|
if (rawHeight <= 0)
|
||||||
|
rawHeight = AtkNodeHelpers.DefaultTextNodeHeight;
|
||||||
|
|
||||||
|
var scale = resNode->ScaleY;
|
||||||
|
if (scale <= 0f)
|
||||||
|
scale = 1f;
|
||||||
|
|
||||||
|
var computed = (int)System.Math.Round(rawHeight * scale);
|
||||||
|
return System.Math.Max(1, computed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe int GetScaledTextWidth(AtkTextNode* node)
|
||||||
|
{
|
||||||
|
if (node == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var resNode = &node->AtkResNode;
|
||||||
|
var rawWidth = (int)resNode->GetWidth();
|
||||||
|
if (rawWidth <= 0)
|
||||||
|
rawWidth = AtkNodeHelpers.DefaultTextNodeWidth;
|
||||||
|
|
||||||
|
var scale = resNode->ScaleX;
|
||||||
|
if (scale <= 0f)
|
||||||
|
scale = 1f;
|
||||||
|
|
||||||
|
var computed = (int)System.Math.Round(rawWidth * scale);
|
||||||
|
return System.Math.Max(1, computed);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string NormalizeIconGlyph(string? rawInput)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(rawInput))
|
||||||
|
return DefaultIconGlyph;
|
||||||
|
|
||||||
|
var trimmed = rawInput.Trim();
|
||||||
|
|
||||||
|
if (Enum.TryParse<SeIconChar>(trimmed, true, out var iconEnum))
|
||||||
|
return SeIconCharExtensions.ToIconString(iconEnum);
|
||||||
|
|
||||||
|
var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? trimmed[2..]
|
||||||
|
: trimmed;
|
||||||
|
|
||||||
|
if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue))
|
||||||
|
return char.ConvertFromUtf32(hexValue);
|
||||||
|
|
||||||
|
var enumerator = trimmed.EnumerateRunes();
|
||||||
|
if (enumerator.MoveNext())
|
||||||
|
return enumerator.Current.ToString();
|
||||||
|
|
||||||
|
return DefaultIconGlyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string ToIconEditorString(string? rawInput)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeIconGlyph(rawInput);
|
||||||
|
var runeEnumerator = normalized.EnumerateRunes();
|
||||||
|
return runeEnumerator.MoveNext()
|
||||||
|
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
|
||||||
|
: DefaultIconGlyph;
|
||||||
|
}
|
||||||
|
private void HideNameplateTextNode(int i)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
private HashSet<ulong> VisibleUserIds => [.. _pairManager.GetOnlineUserPairs()
|
||||||
|
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||||
|
.Select(u => (ulong)u.PlayerCharacterId)];
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearNameplateCaches()
|
||||||
|
{
|
||||||
|
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
||||||
|
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
||||||
|
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
||||||
|
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.Enums;
|
using Dalamud.Game.ClientState.Objects.Enums;
|
||||||
using Dalamud.Game.Gui.NamePlate;
|
using Dalamud.Game.Gui.NamePlate;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
@@ -8,16 +8,17 @@ using LightlessSync.PlayerData.Pairs;
|
|||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.UI;
|
using LightlessSync.UI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
public class NameplateService : DisposableMediatorSubscriberBase
|
public class NameplateService : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<NameplateService> _logger;
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly IClientState _clientState;
|
private readonly IClientState _clientState;
|
||||||
private readonly INamePlateGui _namePlateGui;
|
private readonly INamePlateGui _namePlateGui;
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
|
|
||||||
|
|
||||||
public NameplateService(ILogger<NameplateService> logger,
|
public NameplateService(ILogger<NameplateService> logger,
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
INamePlateGui namePlateGui,
|
INamePlateGui namePlateGui,
|
||||||
@@ -25,25 +26,36 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
|||||||
PairManager pairManager,
|
PairManager pairManager,
|
||||||
LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
|
LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_namePlateGui = namePlateGui;
|
_namePlateGui = namePlateGui;
|
||||||
_clientState = clientState;
|
_clientState = clientState;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
|
|
||||||
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
|
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
|
||||||
_namePlateGui.RequestRedraw();
|
_namePlateGui.RequestRedraw();
|
||||||
Mediator.Subscribe<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw());
|
Mediator.Subscribe<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||||
{
|
{
|
||||||
if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen)) return;
|
if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen))
|
||||||
var visibleUsersIds = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue).Select(u => (ulong)u.PlayerCharacterId).ToHashSet();
|
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;
|
var colors = _configService.Current.NameplateColors;
|
||||||
|
|
||||||
foreach (var handler in handlers)
|
foreach (var handler in handlers)
|
||||||
{
|
{
|
||||||
var playerCharacter = handler.PlayerCharacter;
|
var playerCharacter = handler.PlayerCharacter;
|
||||||
if (playerCharacter == null) { continue; }
|
if (playerCharacter == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember);
|
var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember);
|
||||||
var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend);
|
var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend);
|
||||||
bool partyColorAllowed = (_configService.Current.overridePartyColor && isInParty);
|
bool partyColorAllowed = (_configService.Current.overridePartyColor && isInParty);
|
||||||
@@ -56,13 +68,25 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
|||||||
))
|
))
|
||||||
{
|
{
|
||||||
handler.NameParts.TextWrap = CreateTextWrap(colors);
|
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()
|
public void RequestRedraw()
|
||||||
{
|
{
|
||||||
|
|
||||||
_namePlateGui.RequestRedraw();
|
_namePlateGui.RequestRedraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,12 +104,11 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
|||||||
return (left.ToReadOnlySeString().ToDalamudString(), right.ToReadOnlySeString().ToDalamudString());
|
return (left.ToReadOnlySeString().ToDalamudString(), right.ToReadOnlySeString().ToDalamudString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
|
|
||||||
_namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate;
|
_namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate;
|
||||||
_namePlateGui.RequestRedraw();
|
_namePlateGui.RequestRedraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
220
LightlessSync/Services/PairProcessingLimiter.cs
Normal file
220
LightlessSync/Services/PairProcessingLimiter.cs
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
|
public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private const int HardLimit = 32;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
|
private readonly object _limitLock = new();
|
||||||
|
private readonly SemaphoreSlim _semaphore;
|
||||||
|
private int _currentLimit;
|
||||||
|
private int _pendingReductions;
|
||||||
|
private int _waiting;
|
||||||
|
private int _inFlight;
|
||||||
|
|
||||||
|
public PairProcessingLimiter(ILogger<PairProcessingLimiter> logger, LightlessMediator mediator, LightlessConfigService configService)
|
||||||
|
: base(logger, mediator)
|
||||||
|
{
|
||||||
|
_configService = configService;
|
||||||
|
_currentLimit = CalculateLimit();
|
||||||
|
var initialCount = _configService.Current.EnablePairProcessingLimiter ? _currentLimit : HardLimit;
|
||||||
|
_semaphore = new SemaphoreSlim(initialCount, HardLimit);
|
||||||
|
|
||||||
|
Mediator.Subscribe<PairProcessingLimitChangedMessage>(this, _ => UpdateSemaphoreLimit());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<IAsyncDisposable> AcquireAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return WaitInternalAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PairProcessingLimiterSnapshot GetSnapshot()
|
||||||
|
{
|
||||||
|
lock (_limitLock)
|
||||||
|
{
|
||||||
|
var enabled = IsEnabled;
|
||||||
|
var limit = enabled ? _currentLimit : CalculateLimit();
|
||||||
|
var waiting = Math.Max(0, Volatile.Read(ref _waiting));
|
||||||
|
var inFlight = Math.Max(0, Volatile.Read(ref _inFlight));
|
||||||
|
return new PairProcessingLimiterSnapshot(enabled, limit, inFlight, waiting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsEnabled => _configService.Current.EnablePairProcessingLimiter;
|
||||||
|
|
||||||
|
private async ValueTask<IAsyncDisposable> WaitInternalAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
if (!IsEnabled)
|
||||||
|
{
|
||||||
|
return NoopReleaser.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _waiting);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _semaphore.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Interlocked.Decrement(ref _waiting);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Decrement(ref _waiting);
|
||||||
|
|
||||||
|
if (!IsEnabled)
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
return NoopReleaser.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _inFlight);
|
||||||
|
return new Releaser(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSemaphoreLimit()
|
||||||
|
{
|
||||||
|
lock (_limitLock)
|
||||||
|
{
|
||||||
|
var enabled = IsEnabled;
|
||||||
|
var desiredLimit = CalculateLimit();
|
||||||
|
|
||||||
|
if (!enabled)
|
||||||
|
{
|
||||||
|
var releaseAmount = HardLimit - _semaphore.CurrentCount;
|
||||||
|
if (releaseAmount > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_semaphore.Release(releaseAmount);
|
||||||
|
}
|
||||||
|
catch (SemaphoreFullException)
|
||||||
|
{
|
||||||
|
// ignore, already at max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentLimit = desiredLimit;
|
||||||
|
_pendingReductions = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (desiredLimit == _currentLimit)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (desiredLimit > _currentLimit)
|
||||||
|
{
|
||||||
|
var increment = desiredLimit - _currentLimit;
|
||||||
|
var allowed = Math.Min(increment, HardLimit - _semaphore.CurrentCount);
|
||||||
|
if (allowed > 0)
|
||||||
|
{
|
||||||
|
_semaphore.Release(allowed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var decrement = _currentLimit - desiredLimit;
|
||||||
|
var removed = 0;
|
||||||
|
while (removed < decrement && _semaphore.Wait(0))
|
||||||
|
{
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remaining = decrement - removed;
|
||||||
|
if (remaining > 0)
|
||||||
|
{
|
||||||
|
_pendingReductions += remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentLimit = desiredLimit;
|
||||||
|
Logger.LogDebug("Pair processing concurrency updated to {limit} (pending reductions: {pending})", _currentLimit, _pendingReductions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int CalculateLimit()
|
||||||
|
{
|
||||||
|
var configured = _configService.Current.MaxConcurrentPairApplications;
|
||||||
|
return Math.Clamp(configured, 1, HardLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReleaseOne()
|
||||||
|
{
|
||||||
|
var inFlight = Interlocked.Decrement(ref _inFlight);
|
||||||
|
if (inFlight < 0)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _inFlight, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsEnabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_limitLock)
|
||||||
|
{
|
||||||
|
if (_pendingReductions > 0)
|
||||||
|
{
|
||||||
|
_pendingReductions--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
if (!disposing)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_semaphore.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Releaser : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private PairProcessingLimiter? _owner;
|
||||||
|
|
||||||
|
public Releaser(PairProcessingLimiter owner)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
var owner = Interlocked.Exchange(ref _owner, null);
|
||||||
|
owner?.ReleaseOne();
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NoopReleaser : IAsyncDisposable
|
||||||
|
{
|
||||||
|
public static readonly NoopReleaser Instance = new();
|
||||||
|
|
||||||
|
private NoopReleaser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct PairProcessingLimiterSnapshot(bool IsEnabled, int Limit, int InFlight, int Waiting)
|
||||||
|
{
|
||||||
|
public int Remaining => Math.Max(0, Limit - InFlight);
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
@@ -299,7 +299,7 @@ public class ServerConfigurationManager
|
|||||||
|
|
||||||
internal void AddPairTag(string tag)
|
internal void AddPairTag(string tag)
|
||||||
{
|
{
|
||||||
if (tag.Length > _maxCharactersFolder)
|
if (tag.Length <= _maxCharactersFolder)
|
||||||
{
|
{
|
||||||
CurrentPairTagStorage().ServerAvailablePairTags.Add(tag);
|
CurrentPairTagStorage().ServerAvailablePairTags.Add(tag);
|
||||||
_pairTagConfig.Save();
|
_pairTagConfig.Save();
|
||||||
@@ -313,7 +313,7 @@ public class ServerConfigurationManager
|
|||||||
|
|
||||||
internal void AddSyncshellTag(string tag)
|
internal void AddSyncshellTag(string tag)
|
||||||
{
|
{
|
||||||
if (tag.Length > _maxCharactersFolder)
|
if (tag.Length <= _maxCharactersFolder)
|
||||||
{
|
{
|
||||||
CurrentSyncshellTagStorage().ServerAvailableSyncshellTags.Add(tag);
|
CurrentSyncshellTagStorage().ServerAvailableSyncshellTags.Add(tag);
|
||||||
_syncshellTagConfig.Save();
|
_syncshellTagConfig.Save();
|
||||||
|
|||||||
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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
_ = RefreshSyncshells();
|
||||||
|
}
|
||||||
|
|
||||||
|
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...");
|
||||||
|
_ = RefreshSyncshells();
|
||||||
|
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();
|
||||||
|
_ = RefreshSyncshells();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using System;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
@@ -8,7 +9,6 @@ using LightlessSync.API.Data.Extensions;
|
|||||||
using LightlessSync.API.Dto.Group;
|
using LightlessSync.API.Dto.Group;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.LightlessConfiguration.Configurations;
|
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
@@ -25,6 +25,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
private readonly CharacterAnalyzer _characterAnalyzer;
|
private readonly CharacterAnalyzer _characterAnalyzer;
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
||||||
private readonly DrawEntityFactory _drawEntityFactory;
|
private readonly DrawEntityFactory _drawEntityFactory;
|
||||||
private readonly FileUploadManager _fileTransferManager;
|
private readonly FileUploadManager _fileTransferManager;
|
||||||
@@ -51,25 +53,41 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
private readonly TopTabMenu _tabMenu;
|
private readonly TopTabMenu _tabMenu;
|
||||||
private readonly TagHandler _tagHandler;
|
private readonly TagHandler _tagHandler;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
|
private readonly BroadcastService _broadcastService;
|
||||||
|
|
||||||
private List<IDrawFolder> _drawFolders;
|
private List<IDrawFolder> _drawFolders;
|
||||||
private Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>? _cachedAnalysis;
|
private Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>? _cachedAnalysis;
|
||||||
private Pair? _lastAddedUser;
|
private Pair? _lastAddedUser;
|
||||||
private string _lastAddedUserComment = string.Empty;
|
private string _lastAddedUserComment = string.Empty;
|
||||||
private Vector2 _lastPosition = Vector2.One;
|
private Vector2 _lastPosition = Vector2.One;
|
||||||
private Vector2 _lastSize = Vector2.One;
|
private Vector2 _lastSize = Vector2.One;
|
||||||
private int _secretKeyIdx = -1;
|
|
||||||
private bool _showModalForUserAddition;
|
private bool _showModalForUserAddition;
|
||||||
private float _transferPartHeight;
|
private float _transferPartHeight;
|
||||||
private bool _wasOpen;
|
private bool _wasOpen;
|
||||||
private float _windowContentWidth;
|
private float _windowContentWidth;
|
||||||
|
|
||||||
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, LightlessConfigService configService, ApiController apiController, PairManager pairManager,
|
public CompactUi(
|
||||||
ServerConfigurationManager serverManager, LightlessMediator mediator, FileUploadManager fileTransferManager,
|
ILogger<CompactUi> logger,
|
||||||
TagHandler tagHandler, DrawEntityFactory drawEntityFactory,
|
UiSharedService uiShared,
|
||||||
SelectTagForPairUi selectTagForPairUi, SelectPairForTagUi selectPairForTagUi, RenamePairTagUi renameTagUi,
|
LightlessConfigService configService,
|
||||||
SelectTagForSyncshellUi selectTagForSyncshellUi, SelectSyncshellForTagUi selectSyncshellForTagUi, RenameSyncshellTagUi renameSyncshellTagUi,
|
ApiController apiController,
|
||||||
PerformanceCollectorService performanceCollectorService, IpcManager ipcManager, CharacterAnalyzer characterAnalyzer, PlayerPerformanceConfigService playerPerformanceConfig)
|
PairManager pairManager,
|
||||||
: base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
|
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;
|
_uiSharedService = uiShared;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
@@ -86,7 +104,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
_selectPairsForGroupUi = selectPairForTagUi;
|
_selectPairsForGroupUi = selectPairForTagUi;
|
||||||
_renamePairTagUi = renameTagUi;
|
_renamePairTagUi = renameTagUi;
|
||||||
_ipcManager = ipcManager;
|
_ipcManager = ipcManager;
|
||||||
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService);
|
_broadcastService = broadcastService;
|
||||||
|
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService);
|
||||||
|
|
||||||
AllowPinning = true;
|
AllowPinning = true;
|
||||||
AllowClickthrough = false;
|
AllowClickthrough = false;
|
||||||
@@ -121,10 +140,10 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.Text("Open Lightless Event Viewer");
|
ImGui.Text("Open Lightless Event Viewer");
|
||||||
ImGui.EndTooltip();
|
ImGui.EndTooltip();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
_drawFolders = GetDrawFolders().ToList();
|
_drawFolders = [.. DrawFolders];
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
string dev = "Dev Build";
|
string dev = "Dev Build";
|
||||||
@@ -141,7 +160,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
|
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
|
||||||
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
|
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
|
||||||
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
|
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;
|
Flags |= ImGuiWindowFlags.NoDocking;
|
||||||
|
|
||||||
@@ -152,6 +171,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
};
|
};
|
||||||
_characterAnalyzer = characterAnalyzer;
|
_characterAnalyzer = characterAnalyzer;
|
||||||
_playerPerformanceConfig = playerPerformanceConfig;
|
_playerPerformanceConfig = playerPerformanceConfig;
|
||||||
|
_lightlessMediator = mediator;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
@@ -202,7 +222,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
using (ImRaii.PushId("header")) DrawUIDHeader();
|
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();
|
using (ImRaii.PushId("serverstatus")) DrawServerStatus();
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
@@ -357,7 +377,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
private void DrawTransfers()
|
private void DrawTransfers()
|
||||||
{
|
{
|
||||||
var currentUploads = _fileTransferManager.CurrentUploads.ToList();
|
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot();
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Upload);
|
_uiSharedService.IconText(FontAwesomeIcon.Upload);
|
||||||
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
||||||
@@ -367,10 +387,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
var totalUploads = currentUploads.Count;
|
var totalUploads = currentUploads.Count;
|
||||||
|
|
||||||
var doneUploads = currentUploads.Count(c => c.IsTransferred);
|
var doneUploads = currentUploads.Count(c => c.IsTransferred);
|
||||||
|
var activeUploads = currentUploads.Count(c => !c.IsTransferred);
|
||||||
|
var uploadSlotLimit = Math.Clamp(_configService.Current.ParallelUploads, 1, 8);
|
||||||
var totalUploaded = currentUploads.Sum(c => c.Transferred);
|
var totalUploaded = currentUploads.Sum(c => c.Transferred);
|
||||||
var totalToUpload = currentUploads.Sum(c => c.Total);
|
var totalToUpload = currentUploads.Sum(c => c.Total);
|
||||||
|
|
||||||
ImGui.TextUnformatted($"{doneUploads}/{totalUploads}");
|
ImGui.TextUnformatted($"{doneUploads}/{totalUploads} (slots {activeUploads}/{uploadSlotLimit})");
|
||||||
var uploadText = $"({UiSharedService.ByteToString(totalUploaded)}/{UiSharedService.ByteToString(totalToUpload)})";
|
var uploadText = $"({UiSharedService.ByteToString(totalUploaded)}/{UiSharedService.ByteToString(totalToUpload)})";
|
||||||
var textSize = ImGui.CalcTextSize(uploadText);
|
var textSize = ImGui.CalcTextSize(uploadText);
|
||||||
ImGui.SameLine(_windowContentWidth - textSize.X);
|
ImGui.SameLine(_windowContentWidth - textSize.X);
|
||||||
@@ -383,7 +405,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.TextUnformatted("No uploads in progress");
|
ImGui.TextUnformatted("No uploads in progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentDownloads = _currentDownloads.SelectMany(d => d.Value.Values).ToList();
|
var currentDownloads = BuildCurrentDownloadSnapshot();
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Download);
|
_uiSharedService.IconText(FontAwesomeIcon.Download);
|
||||||
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
||||||
@@ -410,54 +432,223 @@ 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()
|
private void DrawUIDHeader()
|
||||||
{
|
{
|
||||||
var uidText = GetUidText();
|
var uidText = GetUidText();
|
||||||
|
|
||||||
//Getting information of character and triangles threshold to show overlimit status in UID bar.
|
Vector4? vanityTextColor = null;
|
||||||
_cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone();
|
Vector4? vanityGlowColor = null;
|
||||||
var groupedfiles = _cachedAnalysis.First().Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal)
|
bool useVanityColors = false;
|
||||||
.OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
|
|
||||||
var actualTriCount = _cachedAnalysis.First().Value.Sum(f => f.Value.Triangles);
|
|
||||||
var isOverTriHold = actualTriCount > (_playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000);
|
|
||||||
|
|
||||||
using (_uiSharedService.UidFont.Push())
|
if (_configService.Current.useColoredUIDs && _apiController.HasVanity)
|
||||||
{
|
{
|
||||||
var uidTextSize = ImGui.CalcTextSize(uidText);
|
if (!string.IsNullOrWhiteSpace(_apiController.TextColorHex))
|
||||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (uidTextSize.X / 2));
|
{
|
||||||
ImGui.TextColored(GetUidColor(), uidText);
|
vanityTextColor = UIColors.HexToRgba(_apiController.TextColorHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_apiController.TextGlowColorHex))
|
||||||
|
{
|
||||||
|
vanityGlowColor = UIColors.HexToRgba(_apiController.TextGlowColorHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
useVanityColors = vanityTextColor is not null || vanityGlowColor is not null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupedfiles != 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)
|
||||||
{
|
{
|
||||||
//Checking of VRAM threshhold
|
float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f;
|
||||||
var actualVramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal)).Sum(f => f.OriginalSize);
|
var buttonSize = new Vector2(iconSize.X, uidTextSize.Y);
|
||||||
var isOverVRAMUsage = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < actualVramUsage;
|
|
||||||
|
|
||||||
if (isOverTriHold || isOverVRAMUsage)
|
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.SameLine();
|
ImGui.BeginTooltip();
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
|
|
||||||
string warningMessage = "";
|
|
||||||
if (isOverTriHold)
|
|
||||||
{
|
|
||||||
warningMessage += $"You exceed your own triangles threshold by " +
|
|
||||||
$"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles.";
|
|
||||||
warningMessage += Environment.NewLine;
|
|
||||||
|
|
||||||
}
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("PairBlue"));
|
||||||
if (isOverVRAMUsage)
|
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)
|
||||||
{
|
{
|
||||||
warningMessage += $"You exceed your own VRAM threshold by " +
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGreen"));
|
||||||
$"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}.";
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
var valueDict = firstEntry.Value;
|
||||||
|
if (valueDict != null && valueDict.Count > 0)
|
||||||
|
{
|
||||||
|
var groupedfiles = valueDict
|
||||||
|
.Select(v => v.Value)
|
||||||
|
.Where(v => v != null)
|
||||||
|
.GroupBy(f => f.FileType, StringComparer.Ordinal)
|
||||||
|
.OrderBy(k => k.Key, StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var actualTriCount = valueDict
|
||||||
|
.Select(v => v.Value)
|
||||||
|
.Where(v => v != null)
|
||||||
|
.Sum(f => f.Triangles);
|
||||||
|
|
||||||
|
if (groupedfiles != null)
|
||||||
|
{
|
||||||
|
//Checking of VRAM threshhold
|
||||||
|
var texGroup = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
|
||||||
|
var actualVramUsage = texGroup != null ? texGroup.Sum(f => f.OriginalSize) : 0L;
|
||||||
|
var isOverVRAMUsage = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < actualVramUsage;
|
||||||
|
var isOverTriHold = actualTriCount > (_playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000);
|
||||||
|
|
||||||
|
if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetCursorPosY(cursorY + 15f);
|
||||||
|
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
|
||||||
|
|
||||||
|
string warningMessage = "";
|
||||||
|
if (isOverTriHold)
|
||||||
|
{
|
||||||
|
warningMessage += $"You exceed your own triangles threshold by " +
|
||||||
|
$"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles.";
|
||||||
|
warningMessage += Environment.NewLine;
|
||||||
|
|
||||||
|
}
|
||||||
|
if (isOverVRAMUsage)
|
||||||
|
{
|
||||||
|
warningMessage += $"You exceed your own VRAM threshold by " +
|
||||||
|
$"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}.";
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip(warningMessage);
|
||||||
|
if (ImGui.IsItemClicked())
|
||||||
|
{
|
||||||
|
_lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip(warningMessage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_apiController.ServerState is ServerState.Connected)
|
if (_apiController.ServerState is ServerState.Connected)
|
||||||
{
|
{
|
||||||
if (ImGui.IsItemClicked())
|
if (headerItemClicked)
|
||||||
{
|
{
|
||||||
ImGui.SetClipboardText(_apiController.DisplayName);
|
ImGui.SetClipboardText(_apiController.DisplayName);
|
||||||
}
|
}
|
||||||
@@ -466,9 +657,22 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
var origTextSize = ImGui.CalcTextSize(_apiController.UID);
|
var origTextSize = ImGui.CalcTextSize(_apiController.UID);
|
||||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (origTextSize.X / 2));
|
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");
|
UiSharedService.AttachToolTip("Click to copy");
|
||||||
if (ImGui.IsItemClicked())
|
if (uidFooterClicked)
|
||||||
{
|
{
|
||||||
ImGui.SetClipboardText(_apiController.UID);
|
ImGui.SetClipboardText(_apiController.UID);
|
||||||
}
|
}
|
||||||
@@ -480,163 +684,166 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<IDrawFolder> GetDrawFolders()
|
private IEnumerable<IDrawFolder> DrawFolders
|
||||||
{
|
{
|
||||||
List<IDrawFolder> drawFolders = [];
|
get
|
||||||
|
{
|
||||||
|
var drawFolders = new List<IDrawFolder>();
|
||||||
|
var filter = _tabMenu.Filter;
|
||||||
|
|
||||||
var allPairs = _pairManager.PairsWithGroups
|
var allPairs = _pairManager.PairsWithGroups.ToDictionary(k => k.Key, k => k.Value);
|
||||||
.ToDictionary(k => k.Key, k => k.Value);
|
var filteredPairs = allPairs.Where(p => PassesFilter(p.Key, filter)).ToDictionary(k => k.Key, k => k.Value);
|
||||||
var filteredPairs = allPairs
|
|
||||||
.Where(p =>
|
//Filter of online/visible pairs
|
||||||
|
if (_configService.Current.ShowVisibleUsersSeparately)
|
||||||
{
|
{
|
||||||
if (_tabMenu.Filter.IsNullOrEmpty()) return true;
|
var allVisiblePairs = ImmutablePairList(allPairs.Where(p => FilterVisibleUsers(p.Key)));
|
||||||
return p.Key.UserData.AliasOrUID.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ||
|
var filteredVisiblePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterVisibleUsers(p.Key)));
|
||||||
(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)
|
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs));
|
||||||
=> (_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 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());
|
|
||||||
|
|
||||||
|
//Filter of not foldered syncshells
|
||||||
if (_configService.Current.ShowVisibleUsersSeparately)
|
var groupFolders = new List<IDrawFolder>();
|
||||||
{
|
|
||||||
var allVisiblePairs = ImmutablePairList(allPairs
|
|
||||||
.Where(FilterVisibleUsers));
|
|
||||||
var filteredVisiblePairs = BasicSortedDictionary(filteredPairs
|
|
||||||
.Where(FilterVisibleUsers));
|
|
||||||
|
|
||||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs));
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = [];
|
|
||||||
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
|
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);
|
groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs));
|
||||||
syncshellFolderTags.Add(_drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", 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
|
//Filter of grouped/foldered syncshells
|
||||||
.Where(FilterNotTaggedUsers));
|
foreach (var syncshellTag in _tagHandler.GetAllSyncshellTagsSorted())
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
var allOfflineSyncshellUsers = ImmutablePairList(allPairs
|
var syncshellFolderTags = new List<IDrawFolder>();
|
||||||
.Where(FilterOfflineSyncshellUsers));
|
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
|
||||||
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 =>
|
|
||||||
{
|
{
|
||||||
if (string.Equals(u.Key.UserData.UID, group.OwnerUID, StringComparison.Ordinal)) return 0;
|
if (_tagHandler.HasSyncshellTag(group.GID, syncshellTag))
|
||||||
if (group.GroupPairUserInfos.TryGetValue(u.Key.UserData.UID, out var info))
|
|
||||||
{
|
{
|
||||||
if (info.IsModerator()) return 1;
|
GetGroups(allPairs, filteredPairs, group,
|
||||||
if (info.IsPinned()) return 2;
|
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)
|
drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshellTag));
|
||||||
.ToDictionary(k => k.Key, k => k.Value);
|
}
|
||||||
|
|
||||||
|
//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()
|
private string GetServerError()
|
||||||
{
|
{
|
||||||
return _apiController.ServerState switch
|
return _apiController.ServerState switch
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ public class DrawGroupedGroupFolder : IDrawFolder
|
|||||||
|
|
||||||
public void Draw()
|
public void Draw()
|
||||||
{
|
{
|
||||||
if (!_groups.Any()) return;
|
|
||||||
|
|
||||||
string _id = "__folder_syncshells";
|
string _id = "__folder_syncshells";
|
||||||
if (_tag != "")
|
if (_tag != "")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using LightlessSync.API.Data.Extensions;
|
using LightlessSync.API.Data.Extensions;
|
||||||
@@ -12,6 +11,7 @@ using LightlessSync.Services;
|
|||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
using LightlessSync.UI.Handlers;
|
using LightlessSync.UI.Handlers;
|
||||||
|
using LightlessSync.Utils;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
|
|
||||||
namespace LightlessSync.UI.Components;
|
namespace LightlessSync.UI.Components;
|
||||||
@@ -295,6 +295,31 @@ public class DrawUserPair
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
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 Developer"
|
||||||
|
: "Official Lightless Moderator");
|
||||||
|
}
|
||||||
|
ImGui.EndTooltip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawName(float leftSide, float rightSide)
|
private void DrawName(float leftSide, float rightSide)
|
||||||
|
|||||||
@@ -547,73 +547,147 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString());
|
using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString());
|
||||||
if (tab.Success)
|
if (tab.Success)
|
||||||
{
|
{
|
||||||
var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal)
|
var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal).OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
|
||||||
.OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
|
|
||||||
|
|
||||||
ImGui.TextUnformatted("Files for " + kvp.Key);
|
ImGui.PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(1f, 1f));
|
||||||
ImGui.SameLine();
|
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1f, 1f));
|
||||||
ImGui.TextUnformatted(kvp.Value.Count.ToString());
|
|
||||||
ImGui.SameLine();
|
|
||||||
|
|
||||||
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
|
if (ImGui.BeginTable($"##fileStats_{kvp.Key}", 3,
|
||||||
|
ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingFixedFit))
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
|
ImGui.TableNextRow();
|
||||||
}
|
ImGui.TableNextColumn();
|
||||||
if (ImGui.IsItemHovered())
|
ImGui.TextUnformatted($"Files for {kvp.Key}");
|
||||||
{
|
ImGui.TableNextColumn();
|
||||||
string text = "";
|
ImGui.TextUnformatted(kvp.Value.Count.ToString());
|
||||||
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.SameLine();
|
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
|
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|
||||||
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
|
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
|
||||||
{
|
{
|
||||||
using var _ = ImRaii.PushIndent(10f);
|
var currentTriWarning = _playerPerformanceConfig.Current.TrisWarningThresholdThousands;
|
||||||
var currentVramWarning = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB;
|
|
||||||
ImGui.TextUnformatted($"Configured VRAM warning threshold: {currentVramWarning} MiB.");
|
ImGui.TableNextRow();
|
||||||
if (currentVramWarning * 1024 * 1024 < actualVramUsage)
|
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.ColorText(
|
||||||
$"{UiSharedService.ByteToString(actualVramUsage - (currentVramWarning * 1024 * 1024))}.",
|
$"You exceed your own threshold by {actualTriCount - (currentTriWarning * 1000)}",
|
||||||
UIColors.Get("LightlessYellow"));
|
UIColors.Get("LightlessYellow"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui.EndTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
var actualTriCount = kvp.Value.Sum(f => f.Value.Triangles);
|
ImGui.PopStyleVar(2);
|
||||||
ImGui.TextUnformatted($"{kvp.Key} modded model triangles: {actualTriCount}");
|
|
||||||
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
||||||
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
|
|
||||||
|
_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 filePaths = item.FilePaths;
|
||||||
var currentTriWarning = _playerPerformanceConfig.Current.TrisWarningThresholdThousands;
|
UiSharedService.ColorText("Local file path:", UIColors.Get("LightlessBlue"));
|
||||||
ImGui.TextUnformatted($"Configured triangle warning threshold: {currentTriWarning * 1000} triangles.");
|
ImGui.SameLine();
|
||||||
if (currentTriWarning * 1000 < actualTriCount)
|
UiSharedService.TextWrapped(filePaths[0]);
|
||||||
|
if (filePaths.Count > 1)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorText($"You exceed your own threshold by " +
|
ImGui.SameLine();
|
||||||
$"{actualTriCount - (currentTriWarning * 1000)} triangles.",
|
ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)");
|
||||||
UIColors.Get("LightlessYellow"));
|
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();
|
ImGui.Separator();
|
||||||
|
|
||||||
if (_selectedObjectTab != kvp.Key)
|
if (_selectedObjectTab != kvp.Key)
|
||||||
{
|
{
|
||||||
_selectedHash = string.Empty;
|
_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()
|
public override void OnOpen()
|
||||||
@@ -855,7 +894,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
var filePath = item.FilePaths[0];
|
var filePath = item.FilePaths[0];
|
||||||
bool toConvert = _texturesToConvert.ContainsKey(filePath);
|
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))
|
if (toConvert && !_texturesToConvert.ContainsKey(filePath))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using System;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
@@ -19,14 +20,16 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly FileUploadManager _fileTransferManager;
|
private readonly FileUploadManager _fileTransferManager;
|
||||||
private readonly UiSharedService _uiShared;
|
private readonly UiSharedService _uiShared;
|
||||||
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
|
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
|
||||||
|
|
||||||
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
|
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
|
||||||
FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService)
|
PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService)
|
||||||
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
|
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
|
||||||
{
|
{
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
_fileTransferManager = fileTransferManager;
|
_fileTransferManager = fileTransferManager;
|
||||||
_uiShared = uiShared;
|
_uiShared = uiShared;
|
||||||
|
|
||||||
@@ -73,11 +76,25 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (_configService.Current.ShowTransferWindow)
|
if (_configService.Current.ShowTransferWindow)
|
||||||
{
|
{
|
||||||
|
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();
|
||||||
|
if (limiterSnapshot.IsEnabled)
|
||||||
|
{
|
||||||
|
var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey;
|
||||||
|
var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}";
|
||||||
|
queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)";
|
||||||
|
UiSharedService.DrawOutlinedFont(queueText, queueColor, new Vector4(0, 0, 0, 255), 1);
|
||||||
|
ImGui.NewLine();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UiSharedService.DrawOutlinedFont("Pair apply limiter disabled", ImGuiColors.DalamudGrey, new Vector4(0, 0, 0, 255), 1);
|
||||||
|
ImGui.NewLine();
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_fileTransferManager.CurrentUploads.Any())
|
if (_fileTransferManager.IsUploading)
|
||||||
{
|
{
|
||||||
var currentUploads = _fileTransferManager.CurrentUploads.ToList();
|
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot();
|
||||||
var totalUploads = currentUploads.Count;
|
var totalUploads = currentUploads.Count;
|
||||||
|
|
||||||
var doneUploads = currentUploads.Count(c => c.IsTransferred);
|
var doneUploads = currentUploads.Count(c => c.IsTransferred);
|
||||||
@@ -169,7 +186,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
var dlProgressPercent = transferredBytes / (double)totalBytes;
|
var dlProgressPercent = transferredBytes / (double)totalBytes;
|
||||||
drawList.AddRectFilled(dlBarStart,
|
drawList.AddRectFilled(dlBarStart,
|
||||||
dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) },
|
dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) },
|
||||||
UiSharedService.Color(173, 138, 245, transparency), 1);
|
UiSharedService.Color(UIColors.Get("LightlessPurple")));
|
||||||
|
|
||||||
if (_configService.Current.TransferBarsShowText)
|
if (_configService.Current.TransferBarsShowText)
|
||||||
{
|
{
|
||||||
@@ -214,7 +231,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (_uiShared.EditTrackerPosition) return true;
|
if (_uiShared.EditTrackerPosition) return true;
|
||||||
if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return false;
|
if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return false;
|
||||||
if (!_currentDownloads.Any() && !_fileTransferManager.CurrentUploads.Any() && !_uploadingPlayers.Any()) return false;
|
if (!_currentDownloads.Any() && !_fileTransferManager.IsUploading && !_uploadingPlayers.Any()) return false;
|
||||||
if (!IsOpen) return false;
|
if (!IsOpen) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,18 @@ using Dalamud.Interface.Colors;
|
|||||||
using Dalamud.Interface.ImGuiFileDialog;
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
using Dalamud.Interface.Textures.TextureWraps;
|
using Dalamud.Interface.Textures.TextureWraps;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.UI.Style;
|
||||||
|
using LightlessSync.Utils;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace LightlessSync.UI;
|
namespace LightlessSync.UI;
|
||||||
|
|
||||||
@@ -30,6 +34,16 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
private bool _showFileDialogError = false;
|
private bool _showFileDialogError = false;
|
||||||
private bool _wasOpen;
|
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,
|
public EditProfileUi(ILogger<EditProfileUi> logger, LightlessMediator mediator,
|
||||||
ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager,
|
ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager,
|
||||||
LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService)
|
LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService)
|
||||||
@@ -38,8 +52,8 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
IsOpen = false;
|
IsOpen = false;
|
||||||
this.SizeConstraints = new()
|
this.SizeConstraints = new()
|
||||||
{
|
{
|
||||||
MinimumSize = new(768, 512),
|
MinimumSize = new(850, 640),
|
||||||
MaximumSize = new(768, 2000)
|
MaximumSize = new(850, 700)
|
||||||
};
|
};
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
@@ -57,172 +71,320 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
_pfpTextureWrap = null;
|
_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()
|
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));
|
var profile = _lightlessProfileManager.GetLightlessProfile(new UserData(_apiController.UID));
|
||||||
|
|
||||||
if (profile.IsFlagged)
|
if (ImGui.BeginTabBar("##EditProfileTabs"))
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed);
|
if (ImGui.BeginTabItem("Current Profile"))
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
_adjustedForScollBarsOnlineProfile = true;
|
_uiSharedService.MediumText("Current Profile (as saved on server)", UIColors.Get("LightlessPurple"));
|
||||||
}
|
ImGui.Dummy(new Vector2(5));
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
var nsfw = profile.IsNSFW;
|
if (profile.IsFlagged)
|
||||||
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 () =>
|
|
||||||
{
|
{
|
||||||
var fileContent = File.ReadAllBytes(file);
|
UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed);
|
||||||
using MemoryStream ms = new(fileContent);
|
return;
|
||||||
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
|
}
|
||||||
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
|
|
||||||
|
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;
|
_adjustedForScollBarsOnlineProfile = true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
using var image = Image.Load<Rgba32>(fileContent);
|
else
|
||||||
|
|
||||||
if (image.Width > 256 || image.Height > 256 || (fileContent.Length > 250 * 1024))
|
|
||||||
{
|
{
|
||||||
_showFileDialogError = true;
|
_adjustedForScollBarsOnlineProfile = false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
childFrame = childFrame with
|
||||||
|
{
|
||||||
|
X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0),
|
||||||
|
};
|
||||||
|
if (ImGui.BeginChildFrame(101, childFrame))
|
||||||
|
{
|
||||||
|
UiSharedService.TextWrapped(profile.Description);
|
||||||
|
}
|
||||||
|
ImGui.EndChildFrame();
|
||||||
|
}
|
||||||
|
|
||||||
_showFileDialogError = false;
|
var nsfw = profile.IsNSFW;
|
||||||
await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null))
|
ImGui.BeginDisabled();
|
||||||
.ConfigureAwait(false);
|
ImGui.Checkbox("Is NSFW", ref nsfw);
|
||||||
});
|
ImGui.EndDisabled();
|
||||||
});
|
|
||||||
}
|
|
||||||
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();
|
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
||||||
|
ImGui.EndTabItem();
|
||||||
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"))
|
if (ImGui.BeginTabItem("Profile Settings"))
|
||||||
{
|
{
|
||||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText));
|
_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)
|
protected override void Dispose(bool disposing)
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using LightlessSync.API.Dto.Group;
|
using LightlessSync.API.Dto.Group;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
|
using LightlessSync.UI.Style;
|
||||||
|
using LightlessSync.Utils;
|
||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace LightlessSync.UI.Handlers;
|
namespace LightlessSync.UI.Handlers;
|
||||||
|
|
||||||
@@ -22,6 +27,9 @@ public class IdDisplayHandler
|
|||||||
private bool _popupShown = false;
|
private bool _popupShown = false;
|
||||||
private DateTime? _popupTime;
|
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)
|
public IdDisplayHandler(LightlessMediator mediator, ServerConfigurationManager serverManager, LightlessConfigService lightlessConfigService)
|
||||||
{
|
{
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
@@ -89,11 +97,108 @@ public class IdDisplayHandler
|
|||||||
{
|
{
|
||||||
ImGui.SameLine(textPosX);
|
ImGui.SameLine(textPosX);
|
||||||
(bool textIsUid, string playerText) = GetPlayerText(pair);
|
(bool textIsUid, string playerText) = GetPlayerText(pair);
|
||||||
|
|
||||||
if (!string.Equals(_editEntry, pair.UserData.UID, StringComparison.Ordinal))
|
if (!string.Equals(_editEntry, pair.UserData.UID, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
ImGui.AlignTextToFramePadding();
|
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())
|
if (ImGui.IsItemHovered())
|
||||||
{
|
{
|
||||||
@@ -173,10 +278,12 @@ public class IdDisplayHandler
|
|||||||
{
|
{
|
||||||
_editEntry = string.Empty;
|
_editEntry = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel");
|
UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public (bool isGid, string text) GetGroupText(GroupFullInfoDto group)
|
public (bool isGid, string text) GetGroupText(GroupFullInfoDto group)
|
||||||
{
|
{
|
||||||
var textIsGid = true;
|
var textIsGid = true;
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
else
|
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. " +
|
"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.");
|
"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.");
|
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 +
|
"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.");
|
"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.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.AlignTextToFramePadding();
|
||||||
ImGui.TextUnformatted("Syncshell ID");
|
ImGui.TextUnformatted("Syncshell ID");
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
@@ -10,6 +11,7 @@ using LightlessSync.API.Routes;
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.LightlessConfiguration.Configurations;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
@@ -17,15 +19,18 @@ using LightlessSync.Services;
|
|||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
|
using LightlessSync.UtilsEnum.Enum;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using LightlessSync.WebAPI.Files;
|
using LightlessSync.WebAPI.Files;
|
||||||
using LightlessSync.WebAPI.Files.Models;
|
using LightlessSync.WebAPI.Files.Models;
|
||||||
using LightlessSync.WebAPI.SignalR.Utils;
|
using LightlessSync.WebAPI.SignalR.Utils;
|
||||||
using Microsoft.AspNetCore.Http.Connections;
|
using Microsoft.AspNetCore.Http.Connections;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
@@ -50,10 +55,12 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
private readonly PerformanceCollectorService _performanceCollector;
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||||
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly UiSharedService _uiShared;
|
private readonly UiSharedService _uiShared;
|
||||||
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
|
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
|
||||||
private readonly NameplateService _nameplateService;
|
private readonly NameplateService _nameplateService;
|
||||||
|
private readonly NameplateHandler _nameplateHandler;
|
||||||
private (int, int, FileCacheEntity) _currentProgress;
|
private (int, int, FileCacheEntity) _currentProgress;
|
||||||
private bool _deleteAccountPopupModalShown = false;
|
private bool _deleteAccountPopupModalShown = false;
|
||||||
private bool _deleteFilesPopupModalShown = false;
|
private bool _deleteFilesPopupModalShown = false;
|
||||||
@@ -63,6 +70,23 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
private bool _readClearCache = false;
|
private bool _readClearCache = false;
|
||||||
private int _selectedEntry = -1;
|
private int _selectedEntry = -1;
|
||||||
private string _uidToAddForIgnore = string.Empty;
|
private string _uidToAddForIgnore = string.Empty;
|
||||||
|
private string _lightfinderIconInput = string.Empty;
|
||||||
|
private bool _lightfinderIconInputInitialized = false;
|
||||||
|
private int _lightfinderIconPresetIndex = -1;
|
||||||
|
private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[]
|
||||||
|
{
|
||||||
|
("Link Marker", SeIconChar.LinkMarker),
|
||||||
|
("Hyadelyn", SeIconChar.Hyadelyn),
|
||||||
|
("Gil", SeIconChar.Gil),
|
||||||
|
("Quest Sync", SeIconChar.QuestSync),
|
||||||
|
("Glamoured", SeIconChar.Glamoured),
|
||||||
|
("Glamoured (Dyed)", SeIconChar.GlamouredDyed),
|
||||||
|
("Auto-Translate Open", SeIconChar.AutoTranslateOpen),
|
||||||
|
("Auto-Translate Close", SeIconChar.AutoTranslateClose),
|
||||||
|
("Boxed Star", SeIconChar.BoxedStar),
|
||||||
|
("Boxed Plus", SeIconChar.BoxedPlus)
|
||||||
|
};
|
||||||
|
|
||||||
private CancellationTokenSource? _validationCts;
|
private CancellationTokenSource? _validationCts;
|
||||||
private Task<List<FileCacheEntity>>? _validationTask;
|
private Task<List<FileCacheEntity>>? _validationTask;
|
||||||
private bool _wasOpen = false;
|
private bool _wasOpen = false;
|
||||||
@@ -72,6 +96,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
PairManager pairManager,
|
PairManager pairManager,
|
||||||
ServerConfigurationManager serverConfigurationManager,
|
ServerConfigurationManager serverConfigurationManager,
|
||||||
PlayerPerformanceConfigService playerPerformanceConfigService,
|
PlayerPerformanceConfigService playerPerformanceConfigService,
|
||||||
|
PairProcessingLimiter pairProcessingLimiter,
|
||||||
LightlessMediator mediator, PerformanceCollectorService performanceCollector,
|
LightlessMediator mediator, PerformanceCollectorService performanceCollector,
|
||||||
FileUploadManager fileTransferManager,
|
FileUploadManager fileTransferManager,
|
||||||
FileTransferOrchestrator fileTransferOrchestrator,
|
FileTransferOrchestrator fileTransferOrchestrator,
|
||||||
@@ -79,12 +104,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
FileCompactor fileCompactor, ApiController apiController,
|
FileCompactor fileCompactor, ApiController apiController,
|
||||||
IpcManager ipcManager, CacheMonitor cacheMonitor,
|
IpcManager ipcManager, CacheMonitor cacheMonitor,
|
||||||
DalamudUtilService dalamudUtilService, HttpClient httpClient,
|
DalamudUtilService dalamudUtilService, HttpClient httpClient,
|
||||||
NameplateService nameplateService) : base(logger, mediator, "Lightless Sync Settings", performanceCollector)
|
NameplateService nameplateService,
|
||||||
|
NameplateHandler nameplateHandler) : base(logger, mediator, "Lightless Sync Settings", performanceCollector)
|
||||||
{
|
{
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||||
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
_performanceCollector = performanceCollector;
|
_performanceCollector = performanceCollector;
|
||||||
_fileTransferManager = fileTransferManager;
|
_fileTransferManager = fileTransferManager;
|
||||||
_fileTransferOrchestrator = fileTransferOrchestrator;
|
_fileTransferOrchestrator = fileTransferOrchestrator;
|
||||||
@@ -97,6 +124,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
_uiShared = uiShared;
|
_uiShared = uiShared;
|
||||||
_nameplateService = nameplateService;
|
_nameplateService = nameplateService;
|
||||||
|
_nameplateHandler = nameplateHandler;
|
||||||
AllowClickthrough = false;
|
AllowClickthrough = false;
|
||||||
AllowPinning = true;
|
AllowPinning = true;
|
||||||
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
||||||
@@ -218,6 +246,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|
||||||
int maxParallelDownloads = _configService.Current.ParallelDownloads;
|
int maxParallelDownloads = _configService.Current.ParallelDownloads;
|
||||||
|
int maxParallelUploads = _configService.Current.ParallelUploads;
|
||||||
|
int maxPairApplications = _configService.Current.MaxConcurrentPairApplications;
|
||||||
|
bool limitPairApplications = _configService.Current.EnablePairProcessingLimiter;
|
||||||
bool useAlternativeUpload = _configService.Current.UseAlternativeFileUpload;
|
bool useAlternativeUpload = _configService.Current.UseAlternativeFileUpload;
|
||||||
int downloadSpeedLimit = _configService.Current.DownloadSpeedLimitInBytes;
|
int downloadSpeedLimit = _configService.Current.DownloadSpeedLimitInBytes;
|
||||||
|
|
||||||
@@ -254,7 +285,60 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_configService.Current.ParallelDownloads = maxParallelDownloads;
|
_configService.Current.ParallelDownloads = maxParallelDownloads;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
|
Mediator.Publish(new DownloadLimitChangedMessage());
|
||||||
}
|
}
|
||||||
|
_uiShared.DrawHelpText("Controls how many download slots can be active at once.");
|
||||||
|
|
||||||
|
if (ImGui.SliderInt("Maximum Parallel Uploads", ref maxParallelUploads, 1, 8))
|
||||||
|
{
|
||||||
|
_configService.Current.ParallelUploads = maxParallelUploads;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("Controls how many uploads can run at once.");
|
||||||
|
|
||||||
|
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
||||||
|
|
||||||
|
if (ImGui.Checkbox("Enable Pair Download Limiter", ref limitPairApplications))
|
||||||
|
{
|
||||||
|
_configService.Current.EnablePairProcessingLimiter = limitPairApplications;
|
||||||
|
_configService.Save();
|
||||||
|
Mediator.Publish(new PairProcessingLimitChangedMessage());
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("When enabled we stagger pair downloads to avoid large network and game lag caused by attempting to download everyone at once.");
|
||||||
|
|
||||||
|
var limiterDisabledScope = !limitPairApplications;
|
||||||
|
if (limiterDisabledScope)
|
||||||
|
{
|
||||||
|
ImGui.BeginDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.SliderInt("Maximum Concurrent Pair Downloads", ref maxPairApplications, 1, 6))
|
||||||
|
{
|
||||||
|
_configService.Current.MaxConcurrentPairApplications = maxPairApplications;
|
||||||
|
_configService.Save();
|
||||||
|
Mediator.Publish(new PairProcessingLimitChangedMessage());
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("How many pair downloads/applications can run simultaneously when the limit is on.");
|
||||||
|
|
||||||
|
if (limiterDisabledScope)
|
||||||
|
{
|
||||||
|
ImGui.EndDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();
|
||||||
|
if (limiterSnapshot.IsEnabled)
|
||||||
|
{
|
||||||
|
var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey;
|
||||||
|
var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}";
|
||||||
|
queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)";
|
||||||
|
ImGui.TextColored(queueColor, queueText);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextColored(ImGuiColors.DalamudGrey, "Pair apply limiter is disabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
||||||
|
|
||||||
if (ImGui.Checkbox("Use Alternative Upload Method", ref useAlternativeUpload))
|
if (ImGui.Checkbox("Use Alternative Upload Method", ref useAlternativeUpload))
|
||||||
{
|
{
|
||||||
@@ -409,25 +493,33 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (ApiController.ServerState is ServerState.Connected && ImGui.BeginTabItem("Transfers"))
|
if (ApiController.ServerState is ServerState.Connected && ImGui.BeginTabItem("Transfers"))
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted("Uploads");
|
var uploadsSnapshot = _fileTransferManager.GetCurrentUploadsSnapshot();
|
||||||
|
var activeUploads = uploadsSnapshot.Count(c => !c.IsTransferred);
|
||||||
|
var uploadSlotLimit = Math.Clamp(_configService.Current.ParallelUploads, 1, 8);
|
||||||
|
ImGui.TextUnformatted($"Uploads (slots {activeUploads}/{uploadSlotLimit})");
|
||||||
if (ImGui.BeginTable("UploadsTable", 3))
|
if (ImGui.BeginTable("UploadsTable", 3))
|
||||||
{
|
{
|
||||||
ImGui.TableSetupColumn("File");
|
ImGui.TableSetupColumn("File");
|
||||||
ImGui.TableSetupColumn("Uploaded");
|
ImGui.TableSetupColumn("Uploaded");
|
||||||
ImGui.TableSetupColumn("Size");
|
ImGui.TableSetupColumn("Size");
|
||||||
ImGui.TableHeadersRow();
|
ImGui.TableHeadersRow();
|
||||||
foreach (var transfer in _fileTransferManager.CurrentUploads.ToArray())
|
foreach (var transfer in uploadsSnapshot)
|
||||||
{
|
{
|
||||||
var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total));
|
var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total));
|
||||||
var col = ImRaii.PushColor(ImGuiCol.Text, color);
|
using var col = ImRaii.PushColor(ImGuiCol.Text, color);
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.TextUnformatted(transfer.Hash);
|
if (transfer is UploadFileTransfer uploadTransfer)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(uploadTransfer.LocalFile);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(transfer.Hash);
|
||||||
|
}
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Transferred));
|
ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Transferred));
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Total));
|
ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Total));
|
||||||
col.Dispose();
|
|
||||||
ImGui.TableNextRow();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.EndTable();
|
ImGui.EndTable();
|
||||||
@@ -633,25 +725,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_lastTab = "FileCache";
|
_lastTab = "FileCache";
|
||||||
|
|
||||||
_uiShared.UnderlinedBigText("Export MCDF", UIColors.Get("LightlessBlue"));
|
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(10);
|
|
||||||
|
|
||||||
UiSharedService.ColorTextWrapped("Exporting MCDF has moved.", UIColors.Get("LightlessYellow"));
|
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
|
||||||
UiSharedService.TextWrapped("It is now found in the Main UI under \"Your User Menu\" (");
|
|
||||||
ImGui.SameLine();
|
|
||||||
_uiShared.IconText(FontAwesomeIcon.UserCog);
|
|
||||||
ImGui.SameLine();
|
|
||||||
UiSharedService.TextWrapped(") -> \"Character Data Hub\".");
|
|
||||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Running, "Open Lightless Character Data Hub"))
|
|
||||||
{
|
|
||||||
Mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi)));
|
|
||||||
}
|
|
||||||
UiSharedService.TextWrapped("Note: this entry will be removed in the near future. Please use the Main UI to open the Character Data Hub.");
|
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
|
||||||
ImGui.Separator();
|
|
||||||
|
|
||||||
_uiShared.UnderlinedBigText("Storage", UIColors.Get("LightlessBlue"));
|
_uiShared.UnderlinedBigText("Storage", UIColors.Get("LightlessBlue"));
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|
||||||
@@ -832,7 +905,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
foreach (var file in Directory.GetFiles(_configService.Current.CacheFolder))
|
foreach (var file in Directory.GetFiles(_configService.Current.CacheFolder))
|
||||||
{
|
{
|
||||||
File.Delete(file);
|
try
|
||||||
|
{
|
||||||
|
File.Delete(file);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, $"Could not delete file {file} because it is in use.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -933,12 +1013,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
var showUidInDtrTooltip = _configService.Current.ShowUidInDtrTooltip;
|
var showUidInDtrTooltip = _configService.Current.ShowUidInDtrTooltip;
|
||||||
var preferNoteInDtrTooltip = _configService.Current.PreferNoteInDtrTooltip;
|
var preferNoteInDtrTooltip = _configService.Current.PreferNoteInDtrTooltip;
|
||||||
var useColorsInDtr = _configService.Current.UseColorsInDtr;
|
var useColorsInDtr = _configService.Current.UseColorsInDtr;
|
||||||
|
var useLightlessRedesign = _configService.Current.UseLightlessRedesign;
|
||||||
var dtrColorsDefault = _configService.Current.DtrColorsDefault;
|
var dtrColorsDefault = _configService.Current.DtrColorsDefault;
|
||||||
var dtrColorsNotConnected = _configService.Current.DtrColorsNotConnected;
|
var dtrColorsNotConnected = _configService.Current.DtrColorsNotConnected;
|
||||||
var dtrColorsPairsInRange = _configService.Current.DtrColorsPairsInRange;
|
var dtrColorsPairsInRange = _configService.Current.DtrColorsPairsInRange;
|
||||||
var preferNotesInsteadOfName = _configService.Current.PreferNotesOverNamesForVisible;
|
var preferNotesInsteadOfName = _configService.Current.PreferNotesOverNamesForVisible;
|
||||||
var useFocusTarget = _configService.Current.UseFocusTarget;
|
var useFocusTarget = _configService.Current.UseFocusTarget;
|
||||||
var groupUpSyncshells = _configService.Current.GroupUpSyncshells;
|
var groupUpSyncshells = _configService.Current.GroupUpSyncshells;
|
||||||
|
var groupedSyncshells = _configService.Current.ShowGroupedSyncshellsInAll;
|
||||||
var groupInVisible = _configService.Current.ShowSyncshellUsersInVisible;
|
var groupInVisible = _configService.Current.ShowSyncshellUsersInVisible;
|
||||||
var syncshellOfflineSeparate = _configService.Current.ShowSyncshellOfflineUsersSeparately;
|
var syncshellOfflineSeparate = _configService.Current.ShowSyncshellOfflineUsersSeparately;
|
||||||
|
|
||||||
@@ -950,7 +1032,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_configService.Current.EnableRightClickMenus = enableRightClickMenu;
|
_configService.Current.EnableRightClickMenus = enableRightClickMenu;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
}
|
}
|
||||||
_uiShared.DrawHelpText("This will add Lightless related right click menu entries in the game UI on paired players.");
|
_uiShared.DrawHelpText("This will add all Lightless related right click menu entries in the game UI.");
|
||||||
|
|
||||||
if (ImGui.Checkbox("Display status and visible pair count in Server Info Bar", ref enableDtrEntry))
|
if (ImGui.Checkbox("Display status and visible pair count in Server Info Bar", ref enableDtrEntry))
|
||||||
{
|
{
|
||||||
@@ -982,43 +1064,329 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
|
if (_uiShared.MediumTreeNode("Lightfinder", UIColors.Get("LightlessPurple")))
|
||||||
|
{
|
||||||
|
var autoAlign = _configService.Current.LightfinderAutoAlign;
|
||||||
|
var offsetX = (int)_configService.Current.LightfinderLabelOffsetX;
|
||||||
|
var offsetY = (int)_configService.Current.LightfinderLabelOffsetY;
|
||||||
|
var labelScale = _configService.Current.LightfinderLabelScale;
|
||||||
|
|
||||||
|
ImGui.TextUnformatted("Alignment");
|
||||||
|
ImGui.BeginDisabled(autoAlign);
|
||||||
|
if (ImGui.SliderInt("Label Offset X", ref offsetX, -200, 200))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelOffsetX = (short)offsetX;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelOffsetX = 0;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Right click to reset to default.");
|
||||||
|
ImGui.EndDisabled();
|
||||||
|
_uiShared.DrawHelpText("Moves the Lightfinder label horizontally on player nameplates.\nUnavailable when automatic alignment is enabled.");
|
||||||
|
|
||||||
|
|
||||||
|
if (ImGui.SliderInt("Label Offset Y", ref offsetY, -200, 200))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelOffsetY = (short)offsetY;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelOffsetY = 0;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Right click to reset to default.");
|
||||||
|
_uiShared.DrawHelpText("Moves the Lightfinder label vertically on player nameplates.");
|
||||||
|
|
||||||
|
if (ImGui.SliderFloat("Label Size", ref labelScale, 0.5f, 2.0f, "%.2fx"))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelScale = labelScale;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelScale = 1.0f;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Right click to reset to default.");
|
||||||
|
_uiShared.DrawHelpText("Adjusts the Lightfinder label size for both text and icon modes.");
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(8));
|
||||||
|
|
||||||
|
if (ImGui.Checkbox("Automatically align with nameplate", ref autoAlign))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderAutoAlign = autoAlign;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("Automatically position the label relative to the in-game nameplate. Turn off to rely entirely on manual offsets.");
|
||||||
|
|
||||||
|
if (autoAlign)
|
||||||
|
{
|
||||||
|
var alignmentOption = _configService.Current.LabelAlignment;
|
||||||
|
var alignmentLabel = alignmentOption switch
|
||||||
|
{
|
||||||
|
LabelAlignment.Left => "Left",
|
||||||
|
LabelAlignment.Right => "Right",
|
||||||
|
_ => "Center",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ImGui.BeginCombo("Horizontal Alignment", alignmentLabel))
|
||||||
|
{
|
||||||
|
foreach (LabelAlignment option in Enum.GetValues<LabelAlignment>())
|
||||||
|
{
|
||||||
|
var optionLabel = option switch
|
||||||
|
{
|
||||||
|
LabelAlignment.Left => "Left",
|
||||||
|
LabelAlignment.Right => "Right",
|
||||||
|
_ => "Center",
|
||||||
|
};
|
||||||
|
var selected = option == alignmentOption;
|
||||||
|
if (ImGui.Selectable(optionLabel, selected))
|
||||||
|
{
|
||||||
|
_configService.Current.LabelAlignment = option;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected)
|
||||||
|
ImGui.SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndCombo();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
|
||||||
|
|
||||||
|
ImGui.TextUnformatted("Visibility");
|
||||||
|
var showOwn = _configService.Current.LightfinderLabelShowOwn;
|
||||||
|
if (ImGui.Checkbox("Show your own Lightfinder label", ref showOwn))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelShowOwn = showOwn;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("Toggles your own Lightfinder label.");
|
||||||
|
|
||||||
|
var showPaired = _configService.Current.LightfinderLabelShowPaired;
|
||||||
|
if (ImGui.Checkbox("Show paired player(s) Lightfinder label", ref showPaired))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelShowPaired = showPaired;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("Toggles paired player(s) Lightfinder label.");
|
||||||
|
|
||||||
|
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
|
||||||
|
|
||||||
|
ImGui.TextUnformatted("Label");
|
||||||
|
var useIcon = _configService.Current.LightfinderLabelUseIcon;
|
||||||
|
if (ImGui.Checkbox("Show icon instead of text", ref useIcon))
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelUseIcon = useIcon;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.ClearNameplateCaches();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
|
||||||
|
if (useIcon)
|
||||||
|
{
|
||||||
|
RefreshLightfinderIconState();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_lightfinderIconInputInitialized = false;
|
||||||
|
_lightfinderIconPresetIndex = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("Switch between the Lightfinder text label and an icon on nameplates.");
|
||||||
|
|
||||||
|
if (useIcon)
|
||||||
|
{
|
||||||
|
if (!_lightfinderIconInputInitialized)
|
||||||
|
{
|
||||||
|
RefreshLightfinderIconState();
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentPresetLabel = _lightfinderIconPresetIndex >= 0
|
||||||
|
? $"{GetLightfinderPresetGlyph(_lightfinderIconPresetIndex)} {LightfinderIconPresets[_lightfinderIconPresetIndex].Label}"
|
||||||
|
: "Custom";
|
||||||
|
|
||||||
|
if (ImGui.BeginCombo("Preset Icon", currentPresetLabel))
|
||||||
|
{
|
||||||
|
for (int i = 0; i < LightfinderIconPresets.Length; i++)
|
||||||
|
{
|
||||||
|
var optionGlyph = GetLightfinderPresetGlyph(i);
|
||||||
|
var preview = $"{optionGlyph} {LightfinderIconPresets[i].Label}";
|
||||||
|
var selected = i == _lightfinderIconPresetIndex;
|
||||||
|
if (ImGui.Selectable(preview, selected))
|
||||||
|
{
|
||||||
|
ApplyLightfinderIcon(optionGlyph, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.Selectable("Custom", _lightfinderIconPresetIndex == -1))
|
||||||
|
{
|
||||||
|
_lightfinderIconPresetIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndCombo();
|
||||||
|
}
|
||||||
|
|
||||||
|
var editorBuffer = _lightfinderIconInput;
|
||||||
|
if (ImGui.InputText("Icon Glyph", ref editorBuffer, 16))
|
||||||
|
{
|
||||||
|
_lightfinderIconInput = editorBuffer;
|
||||||
|
_lightfinderIconPresetIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.Button("Apply Icon"))
|
||||||
|
{
|
||||||
|
var normalized = NameplateHandler.NormalizeIconGlyph(_lightfinderIconInput);
|
||||||
|
ApplyLightfinderIcon(normalized, _lightfinderIconPresetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Reset Icon"))
|
||||||
|
{
|
||||||
|
var defaultGlyph = NameplateHandler.NormalizeIconGlyph(null);
|
||||||
|
var defaultIndex = -1;
|
||||||
|
for (int i = 0; i < LightfinderIconPresets.Length; i++)
|
||||||
|
{
|
||||||
|
if (string.Equals(GetLightfinderPresetGlyph(i), defaultGlyph, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
defaultIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultIndex < 0)
|
||||||
|
{
|
||||||
|
defaultIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyLightfinderIcon(GetLightfinderPresetGlyph(defaultIndex), defaultIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
var previewGlyph = NameplateHandler.NormalizeIconGlyph(_lightfinderIconInput);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.Text($"Preview: {previewGlyph}");
|
||||||
|
_uiShared.DrawHelpText("Enter a hex code (e.g. E0BB), pick a preset, or paste an icon character directly.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_lightfinderIconInputInitialized = false;
|
||||||
|
_lightfinderIconPresetIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
||||||
|
ImGui.TreePop();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
|
||||||
if (_uiShared.MediumTreeNode("Colors", UIColors.Get("LightlessPurple")))
|
if (_uiShared.MediumTreeNode("Colors", UIColors.Get("LightlessPurple")))
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted("UI Theme Colors");
|
ImGui.TextUnformatted("UI Theme Colors");
|
||||||
|
|
||||||
var colorNames = new[]
|
var colorNames = new[]
|
||||||
{
|
{
|
||||||
("LightlessPurple", "Lightless Purple", "Primary colors"),
|
("LightlessPurple", "Primary Purple", "Section titles and dividers"),
|
||||||
("LightlessBlue", "Lightless Blue", "Secondary colors"),
|
("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"),
|
||||||
("LightlessYellow", "Lightless Yellow", "Warning colors"),
|
("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"),
|
||||||
("PairBlue", "Pair Blue", "Pair UI elements"),
|
("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"),
|
||||||
("DimRed", "Dim Red", "Error and offline")
|
|
||||||
|
("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")
|
||||||
};
|
};
|
||||||
|
if (ImGui.BeginTable("##ColorTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
|
||||||
foreach (var (colorKey, displayName, description) in colorNames)
|
|
||||||
{
|
{
|
||||||
var currentColor = UIColors.Get(colorKey);
|
ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthFixed);
|
||||||
var colorToEdit = currentColor;
|
ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch);
|
||||||
|
ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40);
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
ImGui.AlignTextToFramePadding();
|
foreach (var (colorKey, displayName, description) in colorNames)
|
||||||
|
|
||||||
if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf))
|
|
||||||
{
|
{
|
||||||
UIColors.Set(colorKey, colorToEdit);
|
ImGui.TableNextRow();
|
||||||
}
|
|
||||||
|
// color column
|
||||||
ImGui.SameLine();
|
ImGui.TableSetColumnIndex(0);
|
||||||
ImGui.TextUnformatted($"{displayName} - {description}");
|
var currentColor = UIColors.Get(colorKey);
|
||||||
|
var colorToEdit = currentColor;
|
||||||
if (UIColors.IsCustom(colorKey))
|
if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf))
|
||||||
{
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Undo, $"Reset {colorKey}"))
|
|
||||||
{
|
{
|
||||||
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();
|
ImGui.Spacing();
|
||||||
@@ -1030,6 +1398,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
|
||||||
|
|
||||||
ImGui.TextUnformatted("Server Info Bar Colors");
|
ImGui.TextUnformatted("Server Info Bar Colors");
|
||||||
|
|
||||||
if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr))
|
if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr))
|
||||||
@@ -1064,12 +1434,16 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
|
||||||
|
|
||||||
ImGui.TextUnformatted("Nameplate Colors");
|
ImGui.TextUnformatted("Nameplate Colors");
|
||||||
|
|
||||||
var nameColorsEnabled = _configService.Current.IsNameplateColorsEnabled;
|
var nameColorsEnabled = _configService.Current.IsNameplateColorsEnabled;
|
||||||
var nameColors = _configService.Current.NameplateColors;
|
var nameColors = _configService.Current.NameplateColors;
|
||||||
var isFriendOverride = _configService.Current.overrideFriendColor;
|
var isFriendOverride = _configService.Current.overrideFriendColor;
|
||||||
var isPartyOverride = _configService.Current.overridePartyColor;
|
var isPartyOverride = _configService.Current.overridePartyColor;
|
||||||
|
var isFcTagOverride = _configService.Current.overrideFcTagColor;
|
||||||
|
|
||||||
if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled))
|
if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled))
|
||||||
{
|
{
|
||||||
@@ -1100,8 +1474,35 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_configService.Save();
|
_configService.Save();
|
||||||
_nameplateService.RequestRedraw();
|
_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);
|
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
||||||
ImGui.TreePop();
|
ImGui.TreePop();
|
||||||
}
|
}
|
||||||
@@ -1156,6 +1557,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
_uiShared.DrawHelpText("This will group up all Syncshells in a special 'All Syncshells' folder in the main UI.");
|
_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());
|
||||||
|
}
|
||||||
|
_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))
|
if (ImGui.Checkbox("Show player name for visible players", ref showNameInsteadOfNotes))
|
||||||
{
|
{
|
||||||
_configService.Current.ShowCharacterNameInsteadOfNotesForVisible = showNameInsteadOfNotes;
|
_configService.Current.ShowCharacterNameInsteadOfNotesForVisible = showNameInsteadOfNotes;
|
||||||
@@ -1380,16 +1789,32 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
bool autoPause = _playerPerformanceConfigService.Current.AutoPausePlayersExceedingThresholds;
|
bool autoPause = _playerPerformanceConfigService.Current.AutoPausePlayersExceedingThresholds;
|
||||||
bool autoPauseEveryone = _playerPerformanceConfigService.Current.AutoPausePlayersWithPreferredPermissionsExceedingThresholds;
|
bool autoPauseEveryone = _playerPerformanceConfigService.Current.AutoPausePlayersWithPreferredPermissionsExceedingThresholds;
|
||||||
bool autoPauseInDuty = _playerPerformanceConfigService.Current.PauseInInstanceDuty;
|
bool autoPauseInDuty = _playerPerformanceConfigService.Current.PauseInInstanceDuty;
|
||||||
|
bool autoPauseInCombat = _playerPerformanceConfigService.Current.PauseInCombat;
|
||||||
|
bool autoPauseWhilePerforming = _playerPerformanceConfigService.Current.PauseWhilePerforming;
|
||||||
|
|
||||||
if (_uiShared.MediumTreeNode("Auto Pause", UIColors.Get("LightlessPurple")))
|
if (_uiShared.MediumTreeNode("Auto Pause", UIColors.Get("LightlessPurple")))
|
||||||
{
|
{
|
||||||
|
if (ImGui.Checkbox("Auto pause sync while combat", ref autoPauseInCombat))
|
||||||
|
{
|
||||||
|
_playerPerformanceConfigService.Current.PauseInCombat = autoPauseInCombat;
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("AUTO-ENABLED: Your risk of crashing during a fight increases when this is disabled. For example: VFX mods Loading mid fight can cause a crash." + Environment.NewLine
|
||||||
|
+ UiSharedService.TooltipSeparator + "WARNING: DISABLE AT YOUR OWN RISK.");
|
||||||
|
if (ImGui.Checkbox("Auto pause sync while in Perfomance as Bard", ref autoPauseWhilePerforming))
|
||||||
|
{
|
||||||
|
_playerPerformanceConfigService.Current.PauseWhilePerforming = autoPauseWhilePerforming;
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("AUTO-ENABLED: Your risk of crashing during a performance increases when this is disabled. For example: Some mods can crash you mid performance" + Environment.NewLine
|
||||||
|
+ UiSharedService.TooltipSeparator + "WARNING: DISABLE AT YOUR OWN RISK.");
|
||||||
if (ImGui.Checkbox("Auto pause sync while in instances and duties", ref autoPauseInDuty))
|
if (ImGui.Checkbox("Auto pause sync while in instances and duties", ref autoPauseInDuty))
|
||||||
{
|
{
|
||||||
_playerPerformanceConfigService.Current.PauseInInstanceDuty = autoPauseInDuty;
|
_playerPerformanceConfigService.Current.PauseInInstanceDuty = autoPauseInDuty;
|
||||||
_playerPerformanceConfigService.Save();
|
_playerPerformanceConfigService.Save();
|
||||||
}
|
}
|
||||||
_uiShared.DrawHelpText("When enabled, it will automatically pause all players while you are in an instance, such as a dungeon or raid." + Environment.NewLine
|
_uiShared.DrawHelpText("When enabled, it will automatically pause all players while you are in an instance, such as a dungeon or raid." + Environment.NewLine
|
||||||
+ UiSharedService.TooltipSeparator + "Warning: You many have to leave the dungeon to resync with people again");
|
+ UiSharedService.TooltipSeparator + "Warning: You may have to leave the dungeon to resync with people again");
|
||||||
|
|
||||||
if (ImGui.Checkbox("Automatically pause players exceeding thresholds", ref autoPause))
|
if (ImGui.Checkbox("Automatically pause players exceeding thresholds", ref autoPause))
|
||||||
{
|
{
|
||||||
@@ -2138,6 +2563,39 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
return (true, failedConversions.Count != 0, sb.ToString());
|
return (true, failedConversions.Count != 0, sb.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetLightfinderPresetGlyph(int index)
|
||||||
|
{
|
||||||
|
return NameplateHandler.NormalizeIconGlyph(SeIconCharExtensions.ToIconString(LightfinderIconPresets[index].Icon));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshLightfinderIconState()
|
||||||
|
{
|
||||||
|
var normalized = NameplateHandler.NormalizeIconGlyph(_configService.Current.LightfinderLabelIconGlyph);
|
||||||
|
_lightfinderIconInput = NameplateHandler.ToIconEditorString(normalized);
|
||||||
|
_lightfinderIconInputInitialized = true;
|
||||||
|
|
||||||
|
_lightfinderIconPresetIndex = -1;
|
||||||
|
for (int i = 0; i < LightfinderIconPresets.Length; i++)
|
||||||
|
{
|
||||||
|
if (string.Equals(GetLightfinderPresetGlyph(i), normalized, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_lightfinderIconPresetIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyLightfinderIcon(string normalizedGlyph, int presetIndex)
|
||||||
|
{
|
||||||
|
_configService.Current.LightfinderLabelIconGlyph = normalizedGlyph;
|
||||||
|
_configService.Save();
|
||||||
|
_nameplateHandler.FlagRefresh();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
_lightfinderIconInput = NameplateHandler.ToIconEditorString(normalizedGlyph);
|
||||||
|
_lightfinderIconPresetIndex = presetIndex;
|
||||||
|
_lightfinderIconInputInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawSettingsContent()
|
private void DrawSettingsContent()
|
||||||
{
|
{
|
||||||
if (_apiController.ServerState is ServerState.Connected)
|
if (_apiController.ServerState is ServerState.Connected)
|
||||||
@@ -2215,4 +2673,4 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_wasOpen = IsOpen;
|
_wasOpen = IsOpen;
|
||||||
IsOpen = false;
|
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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -195,9 +195,9 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
using var table = ImRaii.Table("userList#" + GroupFullInfo.Group.GID, 3, tableFlags);
|
using var table = ImRaii.Table("userList#" + GroupFullInfo.Group.GID, 3, tableFlags);
|
||||||
if (table)
|
if (table)
|
||||||
{
|
{
|
||||||
ImGui.TableSetupColumn("Alias/UID/Note", ImGuiTableColumnFlags.None, 5);
|
ImGui.TableSetupColumn("Alias/UID/Note", ImGuiTableColumnFlags.None, 4);
|
||||||
ImGui.TableSetupColumn("Flags", ImGuiTableColumnFlags.None, 1);
|
ImGui.TableSetupColumn("Flags", ImGuiTableColumnFlags.None, 1);
|
||||||
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 2);
|
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 3);
|
||||||
ImGui.TableHeadersRow();
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
var groupedPairs = new Dictionary<Pair, GroupPairUserInfo?>(pairs.Select(p => new KeyValuePair<Pair, GroupPairUserInfo?>(p,
|
var groupedPairs = new Dictionary<Pair, GroupPairUserInfo?>(pairs.Select(p => new KeyValuePair<Pair, GroupPairUserInfo?>(p,
|
||||||
@@ -254,13 +254,32 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
ImGui.TableNextColumn(); // actions
|
ImGui.TableNextColumn(); // actions
|
||||||
if (_isOwner)
|
if (_isOwner)
|
||||||
{
|
{
|
||||||
if (_uiSharedService.IconButton(FontAwesomeIcon.UserShield))
|
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow")))
|
||||||
{
|
{
|
||||||
GroupPairUserInfo userInfo = pair.Value ?? GroupPairUserInfo.None;
|
using (ImRaii.Disabled(!UiSharedService.ShiftPressed()))
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Crown))
|
||||||
|
{
|
||||||
|
_ = _apiController.GroupChangeOwnership(new(GroupFullInfo.Group, pair.Key.UserData));
|
||||||
|
IsOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
userInfo.SetModerator(!userInfo.IsModerator());
|
}
|
||||||
|
UiSharedService.AttachToolTip("Hold SHIFT and click to transfer ownership of this Syncshell to "
|
||||||
|
+ (pair.Key.UserData.AliasOrUID) + Environment.NewLine + "WARNING: This action is irreversible and will close screen.");
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
_ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(GroupFullInfo.Group, pair.Key.UserData, userInfo));
|
using (ImRaii.PushColor(ImGuiCol.Text, pair.Value != null && pair.Value.Value.IsModerator() ? UIColors.Get("DimRed") : UIColors.Get("PairBlue")))
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.UserShield))
|
||||||
|
{
|
||||||
|
GroupPairUserInfo userInfo = pair.Value ?? GroupPairUserInfo.None;
|
||||||
|
|
||||||
|
userInfo.SetModerator(!userInfo.IsModerator());
|
||||||
|
|
||||||
|
_ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(GroupFullInfo.Group, pair.Key.UserData, userInfo));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip(pair.Value != null && pair.Value.Value.IsModerator() ? "Demod user" : "Mod user");
|
UiSharedService.AttachToolTip(pair.Value != null && pair.Value.Value.IsModerator() ? "Demod user" : "Mod user");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
@@ -317,7 +336,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
if (_uiSharedService.MediumTreeNode("Mass Cleanup", UIColors.Get("LightlessPurple")))
|
if (_uiSharedService.MediumTreeNode("Mass Cleanup", UIColors.Get("DimRed")))
|
||||||
{
|
{
|
||||||
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
|
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
|
||||||
{
|
{
|
||||||
@@ -329,6 +348,18 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
UiSharedService.AttachToolTip("This will remove all non-pinned, non-moderator users from the Syncshell."
|
UiSharedService.AttachToolTip("This will remove all non-pinned, non-moderator users from the Syncshell."
|
||||||
+ UiSharedService.TooltipSeparator + "Hold CTRL to enable this button");
|
+ 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);
|
ImGuiHelpers.ScaledDummy(2f);
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGuiHelpers.ScaledDummy(2f);
|
ImGuiHelpers.ScaledDummy(2f);
|
||||||
@@ -391,12 +422,12 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
UiSharedService.TextWrapped($"Syncshell was pruned and {_pruneTask.Result} inactive user(s) have been removed.");
|
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.TreePop();
|
||||||
}
|
}
|
||||||
ImGui.Separator();
|
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"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server"))
|
||||||
{
|
{
|
||||||
@@ -437,7 +468,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
ImGui.EndTable();
|
ImGui.EndTable();
|
||||||
}
|
}
|
||||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f);
|
||||||
ImGui.TreePop();
|
ImGui.TreePop();
|
||||||
}
|
}
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|||||||
338
LightlessSync/UI/SyncshellFinderUI.cs
Normal file
338
LightlessSync/UI/SyncshellFinderUI.cs
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
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 readonly HashSet<string> _recentlyJoined = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
// Check if there is an active broadcast for this syncshell, if not, skipping this syncshell
|
||||||
|
var broadcast = _broadcastScannerService.GetActiveSyncshellBroadcasts()
|
||||||
|
.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal));
|
||||||
|
|
||||||
|
if (broadcast == null)
|
||||||
|
continue; // no active broadcasts
|
||||||
|
|
||||||
|
var (Name, Address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID);
|
||||||
|
if (string.IsNullOrEmpty(Name))
|
||||||
|
continue; // broadcaster not found in area, skipping
|
||||||
|
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
|
||||||
|
var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID;
|
||||||
|
ImGui.TextUnformatted(displayName);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(Address);
|
||||||
|
var 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));
|
||||||
|
|
||||||
|
var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal));
|
||||||
|
var isRecentlyJoined = _recentlyJoined.Contains(shell.GID);
|
||||||
|
|
||||||
|
if (!isAlreadyMember && !isRecentlyJoined)
|
||||||
|
{
|
||||||
|
if (ImGui.Button(label))
|
||||||
|
{
|
||||||
|
_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));
|
||||||
|
|
||||||
|
_recentlyJoined.Add(_joinDto.Group.GID);
|
||||||
|
|
||||||
|
_joinDto = null;
|
||||||
|
_joinInfo = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)];
|
||||||
|
|
||||||
|
_recentlyJoined.RemoveWhere(gid => _currentSyncshells.Any(s => string.Equals(s.GID, gid, StringComparison.Ordinal)));
|
||||||
|
|
||||||
|
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 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 Dalamud.Utility;
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Data.Extensions;
|
using LightlessSync.API.Data.Extensions;
|
||||||
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.Utils;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
|
using Serilog;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Reflection.Emit;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LightlessSync.UI;
|
namespace LightlessSync.UI;
|
||||||
|
|
||||||
@@ -19,18 +27,25 @@ public class TopTabMenu
|
|||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
|
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly PairRequestService _pairRequestService;
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly HashSet<string> _pendingPairRequestActions = new(StringComparer.Ordinal);
|
||||||
|
private bool _pairRequestsExpanded; // useless for now
|
||||||
|
private int _lastRequestCount;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private string _filter = string.Empty;
|
private string _filter = string.Empty;
|
||||||
private int _globalControlCountdown = 0;
|
private int _globalControlCountdown = 0;
|
||||||
|
private float _pairRequestsHeight = 150f;
|
||||||
private string _pairToAdd = string.Empty;
|
private string _pairToAdd = string.Empty;
|
||||||
|
|
||||||
private SelectedTab _selectedTab = SelectedTab.None;
|
private SelectedTab _selectedTab = SelectedTab.None;
|
||||||
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService)
|
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService)
|
||||||
{
|
{
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
|
_pairRequestService = pairRequestService;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,8 +54,9 @@ public class TopTabMenu
|
|||||||
None,
|
None,
|
||||||
Individual,
|
Individual,
|
||||||
Syncshell,
|
Syncshell,
|
||||||
Filter,
|
Lightfinder,
|
||||||
UserConfig
|
UserConfig,
|
||||||
|
Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Filter
|
public string Filter
|
||||||
@@ -60,11 +76,6 @@ public class TopTabMenu
|
|||||||
{
|
{
|
||||||
get => _selectedTab; set
|
get => _selectedTab; set
|
||||||
{
|
{
|
||||||
if (_selectedTab == SelectedTab.Filter && value != SelectedTab.Filter)
|
|
||||||
{
|
|
||||||
Filter = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
_selectedTab = value;
|
_selectedTab = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,11 +83,11 @@ public class TopTabMenu
|
|||||||
{
|
{
|
||||||
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing;
|
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 buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y;
|
||||||
var buttonSize = new Vector2(buttonX, buttonY);
|
var buttonSize = new Vector2(buttonX, buttonY);
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
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)));
|
var btncolor = ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0)));
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(spacing.Y / 2f);
|
ImGuiHelpers.ScaledDummy(spacing.Y / 2f);
|
||||||
@@ -117,19 +128,19 @@ public class TopTabMenu
|
|||||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||||
{
|
{
|
||||||
var x = ImGui.GetCursorScreenPos();
|
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();
|
ImGui.SameLine();
|
||||||
var xAfter = ImGui.GetCursorScreenPos();
|
var xAfter = ImGui.GetCursorScreenPos();
|
||||||
if (TabSelection == SelectedTab.Filter)
|
if (TabSelection == SelectedTab.Lightfinder)
|
||||||
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y },
|
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 },
|
xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
|
||||||
underlineColor, 2);
|
underlineColor, 2);
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Filter");
|
UiSharedService.AttachToolTip("Lightfinder");
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||||
@@ -149,6 +160,18 @@ public class TopTabMenu
|
|||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Your User Menu");
|
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();
|
ImGui.NewLine();
|
||||||
btncolor.Dispose();
|
btncolor.Dispose();
|
||||||
|
|
||||||
@@ -164,9 +187,9 @@ public class TopTabMenu
|
|||||||
DrawSyncshellMenu(availableWidth, spacing.X);
|
DrawSyncshellMenu(availableWidth, spacing.X);
|
||||||
DrawGlobalSyncshellButtons(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)
|
else if (TabSelection == SelectedTab.UserConfig)
|
||||||
{
|
{
|
||||||
@@ -174,7 +197,25 @@ public class TopTabMenu
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
|
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();
|
ImGui.Separator();
|
||||||
|
|
||||||
|
DrawFilter(availableWidth, spacing.X);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawAddPair(float availableXWidth, float spacingX)
|
private void DrawAddPair(float availableXWidth, float spacingX)
|
||||||
@@ -195,6 +236,207 @@ public class TopTabMenu
|
|||||||
UiSharedService.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd));
|
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)
|
private void DrawFilter(float availableWidth, float spacingX)
|
||||||
{
|
{
|
||||||
var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.Ban, "Clear");
|
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)
|
private void DrawUserConfig(float availableWidth, float spacingX)
|
||||||
{
|
{
|
||||||
var buttonX = (availableWidth - spacingX) / 2f;
|
var buttonX = (availableWidth - spacingX) / 2f;
|
||||||
|
|||||||
@@ -9,10 +9,23 @@ namespace LightlessSync.UI
|
|||||||
private static readonly Dictionary<string, string> DefaultHexColors = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly Dictionary<string, string> DefaultHexColors = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ "LightlessPurple", "#ad8af5" },
|
{ "LightlessPurple", "#ad8af5" },
|
||||||
|
{ "LightlessPurpleActive", "#be9eff" },
|
||||||
|
{ "LightlessPurpleDefault", "#9375d1" },
|
||||||
|
|
||||||
|
{ "ButtonDefault", "#323232" },
|
||||||
|
{ "FullBlack", "#000000" },
|
||||||
|
|
||||||
{ "LightlessBlue", "#a6c2ff" },
|
{ "LightlessBlue", "#a6c2ff" },
|
||||||
{ "LightlessYellow", "#ffe97a" },
|
{ "LightlessYellow", "#ffe97a" },
|
||||||
|
{ "LightlessYellow2", "#cfbd63" },
|
||||||
|
{ "LightlessGreen", "#7cd68a" },
|
||||||
{ "PairBlue", "#88a2db" },
|
{ "PairBlue", "#88a2db" },
|
||||||
{ "DimRed", "#d44444" },
|
{ "DimRed", "#d44444" },
|
||||||
|
|
||||||
|
{ "LightlessAdminText", "#ffd663" },
|
||||||
|
{ "LightlessAdminGlow", "#b09343" },
|
||||||
|
{ "LightlessModeratorText", "#94ffda" },
|
||||||
|
{ "LightlessModeratorGlow", "#599c84" },
|
||||||
};
|
};
|
||||||
|
|
||||||
private static LightlessConfigService? _configService;
|
private static LightlessConfigService? _configService;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.GameFonts;
|
using Dalamud.Interface.GameFonts;
|
||||||
@@ -7,6 +7,7 @@ using Dalamud.Interface.ManagedFontAtlas;
|
|||||||
using Dalamud.Interface.Textures.TextureWraps;
|
using Dalamud.Interface.Textures.TextureWraps;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using System;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
@@ -173,12 +174,14 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public static string ByteToString(long bytes, bool addSuffix = true)
|
public static string ByteToString(long bytes, bool addSuffix = true)
|
||||||
{
|
{
|
||||||
string[] suffix = ["B", "KiB", "MiB", "GiB", "TiB"];
|
string[] suffix = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
|
||||||
int i;
|
int i = 0;
|
||||||
double dblSByte = bytes;
|
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}";
|
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));
|
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)
|
public void MediumText(string text, Vector4? color = null)
|
||||||
{
|
{
|
||||||
FontText(text, MediumFont, color);
|
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)
|
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));
|
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,
|
return IconTextButtonInternal(icon, text,
|
||||||
isInPopup ? ColorHelpers.RgbaUintToVector4(ImGui.GetColorU32(ImGuiCol.PopupBg)) : null,
|
isInPopup ? ColorHelpers.RgbaUintToVector4(ImGui.GetColorU32(ImGuiCol.PopupBg)) : null,
|
||||||
width <= 0 ? null : width);
|
width <= 0 ? null : width, center);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDalamudTextureWrap LoadImage(byte[] imageData)
|
public IDalamudTextureWrap LoadImage(byte[] imageData)
|
||||||
@@ -1193,7 +1276,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
ImGui.TextUnformatted(text);
|
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;
|
int num = 0;
|
||||||
if (defaultColor.HasValue)
|
if (defaultColor.HasValue)
|
||||||
@@ -1203,19 +1286,35 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImGui.PushID(text);
|
ImGui.PushID(text);
|
||||||
|
|
||||||
Vector2 vector;
|
Vector2 vector;
|
||||||
using (IconFont.Push())
|
using (IconFont.Push())
|
||||||
vector = ImGui.CalcTextSize(icon.ToIconString());
|
vector = ImGui.CalcTextSize(icon.ToIconString());
|
||||||
|
|
||||||
Vector2 vector2 = ImGui.CalcTextSize(text);
|
Vector2 vector2 = ImGui.CalcTextSize(text);
|
||||||
ImDrawListPtr windowDrawList = ImGui.GetWindowDrawList();
|
ImDrawListPtr windowDrawList = ImGui.GetWindowDrawList();
|
||||||
Vector2 cursorScreenPos = ImGui.GetCursorScreenPos();
|
Vector2 cursorScreenPos = ImGui.GetCursorScreenPos();
|
||||||
float num2 = 3f * ImGuiHelpers.GlobalScale;
|
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();
|
float frameHeight = ImGui.GetFrameHeight();
|
||||||
|
|
||||||
bool result = ImGui.Button(string.Empty, new Vector2(x, frameHeight));
|
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())
|
using (IconFont.Push())
|
||||||
windowDrawList.AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString());
|
windowDrawList.AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString());
|
||||||
|
|
||||||
Vector2 pos2 = new Vector2(pos.X + vector.X + num2, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y);
|
Vector2 pos2 = new Vector2(pos.X + vector.X + num2, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y);
|
||||||
windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), text);
|
windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), text);
|
||||||
ImGui.PopID();
|
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
|
||||||
|
}
|
||||||
15
LightlessSync/Utils/UtilsEnum/LabelAlignment.cs
Normal file
15
LightlessSync/Utils/UtilsEnum/LabelAlignment.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LightlessSync.UtilsEnum.Enum
|
||||||
|
{
|
||||||
|
public enum LabelAlignment
|
||||||
|
{
|
||||||
|
Left,
|
||||||
|
Center,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using K4os.Compression.LZ4.Legacy;
|
using K4os.Compression.LZ4.Legacy;
|
||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Dto.Files;
|
using LightlessSync.API.Dto.Files;
|
||||||
@@ -8,6 +8,7 @@ using LightlessSync.PlayerData.Handlers;
|
|||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.WebAPI.Files.Models;
|
using LightlessSync.WebAPI.Files.Models;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
private readonly FileCompactor _fileCompactor;
|
private readonly FileCompactor _fileCompactor;
|
||||||
private readonly FileCacheManager _fileDbManager;
|
private readonly FileCacheManager _fileDbManager;
|
||||||
private readonly FileTransferOrchestrator _orchestrator;
|
private readonly FileTransferOrchestrator _orchestrator;
|
||||||
private readonly List<ThrottledStream> _activeDownloadStreams;
|
private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams;
|
||||||
|
|
||||||
public FileDownloadManager(ILogger<FileDownloadManager> logger, LightlessMediator mediator,
|
public FileDownloadManager(ILogger<FileDownloadManager> logger, LightlessMediator mediator,
|
||||||
FileTransferOrchestrator orchestrator,
|
FileTransferOrchestrator orchestrator,
|
||||||
@@ -29,14 +30,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
_orchestrator = orchestrator;
|
_orchestrator = orchestrator;
|
||||||
_fileDbManager = fileCacheManager;
|
_fileDbManager = fileCacheManager;
|
||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
_activeDownloadStreams = [];
|
_activeDownloadStreams = new();
|
||||||
|
|
||||||
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
|
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
if (!_activeDownloadStreams.Any()) return;
|
if (_activeDownloadStreams.IsEmpty) return;
|
||||||
var newLimit = _orchestrator.DownloadLimitPerSlot();
|
var newLimit = _orchestrator.DownloadLimitPerSlot();
|
||||||
Logger.LogTrace("Setting new Download Speed Limit to {newLimit}", newLimit);
|
Logger.LogTrace("Setting new Download Speed Limit to {newLimit}", newLimit);
|
||||||
foreach (var stream in _activeDownloadStreams)
|
foreach (var stream in _activeDownloadStreams.Keys)
|
||||||
{
|
{
|
||||||
stream.BandwidthLimit = newLimit;
|
stream.BandwidthLimit = newLimit;
|
||||||
}
|
}
|
||||||
@@ -47,7 +48,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public List<FileTransfer> ForbiddenTransfers => _orchestrator.ForbiddenTransfers;
|
public List<FileTransfer> ForbiddenTransfers => _orchestrator.ForbiddenTransfers;
|
||||||
|
|
||||||
public bool IsDownloading => !CurrentDownloads.Any();
|
public bool IsDownloading => CurrentDownloads.Any();
|
||||||
|
|
||||||
public static void MungeBuffer(Span<byte> buffer)
|
public static void MungeBuffer(Span<byte> buffer)
|
||||||
{
|
{
|
||||||
@@ -84,7 +85,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
ClearDownload();
|
ClearDownload();
|
||||||
foreach (var stream in _activeDownloadStreams.ToList())
|
foreach (var stream in _activeDownloadStreams.Keys.ToList())
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -95,6 +96,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
// do nothing
|
// do nothing
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_activeDownloadStreams.TryRemove(stream, out _);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
@@ -142,30 +147,64 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
await WaitForDownloadReady(fileTransfer, requestId, ct).ConfigureAwait(false);
|
await WaitForDownloadReady(fileTransfer, requestId, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
_downloadStatus[downloadGroup].DownloadStatus = DownloadStatus.Downloading;
|
if (_downloadStatus.TryGetValue(downloadGroup, out var downloadStatus))
|
||||||
|
{
|
||||||
|
downloadStatus.DownloadStatus = DownloadStatus.Downloading;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Download status missing for {group} when starting download", downloadGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int maxRetries = 3;
|
||||||
|
int retryCount = 0;
|
||||||
|
TimeSpan retryDelay = TimeSpan.FromSeconds(2);
|
||||||
|
|
||||||
HttpResponseMessage response = null!;
|
HttpResponseMessage response = null!;
|
||||||
var requestUrl = LightlessFiles.CacheGetFullPath(fileTransfer[0].DownloadUri, requestId);
|
var requestUrl = LightlessFiles.CacheGetFullPath(fileTransfer[0].DownloadUri, requestId);
|
||||||
|
|
||||||
Logger.LogDebug("Downloading {requestUrl} for request {id}", requestUrl, requestId);
|
while (true)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
response = await _orchestrator.SendRequestAsync(HttpMethod.Get, requestUrl, ct, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
|
try
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
catch (HttpRequestException ex)
|
|
||||||
{
|
|
||||||
Logger.LogWarning(ex, "Error during download of {requestUrl}, HttpStatusCode: {code}", requestUrl, ex.StatusCode);
|
|
||||||
if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized)
|
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex);
|
Logger.LogDebug("Attempt {attempt} - Downloading {requestUrl} for request {id}", retryCount + 1, requestUrl, requestId);
|
||||||
|
|
||||||
|
response = await _orchestrator.SendRequestAsync(HttpMethod.Get, requestUrl, ct, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex) when (ex.InnerException is TimeoutException || ex.StatusCode == null)
|
||||||
|
{
|
||||||
|
retryCount++;
|
||||||
|
|
||||||
|
Logger.LogWarning(ex, "Timeout during download of {requestUrl}. Attempt {attempt} of {maxRetries}", requestUrl, retryCount, maxRetries);
|
||||||
|
|
||||||
|
if (retryCount >= maxRetries || ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Max retries reached or cancelled. Failing download for {requestUrl}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(retryDelay, ct).ConfigureAwait(false); // Wait before retrying
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during download of {requestUrl}, HttpStatusCode: {code}", requestUrl, ex.StatusCode);
|
||||||
|
|
||||||
|
if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ThrottledStream? stream = null;
|
ThrottledStream? stream = null;
|
||||||
|
FileStream? fileStream = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var fileStream = File.Create(tempPath);
|
fileStream = File.Create(tempPath);
|
||||||
await using (fileStream.ConfigureAwait(false))
|
await using (fileStream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var bufferSize = response.Content.Headers.ContentLength > 1024 * 1024 ? 65536 : 8196;
|
var bufferSize = response.Content.Headers.ContentLength > 1024 * 1024 ? 65536 : 8196;
|
||||||
@@ -174,8 +213,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
var bytesRead = 0;
|
var bytesRead = 0;
|
||||||
var limit = _orchestrator.DownloadLimitPerSlot();
|
var limit = _orchestrator.DownloadLimitPerSlot();
|
||||||
Logger.LogTrace("Starting Download of {id} with a speed limit of {limit} to {tempPath}", requestId, limit, tempPath);
|
Logger.LogTrace("Starting Download of {id} with a speed limit of {limit} to {tempPath}", requestId, limit, tempPath);
|
||||||
stream = new ThrottledStream(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), limit);
|
|
||||||
_activeDownloadStreams.Add(stream);
|
stream = new(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), limit);
|
||||||
|
|
||||||
|
_activeDownloadStreams.TryAdd(stream, 0);
|
||||||
|
|
||||||
while ((bytesRead = await stream.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
|
while ((bytesRead = await stream.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
@@ -194,24 +236,28 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!tempPath.IsNullOrEmpty())
|
fileStream?.Close();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(tempPath) && File.Exists(tempPath))
|
||||||
|
{
|
||||||
File.Delete(tempPath);
|
File.Delete(tempPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// ignore if file deletion fails
|
// Ignore errors during cleanup
|
||||||
}
|
}
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (stream != null)
|
if (stream != null)
|
||||||
{
|
{
|
||||||
_activeDownloadStreams.Remove(stream);
|
_activeDownloadStreams.TryRemove(stream, out _);
|
||||||
await stream.DisposeAsync().ConfigureAwait(false);
|
await stream.DisposeAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,11 +265,28 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public async Task<List<DownloadFileTransfer>> InitiateDownloadList(GameObjectHandler gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct)
|
public async Task<List<DownloadFileTransfer>> InitiateDownloadList(GameObjectHandler gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Download start: {id}", gameObjectHandler.Name);
|
var objectName = gameObjectHandler?.Name ?? "Unknown";
|
||||||
|
Logger.LogDebug("Download start: {id}", objectName);
|
||||||
|
|
||||||
|
if (fileReplacement == null || fileReplacement.Count == 0)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("{dlName}: No file replacements provided", objectName);
|
||||||
|
CurrentDownloads = [];
|
||||||
|
return CurrentDownloads;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hashes = fileReplacement.Where(f => f != null && !string.IsNullOrWhiteSpace(f.Hash)).Select(f => f.Hash).Distinct(StringComparer.Ordinal).ToList();
|
||||||
|
|
||||||
|
if (hashes.Count == 0)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("{dlName}: No valid hashes to download", objectName);
|
||||||
|
CurrentDownloads = [];
|
||||||
|
return CurrentDownloads;
|
||||||
|
}
|
||||||
|
|
||||||
List<DownloadFileDto> downloadFileInfoFromService =
|
List<DownloadFileDto> downloadFileInfoFromService =
|
||||||
[
|
[
|
||||||
.. await FilesGetSizes(fileReplacement.Select(f => f.Hash).Distinct(StringComparer.Ordinal).ToList(), ct).ConfigureAwait(false),
|
.. await FilesGetSizes(hashes, ct).ConfigureAwait(false),
|
||||||
];
|
];
|
||||||
|
|
||||||
Logger.LogDebug("Files with size 0 or less: {files}", string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash)));
|
Logger.LogDebug("Files with size 0 or less: {files}", string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash)));
|
||||||
@@ -281,15 +344,23 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
FileInfo fi = new(blockFile);
|
FileInfo fi = new(blockFile);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForSlot;
|
if (!_downloadStatus.TryGetValue(fileGroup.Key, out var downloadStatus))
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Download status missing for {group}, aborting", fileGroup.Key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadStatus.DownloadStatus = DownloadStatus.WaitingForSlot;
|
||||||
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
|
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
|
||||||
_downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForQueue;
|
downloadStatus.DownloadStatus = DownloadStatus.WaitingForQueue;
|
||||||
Progress<long> progress = new((bytesDownloaded) =>
|
Progress<long> progress = new((bytesDownloaded) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!_downloadStatus.TryGetValue(fileGroup.Key, out FileDownloadStatus? value)) return;
|
if (_downloadStatus.TryGetValue(fileGroup.Key, out FileDownloadStatus? value))
|
||||||
value.TransferredBytes += bytesDownloaded;
|
{
|
||||||
|
value.TransferredBytes += bytesDownloaded;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -319,6 +390,12 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
status.TransferredFiles = 1;
|
status.TransferredFiles = 1;
|
||||||
status.DownloadStatus = DownloadStatus.Decompressing;
|
status.DownloadStatus = DownloadStatus.Decompressing;
|
||||||
}
|
}
|
||||||
|
if (!File.Exists(blockFile))
|
||||||
|
{
|
||||||
|
Logger.LogWarning("{dlName}: Block file missing before extraction, skipping", fi.Name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fileBlockStream = File.OpenRead(blockFile);
|
fileBlockStream = File.OpenRead(blockFile);
|
||||||
while (fileBlockStream.Position < fileBlockStream.Length)
|
while (fileBlockStream.Position < fileBlockStream.Length)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Dto.Files;
|
using LightlessSync.API.Dto.Files;
|
||||||
using LightlessSync.API.Routes;
|
using LightlessSync.API.Routes;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
@@ -10,6 +10,8 @@ using LightlessSync.WebAPI.Files.Models;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace LightlessSync.WebAPI.Files;
|
namespace LightlessSync.WebAPI.Files;
|
||||||
|
|
||||||
@@ -19,7 +21,9 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
private readonly LightlessConfigService _lightlessConfigService;
|
private readonly LightlessConfigService _lightlessConfigService;
|
||||||
private readonly FileTransferOrchestrator _orchestrator;
|
private readonly FileTransferOrchestrator _orchestrator;
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
private readonly Dictionary<string, DateTime> _verifiedUploadedHashes = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, DateTime> _verifiedUploadedHashes = new(StringComparer.Ordinal);
|
||||||
|
private readonly object _currentUploadsLock = new();
|
||||||
|
private readonly Dictionary<string, FileTransfer> _currentUploadsByHash = new(StringComparer.Ordinal);
|
||||||
private CancellationTokenSource? _uploadCancellationTokenSource = new();
|
private CancellationTokenSource? _uploadCancellationTokenSource = new();
|
||||||
|
|
||||||
public FileUploadManager(ILogger<FileUploadManager> logger, LightlessMediator mediator,
|
public FileUploadManager(ILogger<FileUploadManager> logger, LightlessMediator mediator,
|
||||||
@@ -40,17 +44,38 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<FileTransfer> CurrentUploads { get; } = [];
|
public List<FileTransfer> CurrentUploads { get; } = [];
|
||||||
public bool IsUploading => CurrentUploads.Count > 0;
|
public bool IsUploading
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
return CurrentUploads.Count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FileTransfer> GetCurrentUploadsSnapshot()
|
||||||
|
{
|
||||||
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
return CurrentUploads.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool CancelUpload()
|
public bool CancelUpload()
|
||||||
{
|
{
|
||||||
if (CurrentUploads.Any())
|
if (IsUploading)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Cancelling current upload");
|
Logger.LogDebug("Cancelling current upload");
|
||||||
_uploadCancellationTokenSource?.Cancel();
|
_uploadCancellationTokenSource?.Cancel();
|
||||||
_uploadCancellationTokenSource?.Dispose();
|
_uploadCancellationTokenSource?.Dispose();
|
||||||
_uploadCancellationTokenSource = null;
|
_uploadCancellationTokenSource = null;
|
||||||
CurrentUploads.Clear();
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
CurrentUploads.Clear();
|
||||||
|
_currentUploadsByHash.Clear();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +94,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
Logger.LogDebug("Trying to upload files");
|
Logger.LogDebug("Trying to upload files");
|
||||||
var filesPresentLocally = hashesToUpload.Where(h => _fileDbManager.GetFileCacheByHash(h) != null).ToHashSet(StringComparer.Ordinal);
|
var filesPresentLocally = hashesToUpload.Where(h => _fileDbManager.GetFileCacheByHash(h) != null).ToHashSet(StringComparer.Ordinal);
|
||||||
var locallyMissingFiles = hashesToUpload.Except(filesPresentLocally, StringComparer.Ordinal).ToList();
|
var locallyMissingFiles = hashesToUpload.Except(filesPresentLocally, StringComparer.Ordinal).ToList();
|
||||||
if (locallyMissingFiles.Any())
|
if (locallyMissingFiles.Count != 0)
|
||||||
{
|
{
|
||||||
return locallyMissingFiles;
|
return locallyMissingFiles;
|
||||||
}
|
}
|
||||||
@@ -83,22 +108,44 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
return [.. filesToUpload.Where(f => f.IsForbidden).Select(f => f.Hash)];
|
return [.. filesToUpload.Where(f => f.IsForbidden).Select(f => f.Hash)];
|
||||||
}
|
}
|
||||||
|
|
||||||
Task uploadTask = Task.CompletedTask;
|
var cancellationToken = ct ?? CancellationToken.None;
|
||||||
|
var parallelUploads = Math.Clamp(_lightlessConfigService.Current.ParallelUploads, 1, 8);
|
||||||
|
using SemaphoreSlim uploadSlots = new(parallelUploads, parallelUploads);
|
||||||
|
List<Task> uploadTasks = new();
|
||||||
|
|
||||||
int i = 1;
|
int i = 1;
|
||||||
foreach (var file in filesToUpload)
|
foreach (var file in filesToUpload)
|
||||||
{
|
{
|
||||||
progress.Report($"Uploading file {i++}/{filesToUpload.Count}. Please wait until the upload is completed.");
|
progress.Report($"Uploading file {i++}/{filesToUpload.Count}. Please wait until the upload is completed.");
|
||||||
Logger.LogDebug("[{hash}] Compressing", file);
|
uploadTasks.Add(UploadSingleFileAsync(file, uploadSlots, cancellationToken));
|
||||||
var data = await _fileDbManager.GetCompressedFileData(file.Hash, ct ?? CancellationToken.None).ConfigureAwait(false);
|
|
||||||
Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, _fileDbManager.GetFileCacheByHash(data.Item1)!.ResolvedFilepath);
|
|
||||||
await uploadTask.ConfigureAwait(false);
|
|
||||||
uploadTask = UploadFile(data.Item2, file.Hash, false, ct ?? CancellationToken.None);
|
|
||||||
(ct ?? CancellationToken.None).ThrowIfCancellationRequested();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await uploadTask.ConfigureAwait(false);
|
await Task.WhenAll(uploadTasks).ConfigureAwait(false);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
|
async Task UploadSingleFileAsync(UploadFileDto fileDto, SemaphoreSlim gate, CancellationToken token)
|
||||||
|
{
|
||||||
|
await gate.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
Logger.LogDebug("[{hash}] Compressing", fileDto.Hash);
|
||||||
|
var data = await _fileDbManager.GetCompressedFileData(fileDto.Hash, token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var cacheEntry = _fileDbManager.GetFileCacheByHash(data.Item1);
|
||||||
|
if (cacheEntry != null)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, cacheEntry.ResolvedFilepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await UploadFile(data.Item2, fileDto.Hash, postProgress: false, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
gate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CharacterData> UploadFiles(CharacterData data, List<UserData> visiblePlayers)
|
public async Task<CharacterData> UploadFiles(CharacterData data, List<UserData> visiblePlayers)
|
||||||
@@ -167,7 +214,11 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
_uploadCancellationTokenSource?.Cancel();
|
_uploadCancellationTokenSource?.Cancel();
|
||||||
_uploadCancellationTokenSource?.Dispose();
|
_uploadCancellationTokenSource?.Dispose();
|
||||||
_uploadCancellationTokenSource = null;
|
_uploadCancellationTokenSource = null;
|
||||||
CurrentUploads.Clear();
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
CurrentUploads.Clear();
|
||||||
|
_currentUploadsByHash.Clear();
|
||||||
|
}
|
||||||
_verifiedUploadedHashes.Clear();
|
_verifiedUploadedHashes.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +262,17 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CurrentUploads.Single(f => string.Equals(f.Hash, fileHash, StringComparison.Ordinal)).Transferred = prog.Uploaded;
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
if (_currentUploadsByHash.TryGetValue(fileHash, out var transfer))
|
||||||
|
{
|
||||||
|
transfer.Transferred = prog.Uploaded;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{hash}] Could not find upload transfer during progress update", fileHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -240,10 +301,16 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CurrentUploads.Add(new UploadFileTransfer(file)
|
var uploadTransfer = new UploadFileTransfer(file)
|
||||||
{
|
{
|
||||||
Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length,
|
Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
CurrentUploads.Add(uploadTransfer);
|
||||||
|
_currentUploadsByHash[file.Hash] = uploadTransfer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -264,33 +331,75 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
|||||||
_verifiedUploadedHashes[file.Hash] = DateTime.UtcNow;
|
_verifiedUploadedHashes[file.Hash] = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalSize = CurrentUploads.Sum(c => c.Total);
|
long totalSize;
|
||||||
|
List<FileTransfer> pendingUploads;
|
||||||
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
totalSize = CurrentUploads.Sum(c => c.Total);
|
||||||
|
pendingUploads = CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var parallelUploads = Math.Clamp(_lightlessConfigService.Current.ParallelUploads, 1, 8);
|
||||||
|
using SemaphoreSlim uploadSlots = new(parallelUploads, parallelUploads);
|
||||||
Logger.LogDebug("Compressing and uploading files");
|
Logger.LogDebug("Compressing and uploading files");
|
||||||
Task uploadTask = Task.CompletedTask;
|
List<Task> uploadTasks = new();
|
||||||
foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList())
|
|
||||||
|
foreach (var transfer in pendingUploads)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("[{hash}] Compressing", file);
|
uploadTasks.Add(UploadPendingFileAsync(transfer, uploadSlots, uploadToken));
|
||||||
var data = await _fileDbManager.GetCompressedFileData(file.Hash, uploadToken).ConfigureAwait(false);
|
|
||||||
CurrentUploads.Single(e => string.Equals(e.Hash, data.Item1, StringComparison.Ordinal)).Total = data.Item2.Length;
|
|
||||||
Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, _fileDbManager.GetFileCacheByHash(data.Item1)!.ResolvedFilepath);
|
|
||||||
await uploadTask.ConfigureAwait(false);
|
|
||||||
uploadTask = UploadFile(data.Item2, file.Hash, true, uploadToken);
|
|
||||||
uploadToken.ThrowIfCancellationRequested();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CurrentUploads.Any())
|
await Task.WhenAll(uploadTasks).ConfigureAwait(false);
|
||||||
{
|
|
||||||
await uploadTask.ConfigureAwait(false);
|
|
||||||
|
|
||||||
var compressedSize = CurrentUploads.Sum(c => c.Total);
|
long compressedSize;
|
||||||
Logger.LogDebug("Upload complete, compressed {size} to {compressed}", UiSharedService.ByteToString(totalSize), UiSharedService.ByteToString(compressedSize));
|
HashSet<string> uploadedHashes;
|
||||||
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
compressedSize = CurrentUploads.Sum(c => c.Total);
|
||||||
|
uploadedHashes = CurrentUploads.Select(u => u.Hash).ToHashSet(StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Exists(u => string.Equals(u.Hash, c, StringComparison.Ordinal))))
|
Logger.LogDebug("Upload complete, compressed {size} to {compressed}", UiSharedService.ByteToString(totalSize), UiSharedService.ByteToString(compressedSize));
|
||||||
|
|
||||||
|
foreach (var file in unverifiedUploadHashes.Where(c => !uploadedHashes.Contains(c)))
|
||||||
{
|
{
|
||||||
_verifiedUploadedHashes[file] = DateTime.UtcNow;
|
_verifiedUploadedHashes[file] = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentUploads.Clear();
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
CurrentUploads.Clear();
|
||||||
|
_currentUploadsByHash.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task UploadPendingFileAsync(FileTransfer transfer, SemaphoreSlim gate, CancellationToken token)
|
||||||
|
{
|
||||||
|
await gate.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
Logger.LogDebug("[{hash}] Compressing", transfer.Hash);
|
||||||
|
var data = await _fileDbManager.GetCompressedFileData(transfer.Hash, token).ConfigureAwait(false);
|
||||||
|
lock (_currentUploadsLock)
|
||||||
|
{
|
||||||
|
if (_currentUploadsByHash.TryGetValue(data.Item1, out var trackedUpload))
|
||||||
|
{
|
||||||
|
trackedUpload.Total = data.Item2.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheEntry = _fileDbManager.GetFileCacheByHash(data.Item1);
|
||||||
|
if (cacheEntry != null)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, cacheEntry.ResolvedFilepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await UploadFile(data.Item2, transfer.Hash, true, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
gate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Dto;
|
using LightlessSync.API.Dto;
|
||||||
|
using LightlessSync.API.Dto.Group;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -34,6 +35,36 @@ public partial class ApiController
|
|||||||
await _lightlessHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false);
|
await _lightlessHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task TryPairWithContentId(string otherCid, 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()
|
public async Task UserDelete()
|
||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
@@ -103,6 +134,12 @@ public partial class ApiController
|
|||||||
await _lightlessHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false);
|
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)
|
public async Task UserUpdateDefaultPermissions(DefaultPermissionsDto defaultPermissionsDto)
|
||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
|
|||||||
@@ -105,6 +105,21 @@ public partial class ApiController
|
|||||||
return Task.CompletedTask;
|
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)
|
public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo)
|
||||||
{
|
{
|
||||||
SystemInfoDto = systemInfo;
|
SystemInfoDto = systemInfo;
|
||||||
@@ -277,12 +292,25 @@ public partial class ApiController
|
|||||||
_lightlessHub!.On(nameof(Client_GroupSendInfo), act);
|
_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)
|
public void OnReceiveServerMessage(Action<MessageSeverity, string> act)
|
||||||
{
|
{
|
||||||
if (_initialized) return;
|
if (_initialized) return;
|
||||||
_lightlessHub!.On(nameof(Client_ReceiveServerMessage), act);
|
_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)
|
public void OnUpdateSystemInfo(Action<SystemInfoDto> act)
|
||||||
{
|
{
|
||||||
if (_initialized) return;
|
if (_initialized) return;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using LightlessSync.API.Dto.Group;
|
using LightlessSync.API.Dto.Group;
|
||||||
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.WebAPI.SignalR.Utils;
|
using LightlessSync.WebAPI.SignalR.Utils;
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
|
||||||
@@ -44,6 +45,11 @@ public partial class ApiController
|
|||||||
CheckConnection();
|
CheckConnection();
|
||||||
await _lightlessHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false);
|
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()
|
public async Task<GroupJoinDto> GroupCreate()
|
||||||
{
|
{
|
||||||
@@ -80,6 +86,11 @@ public partial class ApiController
|
|||||||
CheckConnection();
|
CheckConnection();
|
||||||
return await _lightlessHub!.InvokeAsync<bool>(nameof(GroupJoinFinalize), passwordedGroup).ConfigureAwait(false);
|
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)
|
public async Task GroupLeave(GroupDto group)
|
||||||
{
|
{
|
||||||
@@ -116,6 +127,18 @@ public partial class ApiController
|
|||||||
CheckConnection();
|
CheckConnection();
|
||||||
await _lightlessHub!.SendAsync(nameof(GroupUnbanUser), groupPair).ConfigureAwait(false);
|
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()
|
private void CheckConnection()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Extensions;
|
using LightlessSync.API.Data.Extensions;
|
||||||
using LightlessSync.API.Dto;
|
using LightlessSync.API.Dto;
|
||||||
|
using LightlessSync.API.Dto.Group;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.API.SignalR;
|
using LightlessSync.API.SignalR;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
@@ -27,6 +28,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
private readonly HubFactory _hubFactory;
|
private readonly HubFactory _hubFactory;
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly PairRequestService _pairRequestService;
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
private readonly TokenProvider _tokenProvider;
|
private readonly TokenProvider _tokenProvider;
|
||||||
private readonly LightlessConfigService _lightlessConfigService;
|
private readonly LightlessConfigService _lightlessConfigService;
|
||||||
@@ -41,12 +43,13 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
private CensusUpdateMessage? _lastCensus;
|
private CensusUpdateMessage? _lastCensus;
|
||||||
|
|
||||||
public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, DalamudUtilService dalamudUtil,
|
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)
|
TokenProvider tokenProvider, LightlessConfigService lightlessConfigService) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_hubFactory = hubFactory;
|
_hubFactory = hubFactory;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
|
_pairRequestService = pairRequestService;
|
||||||
_serverManager = serverManager;
|
_serverManager = serverManager;
|
||||||
_tokenProvider = tokenProvider;
|
_tokenProvider = tokenProvider;
|
||||||
_lightlessConfigService = lightlessConfigService;
|
_lightlessConfigService = lightlessConfigService;
|
||||||
@@ -76,6 +79,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
public DefaultPermissionsDto? DefaultPermissions => _connectionDto?.DefaultPreferredPermissions ?? null;
|
public DefaultPermissionsDto? DefaultPermissions => _connectionDto?.DefaultPreferredPermissions ?? null;
|
||||||
public string DisplayName => _connectionDto?.User.AliasOrUID ?? string.Empty;
|
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 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));
|
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 string UID => _connectionDto?.User.UID ?? string.Empty;
|
||||||
|
|
||||||
|
public event Action? OnConnected;
|
||||||
|
|
||||||
public async Task<bool> CheckClientHealth()
|
public async Task<bool> CheckClientHealth()
|
||||||
{
|
{
|
||||||
return await _lightlessHub!.InvokeAsync<bool>(nameof(CheckClientHealth)).ConfigureAwait(false);
|
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);
|
_connectionDto = await GetConnectionDto().ConfigureAwait(false);
|
||||||
|
|
||||||
ServerState = ServerState.Connected;
|
ServerState = ServerState.Connected;
|
||||||
|
OnConnected?.Invoke();
|
||||||
|
|
||||||
var currentClientVer = Assembly.GetExecutingAssembly().GetName().Version!;
|
var currentClientVer = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||||
|
|
||||||
@@ -420,6 +430,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
Logger.LogDebug("Initializing data");
|
Logger.LogDebug("Initializing data");
|
||||||
OnDownloadReady((guid) => _ = Client_DownloadReady(guid));
|
OnDownloadReady((guid) => _ = Client_DownloadReady(guid));
|
||||||
OnReceiveServerMessage((sev, msg) => _ = Client_ReceiveServerMessage(sev, msg));
|
OnReceiveServerMessage((sev, msg) => _ = Client_ReceiveServerMessage(sev, msg));
|
||||||
|
OnReceiveBroadcastPairRequest(dto => _ = Client_ReceiveBroadcastPairRequest(dto));
|
||||||
OnUpdateSystemInfo((dto) => _ = Client_UpdateSystemInfo(dto));
|
OnUpdateSystemInfo((dto) => _ = Client_UpdateSystemInfo(dto));
|
||||||
|
|
||||||
OnUserSendOffline((dto) => _ = Client_UserSendOffline(dto));
|
OnUserSendOffline((dto) => _ = Client_UserSendOffline(dto));
|
||||||
@@ -441,6 +452,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
OnGroupPairLeft((dto) => _ = Client_GroupPairLeft(dto));
|
OnGroupPairLeft((dto) => _ = Client_GroupPairLeft(dto));
|
||||||
OnGroupSendFullInfo((dto) => _ = Client_GroupSendFullInfo(dto));
|
OnGroupSendFullInfo((dto) => _ = Client_GroupSendFullInfo(dto));
|
||||||
OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto));
|
OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto));
|
||||||
|
OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto));
|
||||||
OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto));
|
OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto));
|
||||||
|
|
||||||
OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto));
|
OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto));
|
||||||
@@ -517,6 +529,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ServerState = ServerState.Connected;
|
ServerState = ServerState.Connected;
|
||||||
|
OnConnected?.Invoke();
|
||||||
await LoadIninitialPairsAsync().ConfigureAwait(false);
|
await LoadIninitialPairsAsync().ConfigureAwait(false);
|
||||||
await LoadOnlinePairsAsync().ConfigureAwait(false);
|
await LoadOnlinePairsAsync().ConfigureAwait(false);
|
||||||
Mediator.Publish(new ConnectedMessage(_connectionDto));
|
Mediator.Publish(new ConnectedMessage(_connectionDto));
|
||||||
@@ -592,5 +605,20 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
|
|
||||||
ServerState = state;
|
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
|
#pragma warning restore MA0040
|
||||||
Reference in New Issue
Block a user