Compare commits
109 Commits
1.11.2
...
debug-clie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
012e80219e | ||
|
|
dae8127ac8 | ||
|
|
0635caab65 | ||
| 1530ac3911 | |||
| 3f80467180 | |||
|
|
4b4e587a89 | ||
|
|
02c3846031 | ||
|
|
a8a01b3034 | ||
| 50a5046c96 | |||
|
|
c0b8e15380 | ||
| e6735be594 | |||
|
|
fe419336d7 | ||
|
|
ffbeeba929 | ||
|
|
a7475a7007 | ||
|
|
3936cbd439 | ||
|
|
ba16963b66 | ||
| 6467a3e73b | |||
|
|
bb779904f7 | ||
|
|
59d0e8ee37 | ||
|
|
a441bbfcc8 | ||
|
|
c545ccea52 | ||
|
|
c32d9cadff | ||
|
|
d7c9df54cb | ||
| 37ec0961d9 | |||
| 9736c5090d | |||
|
|
4f3ab604db | ||
|
|
6a0f8c507c | ||
|
|
e13fde3d43 | ||
| 7b806ab660 | |||
|
|
387e5ad515 | ||
|
|
70c296a16b | ||
|
|
2a9b5812ed | ||
|
|
9b04976aa6 | ||
|
|
144ac166fb | ||
|
|
b06ffb3341 | ||
| e9461efe11 | |||
| 1f1afdec24 | |||
| d428a436e7 | |||
|
|
ad29fa7b69 | ||
|
|
23c56505ac | ||
|
|
58850f4530 | ||
|
|
f5339dc1d2 | ||
|
|
85ecea6391 | ||
|
|
cd817487e4 | ||
|
|
f50b622f0a | ||
|
|
d295f3e22d | ||
|
|
0dfa667ed3 | ||
|
|
2b118df892 | ||
|
|
f01229a97f | ||
|
|
3fdc9dd958 | ||
|
|
86107acf12 | ||
|
|
27e7fb7ed9 | ||
|
|
17f4ddad89 | ||
| a29e155cec | |||
|
|
1488704db4 | ||
|
|
46db5c87e0 | ||
| a772ee4705 | |||
|
|
1d88c04235 | ||
|
|
a7378652c4 | ||
| 61267d1b03 | |||
|
|
9b6d00570e | ||
|
|
83e4555e4b | ||
|
|
090b81c989 | ||
|
|
ca70c622bc | ||
|
|
49e5fb9d8d | ||
| 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 | |||
| abe28e931c | |||
|
|
b177dbd595 | ||
|
|
10d5519cc1 | ||
|
|
dc0265f614 | ||
|
|
ba0e1cea08 | ||
|
|
1dea9b713e | ||
|
|
23c57aedc4 |
285
.gitea/workflows/lightless-tag-and-release.yml
Normal file
285
.gitea/workflows/lightless-tag-and-release.yml
Normal file
@@ -0,0 +1,285 @@
|
||||
name: Tag and Release Lightless
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, dev ]
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: LightlessSync
|
||||
DOTNET_VERSION: 9.x
|
||||
|
||||
jobs:
|
||||
tag-and-release:
|
||||
runs-on: ubuntu-22.04
|
||||
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: |
|
||||
cd /
|
||||
mkdir -p root/.xlcore/dalamud/Hooks/dev
|
||||
curl -O https://goatcorp.github.io/dalamud-distrib/stg/latest.zip
|
||||
unzip latest.zip -d /root/.xlcore/dalamud/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
|
||||
run: |
|
||||
version=$(grep -oPm1 "(?<=<Version>)[^<]+" LightlessSync/LightlessSync.csproj)
|
||||
echo "version=$version" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Display version
|
||||
run: |
|
||||
echo "Version: ${{ steps.package_version.outputs.version }}"
|
||||
|
||||
- name: Prepare Lightless Client
|
||||
run: |
|
||||
PUBLISH_PATH="/workspace/Lightless-Sync/LightlessClient/LightlessSync/bin/x64/Release/publish/"
|
||||
if [ -d "$PUBLISH_PATH" ]; then
|
||||
rm -rf "$PUBLISH_PATH"
|
||||
echo "Removed $PUBLISH_PATH"
|
||||
else
|
||||
echo "$PUBLISH_PATH does not exist, nothing to remove."
|
||||
fi
|
||||
|
||||
mkdir -p output
|
||||
(cd /workspace/Lightless-Sync/LightlessClient/LightlessSync/bin/x64/Release/ && zip -r $OLDPWD/output/LightlessClient.zip *)
|
||||
|
||||
- name: Create Git tag if not exists (master)
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
tag="${{ steps.package_version.outputs.version }}"
|
||||
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 Git tag if not exists (dev)
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: |
|
||||
tag="${{ steps.package_version.outputs.version }}-Dev"
|
||||
git fetch --tags
|
||||
if ! git tag -l "$tag" | grep -q "$tag"; then
|
||||
echo "Tag $tag does not exist. Creating and pushing..."
|
||||
git config user.name "GitHub Action"
|
||||
git config user.email "action@github.com"
|
||||
git tag "$tag"
|
||||
git push origin "$tag"
|
||||
else
|
||||
echo "Tag $tag already exists. Skipping tag creation."
|
||||
fi
|
||||
|
||||
- name: Create Release (master)
|
||||
if: github.ref == 'refs/heads/master'
|
||||
id: create_release
|
||||
run: |
|
||||
echo "=== Searching for existing release ${{ steps.package_version.outputs.version }}==="
|
||||
|
||||
release_id=$(curl -s -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${{ steps.package_version.outputs.version }}" | jq -r .id)
|
||||
|
||||
if [ "$release_id" != "null" ]; then
|
||||
echo "=== Deleting existing release ${{ steps.package_version.outputs.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 ${{ steps.package_version.outputs.version }}==="
|
||||
response=$(
|
||||
curl --fail-with-body -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||
-d '{
|
||||
"tag_name": "${{ steps.package_version.outputs.version }}",
|
||||
"name": "${{ steps.package_version.outputs.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: Create Release (dev)
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
id: create_release
|
||||
run: |
|
||||
version="${{ steps.package_version.outputs.version }}-Dev"
|
||||
echo "=== Searching for existing release $version==="
|
||||
release_id=$(curl -s -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/$version" | jq -r .id)
|
||||
if [ "$release_id" != "null" ]; then
|
||||
echo "=== Deleting existing release $version==="
|
||||
curl -X DELETE -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/$release_id"
|
||||
fi
|
||||
echo "=== Creating new release $version==="
|
||||
response=$(
|
||||
curl --fail-with-body -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||
-d '{
|
||||
"tag_name": "'"$version"'",
|
||||
"name": "'"$version"'",
|
||||
"draft": false,
|
||||
"prerelease": false
|
||||
}' \
|
||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases"
|
||||
)
|
||||
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 \
|
||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||
-F "attachment=@output/LightlessClient.zip" \
|
||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/$RELEASE_ID/assets"
|
||||
|
||||
- name: Clone plugin hosting repo
|
||||
run: |
|
||||
mkdir LightlessSyncRepo
|
||||
cd LightlessSyncRepo
|
||||
git clone https://git.lightless-sync.org/${{ gitea.repository_owner }}/LightlessSync.git
|
||||
env:
|
||||
GIT_TERMINAL_PROMPT: 0
|
||||
|
||||
- name: Update plogonmaster.json with version (master)
|
||||
if: github.ref == 'refs/heads/master'
|
||||
env:
|
||||
VERSION: ${{ steps.package_version.outputs.version }}
|
||||
run: |
|
||||
set -e
|
||||
|
||||
pluginJsonPath="${PLUGIN_NAME}/bin/x64/Release/${PLUGIN_NAME}.json"
|
||||
repoJsonPath="LightlessSyncRepo/LightlessSync/plogonmaster.json"
|
||||
version="${VERSION}"
|
||||
downloadUrl="https://git.lightless-sync.org/${{ gitea.repository_owner }}/LightlessClient/releases/download/$version/LightlessClient.zip"
|
||||
|
||||
# Read plugin JSON
|
||||
pluginJson=$(cat "$pluginJsonPath")
|
||||
internalName=$(jq -r '.InternalName' <<< "$pluginJson")
|
||||
dalamudApiLevel=$(jq -r '.DalamudApiLevel' <<< "$pluginJson")
|
||||
|
||||
# Read repo JSON (force array if not already)
|
||||
repoJsonRaw=$(cat "$repoJsonPath")
|
||||
if echo "$repoJsonRaw" | jq 'type' | grep -q '"array"'; then
|
||||
repoJson="$repoJsonRaw"
|
||||
else
|
||||
repoJson="[$repoJsonRaw]"
|
||||
fi
|
||||
|
||||
# Update matching plugin entry
|
||||
updatedRepoJson=$(jq \
|
||||
--arg internalName "$internalName" \
|
||||
--arg dalamudApiLevel "$dalamudApiLevel" \
|
||||
--arg version "$version" \
|
||||
--arg downloadUrl "$downloadUrl" \
|
||||
'
|
||||
map(
|
||||
if .InternalName == $internalName
|
||||
then
|
||||
.DalamudApiLevel = $dalamudApiLevel
|
||||
| .AssemblyVersion = $version
|
||||
| .DownloadLinkInstall = $downloadUrl
|
||||
| .DownloadLinkUpdate = $downloadUrl
|
||||
else
|
||||
.
|
||||
end
|
||||
)
|
||||
' <<< "$repoJson")
|
||||
|
||||
# Write back to file
|
||||
echo "$updatedRepoJson" > "$repoJsonPath"
|
||||
# Output the content of the file
|
||||
cat "$repoJsonPath"
|
||||
|
||||
- name: Update plogonmaster.json with version (dev)
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
env:
|
||||
VERSION: ${{ steps.package_version.outputs.version }}
|
||||
run: |
|
||||
set -e
|
||||
pluginJsonPath="${PLUGIN_NAME}/bin/x64/Release/${PLUGIN_NAME}.json"
|
||||
repoJsonPath="LightlessSyncRepo/LightlessSync/plogonmaster.json"
|
||||
assemblyVersion="${VERSION}"
|
||||
version="${VERSION}-Dev"
|
||||
downloadUrl="https://git.lightless-sync.org/${{ gitea.repository_owner }}/LightlessClient/releases/download/$version/LightlessClient.zip"
|
||||
pluginJson=$(cat "$pluginJsonPath")
|
||||
internalName=$(jq -r '.InternalName' <<< "$pluginJson")
|
||||
dalamudApiLevel=$(jq -r '.DalamudApiLevel' <<< "$pluginJson")
|
||||
repoJsonRaw=$(cat "$repoJsonPath")
|
||||
if echo "$repoJsonRaw" | jq 'type' | grep -q '"array"'; then
|
||||
repoJson="$repoJsonRaw"
|
||||
else
|
||||
repoJson="[$repoJsonRaw]"
|
||||
fi
|
||||
updatedRepoJson=$(jq \
|
||||
--arg internalName "$internalName" \
|
||||
--arg dalamudApiLevel "$dalamudApiLevel" \
|
||||
--arg assemblyVersion "$assemblyVersion" \
|
||||
--arg version "$version" \
|
||||
--arg downloadUrl "$downloadUrl" \
|
||||
'
|
||||
map(
|
||||
if .InternalName == $internalName
|
||||
then
|
||||
.DalamudApiLevel = $dalamudApiLevel
|
||||
| .TestingAssemblyVersion = $assemblyVersion
|
||||
| .DownloadLinkTesting = $downloadUrl
|
||||
else
|
||||
.
|
||||
end
|
||||
)
|
||||
' <<< "$repoJson")
|
||||
echo "$updatedRepoJson" > "$repoJsonPath"
|
||||
cat "$repoJsonPath"
|
||||
|
||||
- name: Commit and push to LightlessSync
|
||||
run: |
|
||||
cd LightlessSyncRepo/LightlessSync
|
||||
git config user.name "github-actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
git add .
|
||||
git diff-index --quiet HEAD || git commit -m "Update ${{ env.PLUGIN_NAME }} to ${{ steps.package_version.outputs.version }}"
|
||||
git push https://x-access-token:${{ secrets.AUTOMATION_TOKEN }}@git.lightless-sync.org/${{ gitea.repository_owner }}/LightlessSync.git HEAD:main
|
||||
84
.github/workflows/lightless-tag-and-release.yml
vendored
84
.github/workflows/lightless-tag-and-release.yml
vendored
@@ -1,84 +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
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,6 +1,6 @@
|
||||
[submodule "LightlessAPI"]
|
||||
path = LightlessAPI
|
||||
url = https://github.com/Light-Public-Syncshells/LightlessAPI
|
||||
url = https://git.lightless-sync.org/Lightless-Sync/LightlessAPI.git
|
||||
[submodule "PenumbraAPI"]
|
||||
path = PenumbraAPI
|
||||
url = https://github.com/Ottermandias/Penumbra.Api.git
|
||||
|
||||
Submodule LightlessAPI updated: 3a69c94f7f...44fbe10458
@@ -20,7 +20,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
private long _currentFileProgress = 0;
|
||||
private CancellationTokenSource _scanCancellationTokenSource = new();
|
||||
private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
|
||||
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk"];
|
||||
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
|
||||
|
||||
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
|
||||
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
|
||||
@@ -150,7 +150,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
|
||||
private void LightlessWatcher_FileChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
Logger.LogTrace("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
|
||||
Logger.LogInformation("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
|
||||
|
||||
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||
|
||||
@@ -350,6 +350,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
|
||||
public void InvokeScan()
|
||||
{
|
||||
Logger.LogInformation("InvokeScan called");
|
||||
TotalFiles = 0;
|
||||
_currentFileProgress = 0;
|
||||
_scanCancellationTokenSource = _scanCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
|
||||
@@ -383,11 +384,12 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
scanThread.Start();
|
||||
while (scanThread.IsAlive)
|
||||
{
|
||||
await Task.Delay(250).ConfigureAwait(false);
|
||||
await Task.Delay(250, token).ConfigureAwait(false);
|
||||
}
|
||||
TotalFiles = 0;
|
||||
_currentFileProgress = 0;
|
||||
}, token);
|
||||
Logger.LogInformation("InvokeScan finished");
|
||||
}
|
||||
|
||||
public void RecalculateFileCacheSize(CancellationToken token)
|
||||
@@ -583,7 +585,14 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -612,7 +621,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
return;
|
||||
}
|
||||
|
||||
if (entitiesToUpdate.Any() || entitiesToRemove.Any())
|
||||
if (entitiesToUpdate.Count != 0 || entitiesToRemove.Count != 0)
|
||||
{
|
||||
foreach (var entity in entitiesToUpdate)
|
||||
{
|
||||
@@ -647,6 +656,12 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
CancellationToken = ct
|
||||
}, (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 (!_ipcManager.Penumbra.APIAvailable)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using K4os.Compression.LZ4.Legacy;
|
||||
using K4os.Compression.LZ4.Legacy;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
@@ -16,12 +16,15 @@ public sealed class FileCacheManager : IHostedService
|
||||
public const string CachePrefix = "{cache}";
|
||||
public const string CsvSplit = "|";
|
||||
public const string PenumbraPrefix = "{penumbra}";
|
||||
private const int FileCacheVersion = 1;
|
||||
private const string FileCacheVersionHeaderPrefix = "#lightless-file-cache-version:";
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
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 object _fileWriteLock = new();
|
||||
private readonly Lock _fileWriteLock = new();
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly ILogger<FileCacheManager> _logger;
|
||||
public string CacheFolder => _configService.Current.CacheFolder;
|
||||
@@ -37,15 +40,120 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
private string CsvBakPath => _csvPath + ".bak";
|
||||
|
||||
private static string NormalizeSeparators(string path)
|
||||
{
|
||||
return path.Replace("/", "\\", StringComparison.Ordinal)
|
||||
.Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string NormalizePrefixedPathKey(string prefixedPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(prefixedPath))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return NormalizeSeparators(prefixedPath).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static bool TryBuildPrefixedPath(string path, string? baseDirectory, string prefix, out string prefixedPath, out int matchedLength)
|
||||
{
|
||||
prefixedPath = string.Empty;
|
||||
matchedLength = 0;
|
||||
|
||||
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(baseDirectory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalizedPath = NormalizeSeparators(path).ToLowerInvariant();
|
||||
var normalizedBase = NormalizeSeparators(baseDirectory).TrimEnd('\\').ToLowerInvariant();
|
||||
|
||||
if (!normalizedPath.StartsWith(normalizedBase, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalizedPath.Length > normalizedBase.Length)
|
||||
{
|
||||
if (normalizedPath[normalizedBase.Length] != '\\')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
prefixedPath = prefix + normalizedPath.Substring(normalizedBase.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
prefixedPath = prefix;
|
||||
}
|
||||
|
||||
prefixedPath = prefixedPath.Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
matchedLength = normalizedBase.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string BuildVersionHeader() => $"{FileCacheVersionHeaderPrefix}{FileCacheVersion}";
|
||||
|
||||
private static bool TryParseVersionHeader(string? line, out int version)
|
||||
{
|
||||
version = 0;
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!line.StartsWith(FileCacheVersionHeaderPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var versionSpan = line.AsSpan(FileCacheVersionHeaderPrefix.Length);
|
||||
return int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out version);
|
||||
}
|
||||
|
||||
private string NormalizeToPrefixedPath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return string.Empty;
|
||||
|
||||
var normalized = NormalizeSeparators(path);
|
||||
|
||||
if (normalized.StartsWith(CachePrefix, StringComparison.OrdinalIgnoreCase) ||
|
||||
normalized.StartsWith(PenumbraPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return NormalizePrefixedPathKey(normalized);
|
||||
}
|
||||
|
||||
string? chosenPrefixed = null;
|
||||
var chosenLength = -1;
|
||||
|
||||
if (TryBuildPrefixedPath(normalized, _ipcManager.Penumbra.ModDirectory, PenumbraPrefix, out var penumbraPrefixed, out var penumbraMatch))
|
||||
{
|
||||
chosenPrefixed = penumbraPrefixed;
|
||||
chosenLength = penumbraMatch;
|
||||
}
|
||||
|
||||
if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch))
|
||||
{
|
||||
if (cacheMatch > chosenLength)
|
||||
{
|
||||
chosenPrefixed = cachePrefixed;
|
||||
chosenLength = cacheMatch;
|
||||
}
|
||||
}
|
||||
|
||||
return NormalizePrefixedPathKey(chosenPrefixed ?? normalized);
|
||||
}
|
||||
|
||||
public FileCacheEntity? CreateCacheEntry(string path)
|
||||
{
|
||||
FileInfo fi = new(path);
|
||||
if (!fi.Exists) return null;
|
||||
_logger.LogTrace("Creating cache entry for {path}", path);
|
||||
var fullName = fi.FullName.ToLowerInvariant();
|
||||
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);
|
||||
var cacheFolder = _configService.Current.CacheFolder;
|
||||
if (string.IsNullOrEmpty(cacheFolder)) return null;
|
||||
_logger.LogInformation("CreateCacheEntry finished for {path}", path);
|
||||
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
||||
}
|
||||
|
||||
public FileCacheEntity? CreateFileEntry(string path)
|
||||
@@ -53,26 +161,41 @@ public sealed class FileCacheManager : IHostedService
|
||||
FileInfo fi = new(path);
|
||||
if (!fi.Exists) return null;
|
||||
_logger.LogTrace("Creating file entry for {path}", path);
|
||||
var fullName = fi.FullName.ToLowerInvariant();
|
||||
if (!fullName.Contains(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), StringComparison.Ordinal)) return null;
|
||||
string prefixedPath = fullName.Replace(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
var modDirectory = _ipcManager.Penumbra.ModDirectory;
|
||||
if (string.IsNullOrEmpty(modDirectory)) return null;
|
||||
return CreateFileEntity(modDirectory, PenumbraPrefix, fi);
|
||||
}
|
||||
|
||||
private FileCacheEntity? CreateFileEntity(string directory, string prefix, FileInfo fi)
|
||||
{
|
||||
if (!TryBuildPrefixedPath(fi.FullName, directory, prefix, out var prefixedPath, out _))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
List<FileCacheEntity> output = [];
|
||||
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
|
||||
{
|
||||
var validated = GetValidatedFileCache(fileCache);
|
||||
if (validated != null) output.Add(validated);
|
||||
if (validated != null)
|
||||
{
|
||||
output.Add(validated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +207,7 @@ public sealed class FileCacheManager : IHostedService
|
||||
{
|
||||
_lightlessMediator.Publish(new HaltScanMessage(nameof(ValidateLocalIntegrity)));
|
||||
_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 = [];
|
||||
int i = 0;
|
||||
foreach (var fileCache in cacheEntries)
|
||||
@@ -106,7 +229,7 @@ public sealed class FileCacheManager : IHostedService
|
||||
var computedHash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -149,87 +272,128 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
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();
|
||||
if (item != null) return GetValidatedFileCache(item);
|
||||
var item = entries.Values
|
||||
.OrderBy(p => p.PrefixedFilePath.Contains(PenumbraPrefix, StringComparison.Ordinal) ? 0 : 1)
|
||||
.FirstOrDefault();
|
||||
if (item != null)
|
||||
{
|
||||
return GetValidatedFileCache(item);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private FileCacheEntity? GetFileCacheByPath(string path)
|
||||
{
|
||||
var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant()
|
||||
.Replace(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase);
|
||||
var entry = _fileCaches.SelectMany(v => v.Value).FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (entry == null)
|
||||
var normalizedPrefixedPath = NormalizeToPrefixedPath(path);
|
||||
if (string.IsNullOrEmpty(normalizedPrefixedPath))
|
||||
{
|
||||
_logger.LogDebug("Found no entries for {path}", cleanedPath);
|
||||
return CreateFileEntry(path);
|
||||
return null;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
_logger.LogInformation("GetFileCachesByPaths called for {count} paths", paths.Length);
|
||||
|
||||
_getCachesByPathsSemaphore.Wait();
|
||||
|
||||
try
|
||||
{
|
||||
var cleanedPaths = paths.Distinct(StringComparer.OrdinalIgnoreCase).ToDictionary(p => p,
|
||||
p => p.Replace("/", "\\", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(_ipcManager.Penumbra.ModDirectory!, _ipcManager.Penumbra.ModDirectory!.EndsWith('\\') ? PenumbraPrefix + '\\' : PenumbraPrefix, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(_configService.Current.CacheFolder, _configService.Current.CacheFolder.EndsWith('\\') ? CachePrefix + '\\' : CachePrefix, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("\\\\", "\\", StringComparison.Ordinal),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
var result = new Dictionary<string, FileCacheEntity?>(StringComparer.OrdinalIgnoreCase);
|
||||
var seenNormalized = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
Dictionary<string, FileCacheEntity?> result = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var dict = _fileCaches.SelectMany(f => f.Value)
|
||||
.ToDictionary(d => d.PrefixedFilePath, d => d, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var entry in cleanedPaths)
|
||||
foreach (var originalPath in paths)
|
||||
{
|
||||
//_logger.LogDebug("Checking {path}", entry.Value);
|
||||
|
||||
if (dict.TryGetValue(entry.Value, out var entity))
|
||||
if (string.IsNullOrEmpty(originalPath))
|
||||
{
|
||||
var validatedCache = GetValidatedFileCache(entity);
|
||||
result.Add(entry.Key, validatedCache);
|
||||
result[originalPath] = null;
|
||||
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
|
||||
{
|
||||
if (!entry.Value.Contains(CachePrefix, StringComparison.Ordinal))
|
||||
result.Add(entry.Key, CreateFileEntry(entry.Key));
|
||||
else
|
||||
result.Add(entry.Key, CreateCacheEntry(entry.Key));
|
||||
created = CreateFileEntry(originalPath) ?? CreateCacheEntry(originalPath);
|
||||
}
|
||||
|
||||
result[originalPath] = created;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
_logger.LogInformation("GetFileCachesByPaths finished for {count} paths", paths.Length);
|
||||
_getCachesByPathsSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveHashedFile(string hash, string prefixedFilePath)
|
||||
{
|
||||
var normalizedPath = NormalizePrefixedPathKey(prefixedFilePath);
|
||||
|
||||
if (_fileCaches.TryGetValue(hash, out var caches))
|
||||
{
|
||||
var removedCount = caches?.RemoveAll(c => string.Equals(c.PrefixedFilePath, prefixedFilePath, StringComparison.Ordinal));
|
||||
_logger.LogTrace("Removed from DB: {count} file(s) with hash {hash} and file cache {path}", removedCount, hash, prefixedFilePath);
|
||||
_logger.LogTrace("Removing from DB: {hash} => {path}", 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)
|
||||
@@ -270,7 +434,8 @@ public sealed class FileCacheManager : IHostedService
|
||||
lock (_fileWriteLock)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
foreach (var entry in _fileCaches.SelectMany(k => k.Value).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
||||
sb.AppendLine(BuildVersionHeader());
|
||||
foreach (var entry in _fileCaches.Values.SelectMany(k => k.Values).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
sb.AppendLine(entry.CsvEntry);
|
||||
}
|
||||
@@ -292,6 +457,53 @@ public sealed class FileCacheManager : IHostedService
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureCsvHeaderLocked()
|
||||
{
|
||||
if (!File.Exists(_csvPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string[] existingLines = File.ReadAllLines(_csvPath);
|
||||
if (existingLines.Length > 0 && TryParseVersionHeader(existingLines[0], out var existingVersion) && existingVersion == FileCacheVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder rebuilt = new();
|
||||
rebuilt.AppendLine(BuildVersionHeader());
|
||||
foreach (var line in existingLines)
|
||||
{
|
||||
if (TryParseVersionHeader(line, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(line))
|
||||
{
|
||||
rebuilt.AppendLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllText(_csvPath, rebuilt.ToString());
|
||||
}
|
||||
|
||||
private void BackupUnsupportedCache(string suffix)
|
||||
{
|
||||
var sanitizedSuffix = string.IsNullOrWhiteSpace(suffix) ? "unsupported" : $"{suffix}.unsupported";
|
||||
var backupPath = _csvPath + "." + sanitizedSuffix;
|
||||
|
||||
try
|
||||
{
|
||||
File.Move(_csvPath, backupPath, overwrite: true);
|
||||
_logger.LogWarning("Backed up unsupported file cache to {path}", backupPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to back up unsupported file cache to {path}", backupPath);
|
||||
}
|
||||
}
|
||||
|
||||
internal FileCacheEntity MigrateFileHashToExtension(FileCacheEntity fileCache, string ext)
|
||||
{
|
||||
try
|
||||
@@ -315,16 +527,11 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
private void AddHashedFile(FileCacheEntity fileCache)
|
||||
{
|
||||
if (!_fileCaches.TryGetValue(fileCache.Hash, out var entries) || entries is null)
|
||||
{
|
||||
_fileCaches[fileCache.Hash] = entries = [];
|
||||
}
|
||||
var normalizedPath = NormalizePrefixedPathKey(fileCache.PrefixedFilePath);
|
||||
var entries = _fileCaches.GetOrAdd(fileCache.Hash, _ => new ConcurrentDictionary<string, FileCacheEntity>(StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
if (!entries.Exists(u => string.Equals(u.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
//_logger.LogTrace("Adding to DB: {hash} => {path}", fileCache.Hash, fileCache.PrefixedFilePath);
|
||||
entries.Add(fileCache);
|
||||
}
|
||||
entries[normalizedPath] = fileCache;
|
||||
_fileCachesByPrefixedPath[normalizedPath] = fileCache;
|
||||
}
|
||||
|
||||
private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null)
|
||||
@@ -335,7 +542,15 @@ public sealed class FileCacheManager : IHostedService
|
||||
AddHashedFile(entity);
|
||||
lock (_fileWriteLock)
|
||||
{
|
||||
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
||||
if (!File.Exists(_csvPath))
|
||||
{
|
||||
File.WriteAllLines(_csvPath, new[] { BuildVersionHeader(), entity.CsvEntry });
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureCsvHeaderLocked();
|
||||
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
||||
}
|
||||
}
|
||||
var result = GetFileCacheByPath(fileInfo.FullName);
|
||||
_logger.LogTrace("Creating cache entity for {name} success: {success}", fileInfo.FullName, (result != null));
|
||||
@@ -366,6 +581,12 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
private FileCacheEntity? Validate(FileCacheEntity fileCache)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileCache.ResolvedFilepath))
|
||||
{
|
||||
_logger.LogWarning("FileCacheEntity has empty ResolvedFilepath for hash {hash}, prefixed path {prefixed}", fileCache.Hash, fileCache.PrefixedFilePath);
|
||||
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
|
||||
return null;
|
||||
}
|
||||
var file = new FileInfo(fileCache.ResolvedFilepath);
|
||||
if (!file.Exists)
|
||||
{
|
||||
@@ -439,7 +660,7 @@ public sealed class FileCacheManager : IHostedService
|
||||
{
|
||||
attempts++;
|
||||
_logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath);
|
||||
Thread.Sleep(100);
|
||||
Task.Delay(100, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,49 +669,111 @@ public sealed class FileCacheManager : IHostedService
|
||||
_logger.LogWarning("Could not load entries from {path}, continuing with empty file cache", _csvPath);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Found {amount} files in {path}", entries.Length, _csvPath);
|
||||
bool rewriteRequired = false;
|
||||
bool parseEntries = entries.Length > 0;
|
||||
int startIndex = 0;
|
||||
|
||||
Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var entry in entries)
|
||||
if (entries.Length > 0)
|
||||
{
|
||||
var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None);
|
||||
try
|
||||
var headerLine = entries[0];
|
||||
var hasHeader = !string.IsNullOrEmpty(headerLine) &&
|
||||
headerLine.StartsWith(FileCacheVersionHeaderPrefix, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (hasHeader)
|
||||
{
|
||||
var hash = splittedEntry[0];
|
||||
if (hash.Length != 40) throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
|
||||
var path = splittedEntry[1];
|
||||
var time = splittedEntry[2];
|
||||
|
||||
if (processedFiles.ContainsKey(path))
|
||||
if (!TryParseVersionHeader(headerLine, out var parsedVersion))
|
||||
{
|
||||
_logger.LogWarning("Already processed {file}, ignoring", path);
|
||||
continue;
|
||||
_logger.LogWarning("Failed to parse file cache version header \"{header}\". Backing up existing cache.", headerLine);
|
||||
BackupUnsupportedCache("invalid-version");
|
||||
parseEntries = false;
|
||||
rewriteRequired = true;
|
||||
entries = Array.Empty<string>();
|
||||
}
|
||||
|
||||
processedFiles.Add(path, value: true);
|
||||
|
||||
long size = -1;
|
||||
long compressed = -1;
|
||||
if (splittedEntry.Length > 3)
|
||||
else if (parsedVersion != FileCacheVersion)
|
||||
{
|
||||
if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result))
|
||||
{
|
||||
size = result;
|
||||
}
|
||||
if (long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
|
||||
{
|
||||
compressed = resultCompressed;
|
||||
}
|
||||
_logger.LogWarning("Unsupported file cache version {version} detected (expected {expected}). Backing up existing cache.", parsedVersion, FileCacheVersion);
|
||||
BackupUnsupportedCache($"v{parsedVersion}");
|
||||
parseEntries = false;
|
||||
rewriteRequired = true;
|
||||
entries = Array.Empty<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
startIndex = 1;
|
||||
}
|
||||
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
else if (entries.Length > 0)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to initialize entry {entry}, ignoring", entry);
|
||||
_logger.LogInformation("File cache missing version header, scheduling rewrite.");
|
||||
rewriteRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (processedFiles.Count != entries.Length)
|
||||
var totalEntries = Math.Max(0, entries.Length - startIndex);
|
||||
Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (parseEntries && totalEntries > 0)
|
||||
{
|
||||
_logger.LogInformation("Found {amount} files in {path}", totalEntries, _csvPath);
|
||||
|
||||
for (var index = startIndex; index < entries.Length; index++)
|
||||
{
|
||||
var entry = entries[index];
|
||||
if (string.IsNullOrWhiteSpace(entry))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None);
|
||||
try
|
||||
{
|
||||
var hash = splittedEntry[0];
|
||||
if (hash.Length != 40)
|
||||
throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
|
||||
var path = splittedEntry[1];
|
||||
var time = splittedEntry[2];
|
||||
|
||||
if (processedFiles.ContainsKey(path))
|
||||
{
|
||||
_logger.LogWarning("Already processed {file}, ignoring", path);
|
||||
continue;
|
||||
}
|
||||
|
||||
processedFiles.Add(path, value: true);
|
||||
|
||||
long size = -1;
|
||||
long compressed = -1;
|
||||
if (splittedEntry.Length > 3)
|
||||
{
|
||||
if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result))
|
||||
{
|
||||
size = result;
|
||||
}
|
||||
if (splittedEntry.Length > 4 &&
|
||||
long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
|
||||
{
|
||||
compressed = resultCompressed;
|
||||
}
|
||||
}
|
||||
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to initialize entry {entry}, ignoring", entry);
|
||||
}
|
||||
}
|
||||
|
||||
if (processedFiles.Count != totalEntries)
|
||||
{
|
||||
rewriteRequired = true;
|
||||
}
|
||||
}
|
||||
else if (!parseEntries && entries.Length > 0)
|
||||
{
|
||||
_logger.LogInformation("Skipping existing file cache entries due to incompatible version.");
|
||||
}
|
||||
|
||||
if (rewriteRequired)
|
||||
{
|
||||
WriteOutFullCsv();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
private readonly HashSet<string> _cachedHandledPaths = new(StringComparer.Ordinal);
|
||||
private readonly TransientConfigService _configurationService;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk"];
|
||||
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "kdb"];
|
||||
private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
|
||||
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
|
||||
private ConcurrentDictionary<IntPtr, ObjectKind> _cachedFrameAddresses = [];
|
||||
|
||||
@@ -27,9 +27,9 @@ public sealed class IpcCallerMoodles : IIpcCaller
|
||||
|
||||
_moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version");
|
||||
_moodlesOnChange = pi.GetIpcSubscriber<IPlayerCharacter, object>("Moodles.StatusManagerModified");
|
||||
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtr");
|
||||
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtr");
|
||||
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtr");
|
||||
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtrV2");
|
||||
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtrV2");
|
||||
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtrV2");
|
||||
|
||||
_moodlesOnChange.Subscribe(OnMoodlesChange);
|
||||
|
||||
@@ -47,7 +47,7 @@ public sealed class IpcCallerMoodles : IIpcCaller
|
||||
{
|
||||
try
|
||||
{
|
||||
APIAvailable = _moodlesApiVersion.InvokeFunc() == 1;
|
||||
APIAvailable = _moodlesApiVersion.InvokeFunc() == 3;
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -30,12 +30,12 @@ public sealed class IpcCallerPetNames : IIpcCaller
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
|
||||
_petnamesReady = pi.GetIpcSubscriber<object>("PetRenamer.Ready");
|
||||
_petnamesDisposing = pi.GetIpcSubscriber<object>("PetRenamer.Disposing");
|
||||
_petnamesReady = pi.GetIpcSubscriber<object>("PetRenamer.OnReady");
|
||||
_petnamesDisposing = pi.GetIpcSubscriber<object>("PetRenamer.OnDisposing");
|
||||
_apiVersion = pi.GetIpcSubscriber<(uint, uint)>("PetRenamer.ApiVersion");
|
||||
_enabled = pi.GetIpcSubscriber<bool>("PetRenamer.Enabled");
|
||||
_enabled = pi.GetIpcSubscriber<bool>("PetRenamer.IsEnabled");
|
||||
|
||||
_playerDataChanged = pi.GetIpcSubscriber<string, object>("PetRenamer.PlayerDataChanged");
|
||||
_playerDataChanged = pi.GetIpcSubscriber<string, object>("PetRenamer.OnPlayerDataChanged");
|
||||
_getPlayerData = pi.GetIpcSubscriber<string>("PetRenamer.GetPlayerData");
|
||||
_setPlayerData = pi.GetIpcSubscriber<string, object>("PetRenamer.SetPlayerData");
|
||||
_clearPlayerData = pi.GetIpcSubscriber<ushort, object>("PetRenamer.ClearPlayerData");
|
||||
@@ -56,7 +56,7 @@ public sealed class IpcCallerPetNames : IIpcCaller
|
||||
APIAvailable = _enabled?.InvokeFunc() ?? false;
|
||||
if (APIAvailable)
|
||||
{
|
||||
APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 3, Item2: >= 1 };
|
||||
APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 4, Item2: >= 0 };
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -84,7 +84,7 @@ public sealed class IpcCallerPetNames : IIpcCaller
|
||||
{
|
||||
string localNameData = _getPlayerData.InvokeFunc();
|
||||
return string.IsNullOrEmpty(localNameData) ? string.Empty : localNameData;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Could not obtain Pet Nicknames data");
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Reflection;
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
public enum LightfinderDtrDisplayMode
|
||||
{
|
||||
NearbyBroadcasts = 0,
|
||||
PendingPairRequests = 1,
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using Dalamud.Game.Text;
|
||||
using LightlessSync.UtilsEnum.Enum;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.UI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
[Serializable]
|
||||
public class LightlessConfig : ILightlessConfiguration
|
||||
{
|
||||
public bool AcceptedAgreement { get; set; } = false;
|
||||
public string CacheFolder { get; set; } = string.Empty;
|
||||
public bool DisableOptionalPluginWarnings { get; set; } = false;
|
||||
public bool EnableDtrEntry { get; set; } = false;
|
||||
public bool ShowUidInDtrTooltip { get; set; } = true;
|
||||
public bool PreferNoteInDtrTooltip { get; set; } = false;
|
||||
public bool IsNameplateColorsEnabled { get; set; } = false;
|
||||
public DtrEntry.Colors NameplateColors { get; set; } = new(Foreground: 0xE69138u, Glow: 0xFFBA47u);
|
||||
public Dictionary<string, string> CustomUIColors { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public bool UseColorsInDtr { get; set; } = true;
|
||||
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
||||
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
||||
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u);
|
||||
public bool ShowLightfinderInDtr { get; set; } = false;
|
||||
public bool UseLightfinderColorsInDtr { get; set; } = true;
|
||||
public DtrEntry.Colors DtrColorsLightfinderEnabled { get; set; } = new(Foreground: 0xB590FFu, Glow: 0x4F406Eu);
|
||||
public DtrEntry.Colors DtrColorsLightfinderDisabled { get; set; } = new(Foreground: 0xD44444u, Glow: 0x642222u);
|
||||
public DtrEntry.Colors DtrColorsLightfinderCooldown { get; set; } = new(Foreground: 0xFFE97Au, Glow: 0x766C3Au);
|
||||
public DtrEntry.Colors DtrColorsLightfinderUnavailable { get; set; } = new(Foreground: 0x000000u, Glow: 0x000000u);
|
||||
public LightfinderDtrDisplayMode LightfinderDtrDisplayMode { get; set; } = LightfinderDtrDisplayMode.PendingPairRequests;
|
||||
public bool UseLightlessRedesign { get; set; } = true;
|
||||
public bool EnableRightClickMenus { get; set; } = true;
|
||||
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
||||
public string ExportFolder { get; set; } = string.Empty;
|
||||
public bool FileScanPaused { get; set; } = false;
|
||||
public NotificationLocation InfoNotification { get; set; } = NotificationLocation.Toast;
|
||||
public bool InitialScanComplete { get; set; } = false;
|
||||
public LogLevel LogLevel { get; set; } = LogLevel.Information;
|
||||
public bool LogPerformance { get; set; } = false;
|
||||
public double MaxLocalCacheInGiB { get; set; } = 20;
|
||||
public bool OpenGposeImportOnGposeStart { get; set; } = false;
|
||||
public bool OpenPopupOnAdd { get; set; } = true;
|
||||
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 DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
||||
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||
public float ProfileDelay { get; set; } = 1.5f;
|
||||
public bool ProfilePopoutRight { get; set; } = false;
|
||||
public bool ProfilesAllowNsfw { get; set; } = false;
|
||||
public bool ProfilesShow { get; set; } = true;
|
||||
public bool ShowSyncshellUsersInVisible { get; set; } = true;
|
||||
public bool ShowCharacterNameInsteadOfNotesForVisible { get; set; } = false;
|
||||
public bool ShowOfflineUsersSeparately { get; set; } = true;
|
||||
public bool ShowSyncshellOfflineUsersSeparately { get; set; } = true;
|
||||
public bool ShowGroupedSyncshellsInAll { get; set; } = true;
|
||||
public bool GroupUpSyncshells { get; set; } = true;
|
||||
public bool ShowOnlineNotifications { get; set; } = false;
|
||||
public bool ShowOnlineNotificationsOnlyForIndividualPairs { get; set; } = true;
|
||||
public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false;
|
||||
public bool ShowTransferBars { get; set; } = true;
|
||||
public bool ShowTransferWindow { get; set; } = false;
|
||||
public bool UseNotificationsForDownloads { get; set; } = true;
|
||||
public bool ShowUploading { get; set; } = true;
|
||||
public bool ShowUploadingBigText { get; set; } = true;
|
||||
public bool ShowVisibleUsersSeparately { get; set; } = true;
|
||||
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
||||
public int TransferBarsHeight { get; set; } = 12;
|
||||
public bool TransferBarsShowText { get; set; } = true;
|
||||
public int TransferBarsWidth { get; set; } = 250;
|
||||
public bool UseAlternativeFileUpload { get; set; } = false;
|
||||
public bool UseCompactor { get; set; } = false;
|
||||
public bool DebugStopWhining { get; set; } = false;
|
||||
public bool AutoPopulateEmptyNotesFromCharaName { get; set; } = false;
|
||||
public int Version { get; set; } = 1;
|
||||
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both;
|
||||
|
||||
// Lightless Notification Configuration
|
||||
public bool UseLightlessNotifications { get; set; } = true;
|
||||
public bool ShowNotificationProgress { get; set; } = true;
|
||||
public NotificationLocation LightlessInfoNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||
public NotificationLocation LightlessWarningNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||
public NotificationLocation LightlessErrorNotification { get; set; } = NotificationLocation.ChatAndLightlessUi;
|
||||
public NotificationLocation LightlessPairRequestNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||
public NotificationLocation LightlessDownloadNotification { get; set; } = NotificationLocation.TextOverlay;
|
||||
|
||||
// Basic Settings
|
||||
public float NotificationOpacity { get; set; } = 0.95f;
|
||||
public int MaxSimultaneousNotifications { get; set; } = 5;
|
||||
public bool AutoDismissOnAction { get; set; } = true;
|
||||
public bool DismissNotificationOnClick { get; set; } = false;
|
||||
public bool ShowNotificationTimestamp { get; set; } = false;
|
||||
|
||||
// Position & Layout
|
||||
public NotificationCorner NotificationCorner { get; set; } = NotificationCorner.Right;
|
||||
public int NotificationOffsetY { get; set; } = 50;
|
||||
public int NotificationOffsetX { get; set; } = 0;
|
||||
public float NotificationWidth { get; set; } = 350f;
|
||||
public float NotificationSpacing { get; set; } = 8f;
|
||||
|
||||
// Animation & Effects
|
||||
public float NotificationAnimationSpeed { get; set; } = 10f;
|
||||
public float NotificationSlideSpeed { get; set; } = 10f;
|
||||
public float NotificationAccentBarWidth { get; set; } = 3f;
|
||||
|
||||
// Duration per Type
|
||||
public int InfoNotificationDurationSeconds { get; set; } = 10;
|
||||
public int WarningNotificationDurationSeconds { get; set; } = 15;
|
||||
public int ErrorNotificationDurationSeconds { get; set; } = 20;
|
||||
public int PairRequestDurationSeconds { get; set; } = 180;
|
||||
public int DownloadNotificationDurationSeconds { get; set; } = 300;
|
||||
public uint CustomInfoSoundId { get; set; } = 2; // Se2
|
||||
public uint CustomWarningSoundId { get; set; } = 16; // Se15
|
||||
public uint CustomErrorSoundId { get; set; } = 16; // Se15
|
||||
public uint PairRequestSoundId { get; set; } = 5; // Se5
|
||||
public uint DownloadSoundId { get; set; } = 15; // Se14
|
||||
public bool DisableInfoSound { get; set; } = true;
|
||||
public bool DisableWarningSound { get; set; } = true;
|
||||
public bool DisableErrorSound { get; set; } = true;
|
||||
public bool DisablePairRequestSound { get; set; } = true;
|
||||
public bool DisableDownloadSound { get; set; } = true;
|
||||
public bool UseFocusTarget { get; set; } = false;
|
||||
public bool overrideFriendColor { get; set; } = false;
|
||||
public bool overridePartyColor { get; set; } = false;
|
||||
public bool overrideFcTagColor { get; set; } = false;
|
||||
public bool useColoredUIDs { get; set; } = true;
|
||||
public bool BroadcastEnabled { get; set; } = false;
|
||||
public bool LightfinderAutoEnableOnConnect { get; set; } = false;
|
||||
public short LightfinderLabelOffsetX { get; set; } = 0;
|
||||
public short LightfinderLabelOffsetY { get; set; } = 0;
|
||||
public bool LightfinderLabelUseIcon { get; set; } = false;
|
||||
public bool LightfinderLabelShowOwn { get; set; } = true;
|
||||
public bool LightfinderLabelShowPaired { get; set; } = true;
|
||||
public bool LightfinderLabelShowHidden { get; set; } = false;
|
||||
public string LightfinderLabelIconGlyph { get; set; } = SeIconCharExtensions.ToIconString(SeIconChar.Hyadelyn);
|
||||
public float LightfinderLabelScale { get; set; } = 1.0f;
|
||||
public bool LightfinderAutoAlign { get; set; } = true;
|
||||
public LabelAlignment LabelAlignment { get; set; } = LabelAlignment.Left;
|
||||
public DateTime BroadcastTtl { get; set; } = DateTime.MinValue;
|
||||
public bool SyncshellFinderEnabled { get; set; } = false;
|
||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
public class PairTagStorage : ILightlessConfiguration
|
||||
{
|
||||
public Dictionary<string, Models.PairTagStorage> ServerTagStorage { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public int Version { get; set; } = 0;
|
||||
}
|
||||
@@ -13,4 +13,7 @@ public class PlayerPerformanceConfig : ILightlessConfiguration
|
||||
public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 550;
|
||||
public int TrisAutoPauseThresholdThousands { get; set; } = 250;
|
||||
public List<string> UIDsToIgnore { get; set; } = new();
|
||||
public bool PauseInInstanceDuty { get; set; } = false;
|
||||
public bool PauseWhilePerforming { get; set; } = true;
|
||||
public bool PauseInCombat { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
public class SyncshellTagStorage : ILightlessConfiguration
|
||||
{
|
||||
public Dictionary<string, Models.SyncshellTagStorage> ServerTagStorage { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public int Version { get; set; } = 0;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
[Serializable]
|
||||
public class UiStyleOverride
|
||||
{
|
||||
public uint? Color { get; set; }
|
||||
public float? Float { get; set; }
|
||||
public Vector2Config? Vector2 { get; set; }
|
||||
|
||||
public bool IsEmpty => Color is null && Float is null && Vector2 is null;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public record struct Vector2Config(float X, float Y)
|
||||
{
|
||||
public static implicit operator Vector2(Vector2Config value) => new(value.X, value.Y);
|
||||
public static implicit operator Vector2Config(Vector2 value) => new(value.X, value.Y);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
[Serializable]
|
||||
public class UiThemeConfig : ILightlessConfiguration
|
||||
{
|
||||
public Dictionary<string, UiStyleOverride> StyleOverrides { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public int Version { get; set; } = 1;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Models;
|
||||
|
||||
public enum NotificationLocation
|
||||
{
|
||||
Nowhere,
|
||||
Chat,
|
||||
Toast,
|
||||
Both,
|
||||
LightlessUi,
|
||||
ChatAndLightlessUi,
|
||||
TextOverlay,
|
||||
}
|
||||
|
||||
public enum NotificationType
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
PairRequest,
|
||||
Download
|
||||
}
|
||||
|
||||
public enum NotificationCorner
|
||||
{
|
||||
Right,
|
||||
Left
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Models;
|
||||
|
||||
[Serializable]
|
||||
public class PairTagStorage
|
||||
{
|
||||
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,8 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Models;
|
||||
|
||||
[Serializable]
|
||||
public class SyncshellTagStorage
|
||||
{
|
||||
public HashSet<string> ServerAvailableSyncshellTags { get; set; } = new(StringComparer.Ordinal);
|
||||
public Dictionary<string, List<string>> SyncshellPairedTags { get; set; } = new(StringComparer.Ordinal);
|
||||
}
|
||||
14
LightlessSync/LightlessConfiguration/PairTagConfigService.cs
Normal file
14
LightlessSync/LightlessConfiguration/PairTagConfigService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration;
|
||||
|
||||
public class PairTagConfigService : ConfigurationServiceBase<PairTagStorage>
|
||||
{
|
||||
public const string ConfigName = "servertags.json";
|
||||
|
||||
public PairTagConfigService(string configDir) : base(configDir)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ConfigurationName => ConfigName;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration;
|
||||
|
||||
public class SyncshellTagConfigService : ConfigurationServiceBase<SyncshellTagStorage>
|
||||
{
|
||||
public const string ConfigName = "syncshelltags.json";
|
||||
|
||||
public SyncshellTagConfigService(string configDir) : base(configDir)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ConfigurationName => ConfigName;
|
||||
}
|
||||
14
LightlessSync/LightlessConfiguration/UiThemeConfigService.cs
Normal file
14
LightlessSync/LightlessConfiguration/UiThemeConfigService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration;
|
||||
|
||||
public class UiThemeConfigService : ConfigurationServiceBase<UiThemeConfig>
|
||||
{
|
||||
public const string ConfigName = "ui-theme.json";
|
||||
|
||||
public UiThemeConfigService(string configDir) : base(configDir)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ConfigurationName => ConfigName;
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.PlayerData.Services;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -98,6 +99,7 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
|
||||
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut());
|
||||
|
||||
UIColors.Initialize(_lightlessConfigService);
|
||||
Mediator.StartQueueProcessing();
|
||||
|
||||
return Task.CompletedTask;
|
||||
@@ -151,6 +153,7 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>();
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<VisibleUserDataDistributor>();
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<NameplateService>();
|
||||
|
||||
#if !DEBUG
|
||||
if (_lightlessConfigService.Current.LogLevel != LogLevel.Information)
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Dalamud.NET.Sdk/13.0.0">
|
||||
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
|
||||
<PropertyGroup>
|
||||
<Authors></Authors>
|
||||
<Company></Company>
|
||||
<Version>1.11.2</Version>
|
||||
<Version>1.12.3</Version>
|
||||
<Description></Description>
|
||||
<Copyright></Copyright>
|
||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.UI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
[Serializable]
|
||||
public class LightlessConfig : ILightlessConfiguration
|
||||
{
|
||||
public bool AcceptedAgreement { get; set; } = false;
|
||||
public string CacheFolder { get; set; } = string.Empty;
|
||||
public bool DisableOptionalPluginWarnings { get; set; } = false;
|
||||
public bool EnableDtrEntry { get; set; } = false;
|
||||
public bool ShowUidInDtrTooltip { get; set; } = true;
|
||||
public bool PreferNoteInDtrTooltip { get; set; } = false;
|
||||
public bool UseColorsInDtr { get; set; } = true;
|
||||
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
||||
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
||||
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u);
|
||||
public bool EnableRightClickMenus { get; set; } = true;
|
||||
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
||||
public string ExportFolder { get; set; } = string.Empty;
|
||||
public bool FileScanPaused { get; set; } = false;
|
||||
public NotificationLocation InfoNotification { get; set; } = NotificationLocation.Toast;
|
||||
public bool InitialScanComplete { get; set; } = false;
|
||||
public LogLevel LogLevel { get; set; } = LogLevel.Information;
|
||||
public bool LogPerformance { get; set; } = false;
|
||||
public double MaxLocalCacheInGiB { get; set; } = 20;
|
||||
public bool OpenGposeImportOnGposeStart { get; set; } = false;
|
||||
public bool OpenPopupOnAdd { get; set; } = true;
|
||||
public int ParallelDownloads { get; set; } = 10;
|
||||
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
||||
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
||||
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||
public float ProfileDelay { get; set; } = 1.5f;
|
||||
public bool ProfilePopoutRight { get; set; } = false;
|
||||
public bool ProfilesAllowNsfw { get; set; } = false;
|
||||
public bool ProfilesShow { get; set; } = true;
|
||||
public bool ShowSyncshellUsersInVisible { get; set; } = true;
|
||||
public bool ShowCharacterNameInsteadOfNotesForVisible { get; set; } = false;
|
||||
public bool ShowOfflineUsersSeparately { get; set; } = true;
|
||||
public bool ShowSyncshellOfflineUsersSeparately { get; set; } = true;
|
||||
public bool GroupUpSyncshells { get; set; } = true;
|
||||
public bool ShowOnlineNotifications { get; set; } = false;
|
||||
public bool ShowOnlineNotificationsOnlyForIndividualPairs { get; set; } = true;
|
||||
public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false;
|
||||
public bool ShowTransferBars { get; set; } = true;
|
||||
public bool ShowTransferWindow { get; set; } = false;
|
||||
public bool ShowUploading { get; set; } = true;
|
||||
public bool ShowUploadingBigText { get; set; } = true;
|
||||
public bool ShowVisibleUsersSeparately { get; set; } = true;
|
||||
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
||||
public int TransferBarsHeight { get; set; } = 12;
|
||||
public bool TransferBarsShowText { get; set; } = true;
|
||||
public int TransferBarsWidth { get; set; } = 250;
|
||||
public bool UseAlternativeFileUpload { get; set; } = false;
|
||||
public bool UseCompactor { get; set; } = false;
|
||||
public bool DebugStopWhining { get; set; } = false;
|
||||
public bool AutoPopulateEmptyNotesFromCharaName { get; set; } = false;
|
||||
public int Version { get; set; } = 1;
|
||||
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both;
|
||||
public bool UseFocusTarget { get; set; } = false;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Models;
|
||||
|
||||
public enum NotificationLocation
|
||||
{
|
||||
Nowhere,
|
||||
Chat,
|
||||
Toast,
|
||||
Both
|
||||
}
|
||||
|
||||
public enum NotificationType
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
@@ -21,6 +21,7 @@ public class PairHandlerFactory
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||
private readonly ServerConfigurationManager _serverConfigManager;
|
||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||
|
||||
@@ -28,6 +29,7 @@ public class PairHandlerFactory
|
||||
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
||||
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
||||
FileCacheManager fileCacheManager, LightlessMediator lightlessMediator, PlayerPerformanceService playerPerformanceService,
|
||||
PairProcessingLimiter pairProcessingLimiter,
|
||||
ServerConfigurationManager serverConfigManager)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
@@ -40,6 +42,7 @@ public class PairHandlerFactory
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
_pairProcessingLimiter = pairProcessingLimiter;
|
||||
_serverConfigManager = serverConfigManager;
|
||||
}
|
||||
|
||||
@@ -47,6 +50,6 @@ public class PairHandlerFactory
|
||||
{
|
||||
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _gameObjectHandlerFactory,
|
||||
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
||||
_fileCacheManager, _lightlessMediator, _playerPerformanceService, _serverConfigManager);
|
||||
_fileCacheManager, _lightlessMediator, _playerPerformanceService, _pairProcessingLimiter, _serverConfigManager);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using CharacterData = LightlessSync.PlayerData.Data.CharacterData;
|
||||
|
||||
namespace LightlessSync.PlayerData.Factories;
|
||||
|
||||
@@ -99,7 +98,19 @@ public class PlayerDataFactory
|
||||
|
||||
private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
|
||||
{
|
||||
return ((Character*)playerPointer)->GameObject.DrawObject == null;
|
||||
if (playerPointer == IntPtr.Zero)
|
||||
return true;
|
||||
|
||||
var character = (Character*)playerPointer;
|
||||
|
||||
if (character == null)
|
||||
return true;
|
||||
|
||||
var gameObject = &character->GameObject;
|
||||
if (gameObject == null)
|
||||
return true;
|
||||
|
||||
return gameObject->DrawObject == null;
|
||||
}
|
||||
|
||||
private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
@@ -28,6 +28,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly IHostApplicationLifetime _lifetime;
|
||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||
private readonly ServerConfigurationManager _serverConfigManager;
|
||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
||||
@@ -50,6 +51,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
||||
FileCacheManager fileDbManager, LightlessMediator mediator,
|
||||
PlayerPerformanceService playerPerformanceService,
|
||||
PairProcessingLimiter pairProcessingLimiter,
|
||||
ServerConfigurationManager serverConfigManager) : base(logger, mediator)
|
||||
{
|
||||
Pair = pair;
|
||||
@@ -61,6 +63,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
_lifetime = lifetime;
|
||||
_fileDbManager = fileDbManager;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
_pairProcessingLimiter = pairProcessingLimiter;
|
||||
_serverConfigManager = serverConfigManager;
|
||||
_penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, Pair.UserData.UID).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
|
||||
@@ -88,20 +91,30 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
_redrawOnNextApplication = true;
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<CombatOrPerformanceEndMessage>(this, (msg) =>
|
||||
Mediator.Subscribe<CombatEndMessage>(this, (msg) =>
|
||||
{
|
||||
if (IsVisible && _dataReceivedInDowntime != null)
|
||||
{
|
||||
ApplyCharacterData(_dataReceivedInDowntime.ApplicationId,
|
||||
_dataReceivedInDowntime.CharacterData, _dataReceivedInDowntime.Forced);
|
||||
_dataReceivedInDowntime = null;
|
||||
}
|
||||
EnableSync();
|
||||
});
|
||||
Mediator.Subscribe<CombatOrPerformanceStartMessage>(this, _ =>
|
||||
Mediator.Subscribe<CombatStartMessage>(this, _ =>
|
||||
{
|
||||
_dataReceivedInDowntime = null;
|
||||
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate();
|
||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate();
|
||||
DisableSync();
|
||||
});
|
||||
Mediator.Subscribe<PerformanceEndMessage>(this, (msg) =>
|
||||
{
|
||||
EnableSync();
|
||||
});
|
||||
Mediator.Subscribe<PerformanceStartMessage>(this, _ =>
|
||||
{
|
||||
DisableSync();
|
||||
});
|
||||
Mediator.Subscribe<InstanceOrDutyStartMessage>(this, _ =>
|
||||
{
|
||||
DisableSync();
|
||||
});
|
||||
Mediator.Subscribe<InstanceOrDutyEndMessage>(this, (msg) =>
|
||||
{
|
||||
EnableSync();
|
||||
|
||||
});
|
||||
|
||||
LastAppliedDataBytes = -1;
|
||||
@@ -119,6 +132,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler),
|
||||
EventSeverity.Informational, text)));
|
||||
Mediator.Publish(new RefreshUiMessage());
|
||||
Mediator.Publish(new VisibilityChange());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,11 +148,31 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
|
||||
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,
|
||||
"Cannot apply character data: you are in combat or performing music, deferring application")));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat or performing", applicationBase);
|
||||
"Cannot apply character data: you are in combat, deferring application")));
|
||||
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);
|
||||
SetUploading(isUploading: false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dalamudUtil.IsInInstance)
|
||||
{
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||
"Cannot apply character data: you are in an instance, deferring application")));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(isUploading: false);
|
||||
return;
|
||||
@@ -389,6 +423,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
||||
bool updateModdedPaths, bool updateManip, CancellationToken downloadToken)
|
||||
{
|
||||
await using var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false);
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
|
||||
|
||||
if (updateModdedPaths)
|
||||
@@ -706,6 +741,11 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.LogTrace("[BASE-{appBase}] Modded path calculation cancelled", applicationBase);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "[BASE-{appBase}] Something went wrong during calculation replacements", applicationBase);
|
||||
@@ -715,4 +755,21 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
Logger.LogDebug("[BASE-{appBase}] ModdedPaths calculated in {time}ms, missing files: {count}, total files: {total}", applicationBase, st.ElapsedMilliseconds, missingFiles.Count, moddedDictionary.Keys.Count);
|
||||
return [.. missingFiles];
|
||||
}
|
||||
}
|
||||
|
||||
private void DisableSync()
|
||||
{
|
||||
_dataReceivedInDowntime = null;
|
||||
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate();
|
||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate();
|
||||
}
|
||||
|
||||
private void EnableSync()
|
||||
{
|
||||
if (IsVisible && _dataReceivedInDowntime != null)
|
||||
{
|
||||
ApplyCharacterData(_dataReceivedInDowntime.ApplicationId,
|
||||
_dataReceivedInDowntime.CharacterData, _dataReceivedInDowntime.Forced);
|
||||
_dataReceivedInDowntime = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ public class Pair
|
||||
public long LastAppliedDataTris { get; set; } = -1;
|
||||
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
||||
public string Ident => _onlineUserIdentDto?.Ident ?? string.Empty;
|
||||
public uint PlayerCharacterId => CachedPlayer?.PlayerCharacterId ?? uint.MaxValue;
|
||||
|
||||
public UserData UserData => UserPair.User;
|
||||
|
||||
@@ -71,8 +72,8 @@ public class Pair
|
||||
Name = openProfileSeString,
|
||||
OnClicked = (a) => _mediator.Publish(new ProfileOpenStandaloneMessage(this)),
|
||||
UseDefaultPrefix = false,
|
||||
PrefixChar = 'M',
|
||||
PrefixColor = 526
|
||||
PrefixChar = 'L',
|
||||
PrefixColor = 708
|
||||
});
|
||||
|
||||
args.AddMenuItem(new MenuItem()
|
||||
@@ -80,8 +81,8 @@ public class Pair
|
||||
Name = reapplyDataSeString,
|
||||
OnClicked = (a) => ApplyLastReceivedData(forced: true),
|
||||
UseDefaultPrefix = false,
|
||||
PrefixChar = 'M',
|
||||
PrefixColor = 526
|
||||
PrefixChar = 'L',
|
||||
PrefixColor = 708
|
||||
});
|
||||
|
||||
args.AddMenuItem(new MenuItem()
|
||||
@@ -89,8 +90,8 @@ public class Pair
|
||||
Name = changePermissions,
|
||||
OnClicked = (a) => _mediator.Publish(new OpenPermissionWindow(this)),
|
||||
UseDefaultPrefix = false,
|
||||
PrefixChar = 'M',
|
||||
PrefixColor = 526
|
||||
PrefixChar = 'L',
|
||||
PrefixColor = 708
|
||||
});
|
||||
|
||||
args.AddMenuItem(new MenuItem()
|
||||
@@ -98,8 +99,8 @@ public class Pair
|
||||
Name = cyclePauseState,
|
||||
OnClicked = (a) => _mediator.Publish(new CyclePauseMessage(UserData)),
|
||||
UseDefaultPrefix = false,
|
||||
PrefixChar = 'M',
|
||||
PrefixColor = 526
|
||||
PrefixChar = 'L',
|
||||
PrefixColor = 708
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Comparer;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
@@ -7,10 +7,14 @@ using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.Services;
|
||||
|
||||
using LightlessSync.Services.Events;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
@@ -24,14 +28,19 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||
private Lazy<List<Pair>> _directPairsInternal;
|
||||
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> _groupPairsInternal;
|
||||
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,
|
||||
LightlessConfigService configurationService, LightlessMediator mediator,
|
||||
IContextMenu dalamudContextMenu) : base(logger, mediator)
|
||||
IContextMenu dalamudContextMenu, PairProcessingLimiter pairProcessingLimiter) : base(logger, mediator)
|
||||
{
|
||||
_pairFactory = pairFactory;
|
||||
_configurationService = configurationService;
|
||||
_dalamudContextMenu = dalamudContextMenu;
|
||||
_pairProcessingLimiter = pairProcessingLimiter;
|
||||
Mediator.Subscribe<DisconnectedMessage>(this, (_) => ClearPairs());
|
||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => ReapplyPairData());
|
||||
_directPairsInternal = DirectPairsLazy();
|
||||
@@ -112,6 +121,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||
public void ClearPairs()
|
||||
{
|
||||
Logger.LogDebug("Clearing all Pairs");
|
||||
ResetPairCreationQueue();
|
||||
DisposePairs();
|
||||
_allClientPairs.Clear();
|
||||
_allGroups.Clear();
|
||||
@@ -161,7 +171,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||
Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
pair.CreateCachedPlayer(dto);
|
||||
QueuePairCreation(pair, dto);
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
@@ -332,6 +342,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
ResetPairCreationQueue();
|
||||
_dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu;
|
||||
|
||||
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()
|
||||
{
|
||||
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.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
@@ -101,6 +101,8 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
forced |= _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash;
|
||||
|
||||
if (_fileUploadTask == null || (_fileUploadTask?.IsCompleted ?? false) || forced)
|
||||
@@ -127,6 +129,15 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
||||
_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,5 @@
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin;
|
||||
@@ -12,6 +13,7 @@ using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.PlayerData.Services;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.CharaData;
|
||||
using LightlessSync.Services.Events;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
@@ -28,8 +30,6 @@ using Microsoft.Extensions.Logging;
|
||||
using NReco.Logging.File;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using LightlessSync.Services.CharaData;
|
||||
using Dalamud.Game;
|
||||
|
||||
namespace LightlessSync;
|
||||
|
||||
@@ -41,7 +41,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui,
|
||||
IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager,
|
||||
ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, IGameConfig gameConfig,
|
||||
ISigScanner sigScanner)
|
||||
ISigScanner sigScanner, INamePlateGui namePlateGui, IAddonLifecycle addonLifecycle)
|
||||
{
|
||||
if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName))
|
||||
Directory.CreateDirectory(pluginInterface.ConfigDirectory.FullName);
|
||||
@@ -90,6 +90,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton(new WindowSystem("LightlessSync"));
|
||||
collection.AddSingleton<FileDialogManager>();
|
||||
collection.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", "", useEmbedded: true));
|
||||
collection.AddSingleton(gameGui);
|
||||
|
||||
// add lightless related singletons
|
||||
collection.AddSingleton<LightlessMediator>();
|
||||
@@ -105,6 +106,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton<GameObjectHandlerFactory>();
|
||||
collection.AddSingleton<FileDownloadManagerFactory>();
|
||||
collection.AddSingleton<PairHandlerFactory>();
|
||||
collection.AddSingleton<PairProcessingLimiter>();
|
||||
collection.AddSingleton<PairFactory>();
|
||||
collection.AddSingleton<XivDataAnalyzer>();
|
||||
collection.AddSingleton<CharacterAnalyzer>();
|
||||
@@ -112,6 +114,8 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton<PluginWarningNotificationService>();
|
||||
collection.AddSingleton<FileCompactor>();
|
||||
collection.AddSingleton<TagHandler>();
|
||||
collection.AddSingleton(s => new Lazy<ApiController>(() => s.GetRequiredService<ApiController>()));
|
||||
collection.AddSingleton<PairRequestService>();
|
||||
collection.AddSingleton<IdDisplayHandler>();
|
||||
collection.AddSingleton<PlayerPerformanceService>();
|
||||
collection.AddSingleton<TransientResourceManager>();
|
||||
@@ -130,17 +134,35 @@ public sealed class Plugin : IDalamudPlugin
|
||||
s.GetRequiredService<CharaDataManager>(),
|
||||
s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddSingleton<SelectPairForTagUi>();
|
||||
collection.AddSingleton<RenamePairTagUi>();
|
||||
collection.AddSingleton<SelectSyncshellForTagUi>();
|
||||
collection.AddSingleton<RenameSyncshellTagUi>();
|
||||
collection.AddSingleton((s) => new EventAggregator(pluginInterface.ConfigDirectory.FullName,
|
||||
s.GetRequiredService<ILogger<EventAggregator>>(), s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddSingleton((s) => new DalamudUtilService(s.GetRequiredService<ILogger<DalamudUtilService>>(),
|
||||
clientState, objectTable, framework, gameGui, condition, gameData, targetManager, gameConfig,
|
||||
s.GetRequiredService<BlockedCharacterHandler>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(),
|
||||
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<LightlessConfigService>(), s.GetRequiredService<PlayerPerformanceConfigService>()));
|
||||
collection.AddSingleton((s) => new DtrEntry(
|
||||
s.GetRequiredService<ILogger<DtrEntry>>(),
|
||||
dtrBar,
|
||||
s.GetRequiredService<LightlessConfigService>(),
|
||||
s.GetRequiredService<LightlessMediator>(),
|
||||
s.GetRequiredService<PairManager>(),
|
||||
s.GetRequiredService<PairRequestService>(),
|
||||
s.GetRequiredService<ApiController>(),
|
||||
s.GetRequiredService<ServerConfigurationManager>(),
|
||||
s.GetRequiredService<BroadcastService>(),
|
||||
s.GetRequiredService<BroadcastScannerService>(),
|
||||
s.GetRequiredService<DalamudUtilService>()));
|
||||
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
|
||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu));
|
||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu, s.GetRequiredService<PairProcessingLimiter>()));
|
||||
collection.AddSingleton<RedrawManager>();
|
||||
collection.AddSingleton<BroadcastService>();
|
||||
collection.AddSingleton(addonLifecycle);
|
||||
collection.AddSingleton(p => new ContextMenuService(contextMenu, pluginInterface, gameData,
|
||||
p.GetRequiredService<ILogger<ContextMenuService>>(), p.GetRequiredService<DalamudUtilService>(), p.GetRequiredService<ApiController>(), objectTable,
|
||||
p.GetRequiredService<LightlessConfigService>(), p.GetRequiredService<PairRequestService>(), p.GetRequiredService<PairManager>(), clientState));
|
||||
collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService<ILogger<IpcCallerPenumbra>>(), pluginInterface,
|
||||
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<RedrawManager>()));
|
||||
collection.AddSingleton((s) => new IpcCallerGlamourer(s.GetRequiredService<ILogger<IpcCallerGlamourer>>(), pluginInterface,
|
||||
@@ -161,9 +183,14 @@ public sealed class Plugin : IDalamudPlugin
|
||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<IpcCallerPenumbra>(), s.GetRequiredService<IpcCallerGlamourer>(),
|
||||
s.GetRequiredService<IpcCallerCustomize>(), s.GetRequiredService<IpcCallerHeels>(), s.GetRequiredService<IpcCallerHonorific>(),
|
||||
s.GetRequiredService<IpcCallerMoodles>(), s.GetRequiredService<IpcCallerPetNames>(), s.GetRequiredService<IpcCallerBrio>()));
|
||||
collection.AddSingleton((s) => new NotificationService(s.GetRequiredService<ILogger<NotificationService>>(),
|
||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<DalamudUtilService>(),
|
||||
notificationManager, chatGui, s.GetRequiredService<LightlessConfigService>()));
|
||||
collection.AddSingleton((s) => new NotificationService(
|
||||
s.GetRequiredService<ILogger<NotificationService>>(),
|
||||
s.GetRequiredService<LightlessConfigService>(),
|
||||
s.GetRequiredService<DalamudUtilService>(),
|
||||
notificationManager,
|
||||
chatGui,
|
||||
s.GetRequiredService<LightlessMediator>(),
|
||||
s.GetRequiredService<PairRequestService>()));
|
||||
collection.AddSingleton((s) =>
|
||||
{
|
||||
var httpClient = new HttpClient();
|
||||
@@ -171,32 +198,44 @@ public sealed class Plugin : IDalamudPlugin
|
||||
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
||||
return httpClient;
|
||||
});
|
||||
collection.AddSingleton((s) => new LightlessConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new UiThemeConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) =>
|
||||
{
|
||||
var cfg = new LightlessConfigService(pluginInterface.ConfigDirectory.FullName);
|
||||
var theme = s.GetRequiredService<UiThemeConfigService>();
|
||||
LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
|
||||
return cfg;
|
||||
});
|
||||
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new ServerTagConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new PairTagConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new SyncshellTagConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new TransientConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new XivDataStorageService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<LightlessConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<UiThemeConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<ServerTagConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<PairTagConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<SyncshellTagConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<TransientConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<XivDataStorageService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<PlayerPerformanceConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
|
||||
collection.AddSingleton<ConfigurationMigrator>();
|
||||
collection.AddSingleton<ConfigurationSaveService>();
|
||||
|
||||
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
|
||||
collection.AddScoped<DrawEntityFactory>();
|
||||
collection.AddScoped<CacheMonitor>();
|
||||
collection.AddScoped<UiFactory>();
|
||||
collection.AddScoped<SelectTagForPairUi>();
|
||||
collection.AddScoped<SelectTagForSyncshellUi>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, SettingsUi>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, CompactUi>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
|
||||
@@ -212,7 +251,15 @@ public sealed class Plugin : IDalamudPlugin
|
||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<FileDialogManager>(),
|
||||
s.GetRequiredService<LightlessProfileManager>(), s.GetRequiredService<PerformanceCollectorService>()));
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, BroadcastUI>((s) => new BroadcastUI(s.GetRequiredService<ILogger<BroadcastUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>()));
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>((s) => new SyncshellFinderUI(s.GetRequiredService<ILogger<SyncshellFinderUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<DalamudUtilService>()));
|
||||
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, LightlessNotificationUI>((s) =>
|
||||
new LightlessNotificationUI(
|
||||
s.GetRequiredService<ILogger<LightlessNotificationUI>>(),
|
||||
s.GetRequiredService<LightlessMediator>(),
|
||||
s.GetRequiredService<PerformanceCollectorService>(),
|
||||
s.GetRequiredService<LightlessConfigService>()));
|
||||
collection.AddScoped<IPopupHandler, CensusPopupHandler>();
|
||||
collection.AddScoped<CacheCreationService>();
|
||||
collection.AddScoped<PlayerDataFactory>();
|
||||
@@ -220,7 +267,9 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddScoped((s) => new UiService(s.GetRequiredService<ILogger<UiService>>(), pluginInterface.UiBuilder, s.GetRequiredService<LightlessConfigService>(),
|
||||
s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(),
|
||||
s.GetRequiredService<UiFactory>(),
|
||||
s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<LightlessMediator>()));
|
||||
s.GetRequiredService<FileDialogManager>(),
|
||||
s.GetRequiredService<LightlessMediator>(),
|
||||
s.GetRequiredService<NotificationService>()));
|
||||
collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(),
|
||||
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<ApiController>(),
|
||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<LightlessConfigService>()));
|
||||
@@ -228,6 +277,10 @@ public sealed class Plugin : IDalamudPlugin
|
||||
s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<DalamudUtilService>(),
|
||||
pluginInterface, textureProvider, s.GetRequiredService<Dalamud.Localization>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<TokenProvider>(),
|
||||
s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), namePlateGui, clientState,
|
||||
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<LightlessMediator>());
|
||||
@@ -240,6 +293,8 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<LightlessPlugin>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<ContextMenuService>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<BroadcastService>());
|
||||
})
|
||||
.Build();
|
||||
|
||||
@@ -251,4 +306,4 @@ public sealed class Plugin : IDalamudPlugin
|
||||
_host.StopAsync().GetAwaiter().GetResult();
|
||||
_host.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
232
LightlessSync/Services/BroadcastScanningService.cs
Normal file
232
LightlessSync/Services/BroadcastScanningService.cs
Normal file
@@ -0,0 +1,232 @@
|
||||
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();
|
||||
}
|
||||
|
||||
public int CountActiveBroadcasts(string? excludeHashedCid = null)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var comparer = StringComparer.Ordinal;
|
||||
return _broadcastCache.Count(entry =>
|
||||
entry.Value.IsBroadcasting &&
|
||||
entry.Value.ExpiryTime > now &&
|
||||
(excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid)));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
_framework.Update -= OnFrameworkUpdate;
|
||||
_cleanupCts.Cancel();
|
||||
_cleanupTask?.Wait(100);
|
||||
_nameplateHandler.Uninit();
|
||||
}
|
||||
}
|
||||
504
LightlessSync/Services/BroadcastService.cs
Normal file
504
LightlessSync/Services/BroadcastService.cs
Normal file
@@ -0,0 +1,504 @@
|
||||
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;
|
||||
using System.Threading;
|
||||
|
||||
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;
|
||||
private CancellationTokenSource? _lightfinderCancelTokens;
|
||||
private Action? _connectedHandler;
|
||||
public LightlessMediator Mediator => _mediator;
|
||||
|
||||
public bool IsLightFinderAvailable { get; private set; } = false;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private async Task<string?> GetLocalHashedCidAsync(string context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false);
|
||||
return cid.ToString().GetHash256();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to resolve CID for {Context}", context);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyBroadcastDisabled(bool forcePublish = false)
|
||||
{
|
||||
bool wasEnabled = _config.Current.BroadcastEnabled;
|
||||
bool hadExpiry = _config.Current.BroadcastTtl != DateTime.MinValue;
|
||||
bool hadRemaining = _remainingTtl.HasValue;
|
||||
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
|
||||
if (wasEnabled || hadExpiry)
|
||||
_config.Save();
|
||||
|
||||
_remainingTtl = null;
|
||||
_waitingForTtlFetch = false;
|
||||
_syncedOnStartup = false;
|
||||
|
||||
if (forcePublish || wasEnabled || hadRemaining)
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
}
|
||||
|
||||
private bool TryApplyBroadcastEnabled(TimeSpan? ttl, string context)
|
||||
{
|
||||
if (ttl is not { } validTtl || validTtl <= TimeSpan.Zero)
|
||||
{
|
||||
_logger.LogWarning("Lightfinder enable skipped ({Context}): invalid TTL ({TTL})", context, ttl);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool wasEnabled = _config.Current.BroadcastEnabled;
|
||||
TimeSpan? previousRemaining = _remainingTtl;
|
||||
DateTime previousExpiry = _config.Current.BroadcastTtl;
|
||||
|
||||
var newExpiry = DateTime.UtcNow + validTtl;
|
||||
|
||||
_config.Current.BroadcastEnabled = true;
|
||||
_config.Current.BroadcastTtl = newExpiry;
|
||||
|
||||
if (!wasEnabled || previousExpiry != newExpiry)
|
||||
_config.Save();
|
||||
|
||||
_remainingTtl = validTtl;
|
||||
_waitingForTtlFetch = false;
|
||||
|
||||
if (!wasEnabled || previousRemaining != validTtl)
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl));
|
||||
|
||||
_logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleLightfinderUnavailable(string message, Exception? ex = null)
|
||||
{
|
||||
if (ex != null)
|
||||
_logger.LogWarning(ex, message);
|
||||
else
|
||||
_logger.LogWarning(message);
|
||||
|
||||
IsLightFinderAvailable = false;
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
}
|
||||
|
||||
private void OnDisconnected()
|
||||
{
|
||||
IsLightFinderAvailable = false;
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
_logger.LogDebug("Cleared Lightfinder state due to disconnect.");
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_mediator.Subscribe<EnableBroadcastMessage>(this, OnEnableBroadcast);
|
||||
_mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
||||
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
||||
_mediator.Subscribe<DisconnectedMessage>(this, _ => OnDisconnected());
|
||||
|
||||
IsLightFinderAvailable = false;
|
||||
|
||||
_lightfinderCancelTokens?.Cancel();
|
||||
_lightfinderCancelTokens?.Dispose();
|
||||
_lightfinderCancelTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_connectedHandler = () => _ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token);
|
||||
_apiController.OnConnected += _connectedHandler;
|
||||
|
||||
if (_apiController.IsConnected)
|
||||
_ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_lightfinderCancelTokens?.Cancel();
|
||||
_lightfinderCancelTokens?.Dispose();
|
||||
_lightfinderCancelTokens = null;
|
||||
|
||||
if (_connectedHandler is not null)
|
||||
{
|
||||
_apiController.OnConnected -= _connectedHandler;
|
||||
_connectedHandler = null;
|
||||
}
|
||||
|
||||
_mediator.UnsubscribeAll(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!_apiController.IsConnected && !cancellationToken.IsCancellationRequested)
|
||||
await Task.Delay(250, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var hashedCid = await GetLocalHashedCidAsync("Lightfinder state check").ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(hashedCid))
|
||||
return;
|
||||
|
||||
BroadcastStatusInfoDto? status = null;
|
||||
try
|
||||
{
|
||||
status = await _apiController.IsUserBroadcasting(hashedCid).ConfigureAwait(false);
|
||||
}
|
||||
catch (HubException ex) when (ex.Message.Contains("Method does not exist", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HandleLightfinderUnavailable("Lightfinder unavailable on server (required method missing).", ex);
|
||||
}
|
||||
|
||||
if (!IsLightFinderAvailable)
|
||||
_logger.LogInformation("Lightfinder is available.");
|
||||
|
||||
IsLightFinderAvailable = true;
|
||||
|
||||
bool isBroadcasting = status?.IsBroadcasting == true;
|
||||
TimeSpan? ttl = status?.TTL;
|
||||
|
||||
if (isBroadcasting)
|
||||
{
|
||||
if (ttl is not { } remaining || remaining <= TimeSpan.Zero)
|
||||
ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
||||
|
||||
if (TryApplyBroadcastEnabled(ttl, "server handshake"))
|
||||
{
|
||||
_syncedOnStartup = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isBroadcasting = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBroadcasting)
|
||||
{
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
_logger.LogInformation("Lightfinder is available but no active broadcast was found.");
|
||||
}
|
||||
|
||||
if (_config.Current.LightfinderAutoEnableOnConnect && !isBroadcasting)
|
||||
{
|
||||
_logger.LogInformation("Auto-enabling Lightfinder broadcast after reconnect.");
|
||||
_mediator.Publish(new EnableBroadcastMessage(hashedCid, true));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Lightfinder check was canceled.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleLightfinderUnavailable("Lightfinder check failed.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
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.Enabled, groupDto).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid);
|
||||
|
||||
if (!msg.Enabled)
|
||||
{
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
return;
|
||||
}
|
||||
|
||||
_waitingForTtlFetch = true;
|
||||
|
||||
try
|
||||
{
|
||||
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
|
||||
|
||||
if (TryApplyBroadcastEnabled(ttl, "client request"))
|
||||
{
|
||||
_logger.LogDebug("Fetched TTL from server: {TTL}", ttl);
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_waitingForTtlFetch = false;
|
||||
}
|
||||
}
|
||||
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? cidForLog = null)
|
||||
{
|
||||
TimeSpan? ttl = null;
|
||||
await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => {
|
||||
try
|
||||
{
|
||||
ttl = await _apiController.GetBroadcastTtl().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (cidForLog is { Length: > 0 })
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch broadcast TTL for {Cid}", cidForLog);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch broadcast TTL");
|
||||
}
|
||||
}
|
||||
}).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 GetLocalHashedCidAsync(nameof(ToggleBroadcast)).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(hashedCid))
|
||||
{
|
||||
_logger.LogWarning("ToggleBroadcast - unable to resolve CID.");
|
||||
return;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
var hashedCid = await GetLocalHashedCidAsync("startup TTL refresh").ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(hashedCid))
|
||||
{
|
||||
_logger.LogDebug("Skipping TTL refresh; hashed CID unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
||||
if (TryApplyBroadcastEnabled(ttl, "startup TTL refresh"))
|
||||
{
|
||||
_syncedOnStartup = true;
|
||||
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", ttl);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state.");
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh TTL in OnTick");
|
||||
_syncedOnStartup = false;
|
||||
}
|
||||
}
|
||||
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.");
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_remainingTtl = null;
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -944,9 +944,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
||||
|
||||
Logger.LogTrace("[{appId}] Computing local missing files", applicationId);
|
||||
|
||||
Dictionary<string, string> modPaths;
|
||||
List<FileReplacementData> missingFiles;
|
||||
_fileHandler.ComputeMissingFiles(charaDataDownloadDto, out modPaths, out missingFiles);
|
||||
_fileHandler.ComputeMissingFiles(charaDataDownloadDto, out Dictionary<string, string> modPaths, out List<FileReplacementData> missingFiles);
|
||||
|
||||
Logger.LogTrace("[{appId}] Computing local missing files", applicationId);
|
||||
|
||||
@@ -990,7 +988,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
_uploadCts = _uploadCts.CancelRecreate();
|
||||
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));
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_charaDataConfigService.Current.NearbyDrawWisps || _dalamudUtilService.IsInGpose || _dalamudUtilService.IsInCombatOrPerforming)
|
||||
if (!_charaDataConfigService.Current.NearbyDrawWisps || _dalamudUtilService.IsInGpose || _dalamudUtilService.IsInCombat || _dalamudUtilService.IsPerforming || _dalamudUtilService.IsInInstance)
|
||||
ClearAllVfx();
|
||||
|
||||
var camera = CameraManager.Instance()->CurrentCamera;
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LightlessSync.Services.CharaData
|
||||
namespace LightlessSync.Services.CharaData
|
||||
{
|
||||
internal class CharaDataTogetherManager
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Dalamud.Utility;
|
||||
using Lumina.Excel.Sheets;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using Lumina.Excel.Sheets;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using Lumina.Data.Files;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.Utils;
|
||||
using Lumina.Data.Files;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
@@ -42,7 +42,8 @@ public sealed class CommandManagerService : IDisposable
|
||||
"\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 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 finder - Opens the Lightfinder window"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -122,5 +123,9 @@ public sealed class CommandManagerService : IDisposable
|
||||
{
|
||||
_mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
||||
}
|
||||
else if (string.Equals(splitArgs[0], "finder", 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).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,3 @@
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
@@ -10,13 +9,13 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using Lumina.Excel.Sheets;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.Interop;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
@@ -40,6 +39,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly PerformanceCollectorService _performanceCollector;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||
private uint? _classJobId = 0;
|
||||
private DateTime _delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
||||
private string _lastGlobalBlockPlayer = string.Empty;
|
||||
@@ -53,7 +53,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
public DalamudUtilService(ILogger<DalamudUtilService> logger, IClientState clientState, IObjectTable objectTable, IFramework framework,
|
||||
IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, IGameConfig gameConfig,
|
||||
BlockedCharacterHandler blockedCharacterHandler, LightlessMediator mediator, PerformanceCollectorService performanceCollector,
|
||||
LightlessConfigService configService)
|
||||
LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService)
|
||||
{
|
||||
_logger = logger;
|
||||
_clientState = clientState;
|
||||
@@ -67,6 +67,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
Mediator = mediator;
|
||||
_performanceCollector = performanceCollector;
|
||||
_configService = configService;
|
||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||
WorldData = new(() =>
|
||||
{
|
||||
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(Dalamud.Game.ClientLanguage.English)!
|
||||
@@ -135,7 +136,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_cid = RebuildCID();
|
||||
}
|
||||
|
||||
private Lazy<ulong> RebuildCID() => new(GetCID);
|
||||
private Lazy<ulong> RebuildCID() => new(GetCID);
|
||||
|
||||
public bool IsWine { get; init; }
|
||||
|
||||
@@ -161,7 +162,9 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
public bool IsLoggedIn { get; private set; }
|
||||
public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread;
|
||||
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 HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
|
||||
public uint ClassJobId => _classJobId!.Value;
|
||||
public Lazy<Dictionary<uint, string>> JobData { get; private set; }
|
||||
@@ -310,7 +313,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return await RunOnFrameworkThread(() => _cid.Value.ToString().GetHash256()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private unsafe static string GetHashedCIDFromPlayerPointer(nint ptr)
|
||||
public unsafe static string GetHashedCIDFromPlayerPointer(nint ptr)
|
||||
{
|
||||
return ((BattleChara*)ptr)->Character.ContentId.ToString().GetHash256();
|
||||
}
|
||||
@@ -418,6 +421,16 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return await RunOnFrameworkThread(() => IsObjectPresent(obj)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public IPlayerCharacter? GetPlayerByNameAndWorld(string name, ushort homeWorldId)
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
return _objectTable
|
||||
.OfType<IPlayerCharacter>()
|
||||
.FirstOrDefault(p =>
|
||||
string.Equals(p.Name.TextValue, name, StringComparison.Ordinal) &&
|
||||
p.HomeWorld.RowId == homeWorldId);
|
||||
}
|
||||
|
||||
public async Task RunOnFrameworkThread(System.Action act, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
|
||||
@@ -528,7 +541,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
curWaitTime += tick;
|
||||
Thread.Sleep(tick);
|
||||
}
|
||||
|
||||
Thread.Sleep(tick * 2);
|
||||
}
|
||||
|
||||
@@ -544,6 +556,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return result;
|
||||
}
|
||||
|
||||
public string? GetWorldNameFromPlayerAddress(nint address)
|
||||
{
|
||||
if (address == nint.Zero) return null;
|
||||
|
||||
EnsureIsOnFramework();
|
||||
var playerCharacter = _objectTable.OfType<IPlayerCharacter>().FirstOrDefault(p => p.Address == address);
|
||||
if (playerCharacter == null) return null;
|
||||
|
||||
var worldId = (ushort)playerCharacter.HomeWorld.RowId;
|
||||
return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
|
||||
}
|
||||
|
||||
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
|
||||
{
|
||||
var gameObj = (GameObject*)address;
|
||||
@@ -668,19 +692,47 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
Mediator.Publish(new GposeEndMessage());
|
||||
}
|
||||
|
||||
if ((_condition[ConditionFlag.Performing] || _condition[ConditionFlag.InCombat]) && !IsInCombatOrPerforming)
|
||||
{
|
||||
_logger.LogDebug("Combat/Performance start");
|
||||
IsInCombatOrPerforming = true;
|
||||
Mediator.Publish(new CombatOrPerformanceStartMessage());
|
||||
Mediator.Publish(new HaltScanMessage(nameof(IsInCombatOrPerforming)));
|
||||
if ((_condition[ConditionFlag.InCombat]) && !IsInCombat && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat)
|
||||
{
|
||||
_logger.LogDebug("Combat start");
|
||||
IsInCombat = true;
|
||||
Mediator.Publish(new CombatStartMessage());
|
||||
Mediator.Publish(new HaltScanMessage(nameof(IsInCombat)));
|
||||
}
|
||||
else if ((!_condition[ConditionFlag.Performing] && !_condition[ConditionFlag.InCombat]) && IsInCombatOrPerforming)
|
||||
else if ((!_condition[ConditionFlag.InCombat]) && IsInCombat && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat)
|
||||
{
|
||||
_logger.LogDebug("Combat/Performance end");
|
||||
IsInCombatOrPerforming = false;
|
||||
Mediator.Publish(new CombatOrPerformanceEndMessage());
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(IsInCombatOrPerforming)));
|
||||
_logger.LogDebug("Combat end");
|
||||
IsInCombat = false;
|
||||
Mediator.Publish(new CombatEndMessage());
|
||||
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)
|
||||
{
|
||||
_logger.LogDebug("Instance start");
|
||||
IsInInstance = true;
|
||||
Mediator.Publish(new InstanceOrDutyStartMessage());
|
||||
Mediator.Publish(new HaltScanMessage(nameof(IsInInstance)));
|
||||
}
|
||||
else if (((!_condition[ConditionFlag.BoundByDuty]) && IsInInstance && _playerPerformanceConfigService.Current.PauseInInstanceDuty) || ((_condition[ConditionFlag.BoundByDuty]) && IsInInstance && !_playerPerformanceConfigService.Current.PauseInInstanceDuty))
|
||||
{
|
||||
_logger.LogDebug("Instance end");
|
||||
IsInInstance = false;
|
||||
Mediator.Publish(new InstanceOrDutyEndMessage());
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(IsInInstance)));
|
||||
}
|
||||
|
||||
if (_condition[ConditionFlag.WatchingCutscene] && !IsInCutscene)
|
||||
@@ -736,7 +788,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_classJobId = localPlayer.ClassJob.RowId;
|
||||
}
|
||||
|
||||
if (!IsInCombatOrPerforming)
|
||||
if (!IsInCombat || !IsPerforming || !IsInInstance)
|
||||
Mediator.Publish(new FrameworkUpdateMessage());
|
||||
|
||||
Mediator.Publish(new PriorityFrameworkUpdateMessage());
|
||||
@@ -765,7 +817,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
IsLodEnabled = lodEnabled;
|
||||
}
|
||||
|
||||
if (IsInCombatOrPerforming)
|
||||
if (IsInCombat || IsPerforming || IsInInstance)
|
||||
Mediator.Publish(new FrameworkUpdateMessage());
|
||||
|
||||
Mediator.Publish(new DelayedFrameworkUpdateMessage());
|
||||
|
||||
80
LightlessSync/Services/LightlessProfileManager.cs
Normal file
80
LightlessSync/Services/LightlessProfileManager.cs
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
@@ -17,6 +17,7 @@ namespace LightlessSync.Services.Mediator;
|
||||
public record SwitchToIntroUiMessage : MessageBase;
|
||||
public record SwitchToMainUiMessage : MessageBase;
|
||||
public record OpenSettingsUiMessage : MessageBase;
|
||||
public record OpenLightfinderSettingsMessage : MessageBase;
|
||||
public record DalamudLoginMessage : MessageBase;
|
||||
public record DalamudLogoutMessage : MessageBase;
|
||||
public record PriorityFrameworkUpdateMessage : SameThreadMessage;
|
||||
@@ -47,18 +48,21 @@ public record PetNamesMessage(string PetNicknamesData) : MessageBase;
|
||||
public record HonorificReadyMessage : MessageBase;
|
||||
public record TransientResourceChangedMessage(IntPtr Address) : MessageBase;
|
||||
public record HaltScanMessage(string Source) : MessageBase;
|
||||
public record ResumeScanMessage(string Source) : MessageBase;
|
||||
public record NotificationMessage
|
||||
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
|
||||
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
|
||||
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
|
||||
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
|
||||
public record LightlessNotificationMessage(LightlessSync.UI.Models.LightlessNotification Notification) : MessageBase;
|
||||
public record LightlessNotificationDismissMessage(string NotificationId) : MessageBase;
|
||||
public record ClearAllNotificationsMessage : MessageBase;
|
||||
public record CharacterDataAnalyzedMessage : MessageBase;
|
||||
public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase;
|
||||
public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase;
|
||||
public record HubReconnectingMessage(Exception? Exception) : SameThreadMessage;
|
||||
public record HubReconnectedMessage(string? Arg) : SameThreadMessage;
|
||||
public record HubClosedMessage(Exception? Exception) : SameThreadMessage;
|
||||
public record ResumeScanMessage(string Source) : MessageBase;
|
||||
public record DownloadReadyMessage(Guid RequestId) : MessageBase;
|
||||
public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase;
|
||||
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
|
||||
@@ -77,10 +81,15 @@ public record OpenCensusPopupMessage() : MessageBase;
|
||||
public record OpenSyncshellAdminPanel(GroupFullInfoDto GroupInfo) : MessageBase;
|
||||
public record OpenPermissionWindow(Pair Pair) : MessageBase;
|
||||
public record DownloadLimitChangedMessage() : SameThreadMessage;
|
||||
public record PairProcessingLimitChangedMessage : SameThreadMessage;
|
||||
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
|
||||
public record TargetPairMessage(Pair Pair) : MessageBase;
|
||||
public record CombatOrPerformanceStartMessage : MessageBase;
|
||||
public record CombatOrPerformanceEndMessage : MessageBase;
|
||||
public record CombatStartMessage : MessageBase;
|
||||
public record CombatEndMessage : MessageBase;
|
||||
public record PerformanceStartMessage : MessageBase;
|
||||
public record PerformanceEndMessage : MessageBase;
|
||||
public record InstanceOrDutyStartMessage : MessageBase;
|
||||
public record InstanceOrDutyEndMessage : MessageBase;
|
||||
public record EventMessage(Event Event) : MessageBase;
|
||||
public record PenumbraDirectoryChangedMessage(string? ModDirectory) : MessageBase;
|
||||
public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage;
|
||||
@@ -93,5 +102,10 @@ public record GPoseLobbyReceiveCharaData(CharaDataDownloadDto CharaDataDownloadD
|
||||
public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase;
|
||||
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
||||
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
|
||||
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
|
||||
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
|
||||
public record SyncshellBroadcastsUpdatedMessage : MessageBase;
|
||||
public record PairRequestsUpdatedMessage : MessageBase;
|
||||
public record VisibilityChange : MessageBase;
|
||||
#pragma warning restore S2094
|
||||
#pragma warning restore MA0048 // File name must match type name
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services.Mediator;
|
||||
|
||||
619
LightlessSync/Services/NameplateHandler.cs
Normal file
619
LightlessSync/Services/NameplateHandler.cs
Normal file
@@ -0,0 +1,619 @@
|
||||
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();
|
||||
|
||||
if (framework == null) return;
|
||||
|
||||
var ui3DModule = framework->GetUIModule()->GetUI3DModule();
|
||||
|
||||
if (ui3DModule == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
|
||||
{
|
||||
if (ui3DModule->NamePlateObjectInfoPointers.IsEmpty) continue;
|
||||
|
||||
var objectInfoPtr = ui3DModule->NamePlateObjectInfoPointers[i];
|
||||
|
||||
if (objectInfoPtr == null) continue;
|
||||
|
||||
var objectInfo = objectInfoPtr.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;
|
||||
|
||||
if (mpNameplateAddon == 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()) || _configService.Current.LightfinderLabelShowHidden;
|
||||
pNode->AtkResNode.ToggleVisibility(IsVisible);
|
||||
|
||||
if (nameplateObject.RootComponentNode == null ||
|
||||
nameplateObject.NameContainer == null ||
|
||||
nameplateObject.NameText == null)
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var nameContainer = nameplateObject.NameContainer;
|
||||
var nameText = nameplateObject.NameText;
|
||||
|
||||
if (nameContainer == null || nameText == null)
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var labelColor = UIColors.Get("Lightfinder");
|
||||
var edgeColor = UIColors.Get("LightfinderEdge");
|
||||
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.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
||||
labelContent = DefaultLabelText;
|
||||
|
||||
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
||||
|
||||
pNode->SetText(labelContent);
|
||||
|
||||
if (!config.LightfinderLabelUseIcon)
|
||||
{
|
||||
pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize;
|
||||
pNode->AtkResNode.Width = 0;
|
||||
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||
if (nodeWidth <= 0)
|
||||
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||
pNode->AtkResNode.Width = (ushort)nodeWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
pNode->TextFlags |= TextFlags.AutoAdjustNodeSize;
|
||||
pNode->AtkResNode.Width = 0;
|
||||
nodeWidth = pNode->AtkResNode.GetWidth();
|
||||
}
|
||||
|
||||
|
||||
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
|
||||
{
|
||||
var nameplateWidth = (int)nameContainer->Width;
|
||||
|
||||
int leftPos = nameplateWidth / 8;
|
||||
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
|
||||
int centrePos = (nameplateWidth - nodeWidth) / 2;
|
||||
int staticMargin = 24;
|
||||
int calcMargin = (int)(nameplateWidth * 0.08f);
|
||||
|
||||
switch (config.LabelAlignment)
|
||||
{
|
||||
case LabelAlignment.Left:
|
||||
positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos;
|
||||
alignment = AlignmentType.BottomLeft;
|
||||
break;
|
||||
case LabelAlignment.Right:
|
||||
positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin;
|
||||
alignment = AlignmentType.BottomRight;
|
||||
break;
|
||||
default:
|
||||
positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin;
|
||||
alignment = AlignmentType.Bottom;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
positionX = 58 + config.LightfinderLabelOffsetX;
|
||||
alignment = AlignmentType.Bottom;
|
||||
}
|
||||
|
||||
positionY += config.LightfinderLabelOffsetY;
|
||||
|
||||
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
||||
pNode->AtkResNode.SetUseDepthBasedPriority(true);
|
||||
|
||||
pNode->AtkResNode.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);
|
||||
}
|
||||
}
|
||||
114
LightlessSync/Services/NameplateService.cs
Normal file
114
LightlessSync/Services/NameplateService.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.Gui.NamePlate;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public class NameplateService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly ILogger<NameplateService> _logger;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly INamePlateGui _namePlateGui;
|
||||
private readonly PairManager _pairManager;
|
||||
|
||||
public NameplateService(ILogger<NameplateService> logger,
|
||||
LightlessConfigService configService,
|
||||
INamePlateGui namePlateGui,
|
||||
IClientState clientState,
|
||||
PairManager pairManager,
|
||||
LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_namePlateGui = namePlateGui;
|
||||
_clientState = clientState;
|
||||
_pairManager = pairManager;
|
||||
|
||||
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
|
||||
_namePlateGui.RequestRedraw();
|
||||
Mediator.Subscribe<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw());
|
||||
}
|
||||
|
||||
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||
{
|
||||
if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen))
|
||||
return;
|
||||
|
||||
var visibleUsersIds = _pairManager.GetOnlineUserPairs()
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)
|
||||
.ToHashSet();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var colors = _configService.Current.NameplateColors;
|
||||
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
var playerCharacter = handler.PlayerCharacter;
|
||||
if (playerCharacter == null)
|
||||
continue;
|
||||
|
||||
var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember);
|
||||
var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend);
|
||||
bool partyColorAllowed = (_configService.Current.overridePartyColor && isInParty);
|
||||
bool friendColorAllowed = (_configService.Current.overrideFriendColor && isFriend);
|
||||
|
||||
if (visibleUsersIds.Contains(handler.GameObjectId) &&
|
||||
!(
|
||||
(isInParty && !partyColorAllowed) ||
|
||||
(isFriend && !friendColorAllowed)
|
||||
))
|
||||
{
|
||||
handler.NameParts.TextWrap = CreateTextWrap(colors);
|
||||
|
||||
if (_configService.Current.overrideFcTagColor)
|
||||
{
|
||||
bool hasActualFcTag = playerCharacter.CompanyTag.TextValue.Length > 0;
|
||||
bool isFromDifferentRealm = playerCharacter.HomeWorld.RowId != playerCharacter.CurrentWorld.RowId;
|
||||
bool shouldColorFcArea = hasActualFcTag || (!hasActualFcTag && isFromDifferentRealm);
|
||||
|
||||
if (shouldColorFcArea)
|
||||
{
|
||||
handler.FreeCompanyTagParts.OuterWrap = CreateTextWrap(colors);
|
||||
handler.FreeCompanyTagParts.TextWrap = CreateTextWrap(colors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RequestRedraw()
|
||||
{
|
||||
_namePlateGui.RequestRedraw();
|
||||
}
|
||||
|
||||
private static (SeString, SeString) CreateTextWrap(DtrEntry.Colors color)
|
||||
{
|
||||
var left = new Lumina.Text.SeStringBuilder();
|
||||
var right = new Lumina.Text.SeStringBuilder();
|
||||
|
||||
left.PushColorRgba(color.Foreground);
|
||||
right.PopColor();
|
||||
|
||||
left.PushEdgeColorRgba(color.Glow);
|
||||
right.PopEdgeColor();
|
||||
|
||||
return (left.ToReadOnlySeString().ToDalamudString(), right.ToReadOnlySeString().ToDalamudString());
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
_namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate;
|
||||
_namePlateGui.RequestRedraw();
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,495 @@
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Models;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using NotificationType = LightlessSync.LightlessConfiguration.Models.NotificationType;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public class NotificationService : DisposableMediatorSubscriberBase, IHostedService
|
||||
{
|
||||
private readonly ILogger<NotificationService> _logger;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly INotificationManager _notificationManager;
|
||||
private readonly IChatGui _chatGui;
|
||||
private readonly LightlessConfigService _configurationService;
|
||||
private readonly PairRequestService _pairRequestService;
|
||||
private readonly HashSet<string> _shownPairRequestNotifications = new();
|
||||
|
||||
public NotificationService(ILogger<NotificationService> logger, LightlessMediator mediator,
|
||||
public NotificationService(
|
||||
ILogger<NotificationService> logger,
|
||||
LightlessConfigService configService,
|
||||
DalamudUtilService dalamudUtilService,
|
||||
INotificationManager notificationManager,
|
||||
IChatGui chatGui, LightlessConfigService configurationService) : base(logger, mediator)
|
||||
IChatGui chatGui,
|
||||
LightlessMediator mediator,
|
||||
PairRequestService pairRequestService) : base(logger, mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_notificationManager = notificationManager;
|
||||
_chatGui = chatGui;
|
||||
_configurationService = configurationService;
|
||||
_pairRequestService = pairRequestService;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
|
||||
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
|
||||
Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info,
|
||||
TimeSpan? duration = null, List<LightlessNotificationAction>? actions = null, uint? soundEffectId = null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
var notification = CreateNotification(title, message, type, duration, actions, soundEffectId);
|
||||
|
||||
if (_configService.Current.AutoDismissOnAction && notification.Actions.Any())
|
||||
{
|
||||
WrapActionsWithAutoDismiss(notification);
|
||||
}
|
||||
|
||||
if (notification.SoundEffectId.HasValue)
|
||||
{
|
||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||
}
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
}
|
||||
|
||||
private void PrintErrorChat(string? message)
|
||||
private LightlessNotification CreateNotification(string title, string message, NotificationType type,
|
||||
TimeSpan? duration, List<LightlessNotificationAction>? actions, uint? soundEffectId)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
|
||||
_chatGui.PrintError(se.BuiltString);
|
||||
return new LightlessNotification
|
||||
{
|
||||
Title = title,
|
||||
Message = message,
|
||||
Type = type,
|
||||
Duration = duration ?? GetDefaultDurationForType(type),
|
||||
Actions = actions ?? new List<LightlessNotificationAction>(),
|
||||
SoundEffectId = GetSoundEffectId(type, soundEffectId),
|
||||
ShowProgress = _configService.Current.ShowNotificationProgress,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private void PrintInfoChat(string? message)
|
||||
private void WrapActionsWithAutoDismiss(LightlessNotification notification)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ").AddItalics(message ?? string.Empty);
|
||||
_chatGui.Print(se.BuiltString);
|
||||
foreach (var action in notification.Actions)
|
||||
{
|
||||
var originalOnClick = action.OnClick;
|
||||
action.OnClick = (n) =>
|
||||
{
|
||||
originalOnClick(n);
|
||||
if (_configService.Current.AutoDismissOnAction)
|
||||
{
|
||||
DismissNotification(n);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintWarnChat(string? message)
|
||||
private void DismissNotification(LightlessNotification notification)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
|
||||
_chatGui.Print(se.BuiltString);
|
||||
notification.IsDismissed = true;
|
||||
notification.IsAnimatingOut = true;
|
||||
}
|
||||
|
||||
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
|
||||
{
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Id = $"pair_request_{senderId}",
|
||||
Title = "Pair Request Received",
|
||||
Message = $"{senderName} wants to directly pair with you.",
|
||||
Type = NotificationType.PairRequest,
|
||||
Duration = TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
|
||||
SoundEffectId = GetPairRequestSoundId(),
|
||||
Actions = CreatePairRequestActions(onAccept, onDecline)
|
||||
};
|
||||
|
||||
if (notification.SoundEffectId.HasValue)
|
||||
{
|
||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||
}
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
}
|
||||
|
||||
private uint? GetPairRequestSoundId() =>
|
||||
!_configService.Current.DisablePairRequestSound ? _configService.Current.PairRequestSoundId : null;
|
||||
|
||||
private List<LightlessNotificationAction> CreatePairRequestActions(Action onAccept, Action onDecline)
|
||||
{
|
||||
return new List<LightlessNotificationAction>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = "accept",
|
||||
Label = "Accept",
|
||||
Icon = FontAwesomeIcon.Check,
|
||||
Color = UIColors.Get("LightlessGreen"),
|
||||
IsPrimary = true,
|
||||
OnClick = (n) =>
|
||||
{
|
||||
_logger.LogInformation("Pair request accepted");
|
||||
onAccept();
|
||||
DismissNotification(n);
|
||||
}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = "decline",
|
||||
Label = "Decline",
|
||||
Icon = FontAwesomeIcon.Times,
|
||||
Color = UIColors.Get("DimRed"),
|
||||
IsDestructive = true,
|
||||
OnClick = (n) =>
|
||||
{
|
||||
_logger.LogInformation("Pair request declined");
|
||||
onDecline();
|
||||
DismissNotification(n);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null)
|
||||
{
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Title = "Download Complete",
|
||||
Message = FormatDownloadCompleteMessage(fileName, fileCount),
|
||||
Type = NotificationType.Info,
|
||||
Duration = TimeSpan.FromSeconds(8),
|
||||
Actions = CreateDownloadCompleteActions(onOpenFolder),
|
||||
SoundEffectId = NotificationSounds.DownloadComplete
|
||||
};
|
||||
|
||||
if (notification.SoundEffectId.HasValue)
|
||||
{
|
||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||
}
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
}
|
||||
|
||||
private string FormatDownloadCompleteMessage(string fileName, int fileCount) =>
|
||||
fileCount > 1
|
||||
? $"Downloaded {fileCount} files successfully."
|
||||
: $"Downloaded {fileName} successfully.";
|
||||
|
||||
private List<LightlessNotificationAction> CreateDownloadCompleteActions(Action? onOpenFolder)
|
||||
{
|
||||
var actions = new List<LightlessNotificationAction>();
|
||||
|
||||
if (onOpenFolder != null)
|
||||
{
|
||||
actions.Add(new LightlessNotificationAction
|
||||
{
|
||||
Id = "open_folder",
|
||||
Label = "Open Folder",
|
||||
Icon = FontAwesomeIcon.FolderOpen,
|
||||
Color = UIColors.Get("LightlessBlue"),
|
||||
OnClick = (n) =>
|
||||
{
|
||||
onOpenFolder();
|
||||
DismissNotification(n);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null,
|
||||
Action? onViewLog = null)
|
||||
{
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Title = title,
|
||||
Message = FormatErrorMessage(message, exception),
|
||||
Type = NotificationType.Error,
|
||||
Duration = TimeSpan.FromSeconds(15),
|
||||
Actions = CreateErrorActions(onRetry, onViewLog),
|
||||
SoundEffectId = NotificationSounds.Error
|
||||
};
|
||||
|
||||
if (notification.SoundEffectId.HasValue)
|
||||
{
|
||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||
}
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
}
|
||||
|
||||
private string FormatErrorMessage(string message, Exception? exception) =>
|
||||
exception != null ? $"{message}\n\nError: {exception.Message}" : message;
|
||||
|
||||
private List<LightlessNotificationAction> CreateErrorActions(Action? onRetry, Action? onViewLog)
|
||||
{
|
||||
var actions = new List<LightlessNotificationAction>();
|
||||
|
||||
if (onRetry != null)
|
||||
{
|
||||
actions.Add(new LightlessNotificationAction
|
||||
{
|
||||
Id = "retry",
|
||||
Label = "Retry",
|
||||
Icon = FontAwesomeIcon.Redo,
|
||||
Color = UIColors.Get("LightlessBlue"),
|
||||
OnClick = (n) =>
|
||||
{
|
||||
onRetry();
|
||||
DismissNotification(n);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (onViewLog != null)
|
||||
{
|
||||
actions.Add(new LightlessNotificationAction
|
||||
{
|
||||
Id = "view_log",
|
||||
Label = "View Log",
|
||||
Icon = FontAwesomeIcon.FileAlt,
|
||||
Color = UIColors.Get("LightlessYellow"),
|
||||
OnClick = (n) => onViewLog()
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus,
|
||||
int queueWaiting = 0)
|
||||
{
|
||||
var userDownloads = downloadStatus.Where(x => x.playerName != "Pair Queue").ToList();
|
||||
var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.progress) : 0f;
|
||||
var message = BuildPairDownloadMessage(userDownloads, queueWaiting);
|
||||
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Id = "pair_download_progress",
|
||||
Title = "Downloading Pair Data",
|
||||
Message = message,
|
||||
Type = NotificationType.Download,
|
||||
Duration = TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
|
||||
ShowProgress = true,
|
||||
Progress = totalProgress
|
||||
};
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
|
||||
if (AreAllDownloadsCompleted(userDownloads))
|
||||
{
|
||||
DismissPairDownloadNotification();
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildPairDownloadMessage(List<(string playerName, float progress, string status)> userDownloads,
|
||||
int queueWaiting)
|
||||
{
|
||||
var messageParts = new List<string>();
|
||||
|
||||
if (queueWaiting > 0)
|
||||
{
|
||||
messageParts.Add($"Queue: {queueWaiting} waiting");
|
||||
}
|
||||
|
||||
if (userDownloads.Count > 0)
|
||||
{
|
||||
var completedCount = userDownloads.Count(x => x.progress >= 1.0f);
|
||||
messageParts.Add($"Progress: {completedCount}/{userDownloads.Count} completed");
|
||||
}
|
||||
|
||||
var activeDownloadLines = BuildActiveDownloadLines(userDownloads);
|
||||
if (!string.IsNullOrEmpty(activeDownloadLines))
|
||||
{
|
||||
messageParts.Add(activeDownloadLines);
|
||||
}
|
||||
|
||||
return string.Join("\n", messageParts);
|
||||
}
|
||||
|
||||
private string BuildActiveDownloadLines(List<(string playerName, float progress, string status)> userDownloads)
|
||||
{
|
||||
var activeDownloads = userDownloads
|
||||
.Where(x => x.progress < 1.0f)
|
||||
.Take(_configService.Current.MaxConcurrentPairApplications);
|
||||
|
||||
if (!activeDownloads.Any()) return string.Empty;
|
||||
|
||||
return string.Join("\n", activeDownloads.Select(x => $"• {x.playerName}: {FormatDownloadStatus(x)}"));
|
||||
}
|
||||
|
||||
private string FormatDownloadStatus((string playerName, float progress, string status) download) =>
|
||||
download.status switch
|
||||
{
|
||||
"downloading" => $"{download.progress:P0}",
|
||||
"decompressing" => "decompressing",
|
||||
"queued" => "queued",
|
||||
"waiting" => "waiting for slot",
|
||||
_ => download.status
|
||||
};
|
||||
|
||||
private bool AreAllDownloadsCompleted(List<(string playerName, float progress, string status)> userDownloads) =>
|
||||
userDownloads.Any() && userDownloads.All(x => x.progress >= 1.0f);
|
||||
|
||||
public void DismissPairDownloadNotification() =>
|
||||
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
||||
|
||||
private TimeSpan GetDefaultDurationForType(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => TimeSpan.FromSeconds(_configService.Current.InfoNotificationDurationSeconds),
|
||||
NotificationType.Warning => TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds),
|
||||
NotificationType.Error => TimeSpan.FromSeconds(_configService.Current.ErrorNotificationDurationSeconds),
|
||||
NotificationType.PairRequest => TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
|
||||
NotificationType.Download => TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
|
||||
_ => TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
private uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId)
|
||||
{
|
||||
if (overrideSoundId.HasValue) return overrideSoundId;
|
||||
if (IsSoundDisabledForType(type)) return null;
|
||||
return GetConfiguredSoundForType(type);
|
||||
}
|
||||
|
||||
private bool IsSoundDisabledForType(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => _configService.Current.DisableInfoSound,
|
||||
NotificationType.Warning => _configService.Current.DisableWarningSound,
|
||||
NotificationType.Error => _configService.Current.DisableErrorSound,
|
||||
NotificationType.Download => _configService.Current.DisableDownloadSound,
|
||||
_ => false
|
||||
};
|
||||
|
||||
private uint GetConfiguredSoundForType(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => _configService.Current.CustomInfoSoundId,
|
||||
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
|
||||
NotificationType.Error => _configService.Current.CustomErrorSoundId,
|
||||
NotificationType.Download => _configService.Current.DownloadSoundId,
|
||||
_ => NotificationSounds.GetDefaultSound(type)
|
||||
};
|
||||
|
||||
private void PlayNotificationSound(uint soundEffectId)
|
||||
{
|
||||
try
|
||||
{
|
||||
UIGlobals.PlayChatSoundEffect(soundEffectId);
|
||||
_logger.LogDebug("Played notification sound effect {SoundId} via ChatGui", soundEffectId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to play notification sound effect {SoundId}", soundEffectId);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleNotificationMessage(NotificationMessage msg)
|
||||
{
|
||||
_logger.LogInformation("{msg}", msg.ToString());
|
||||
if (!_dalamudUtilService.IsLoggedIn) return;
|
||||
|
||||
var location = GetNotificationLocation(msg.Type);
|
||||
ShowNotificationLocationBased(msg, location);
|
||||
}
|
||||
|
||||
private NotificationLocation GetNotificationLocation(NotificationType type) =>
|
||||
_configService.Current.UseLightlessNotifications
|
||||
? GetLightlessNotificationLocation(type)
|
||||
: GetClassicNotificationLocation(type);
|
||||
|
||||
private NotificationLocation GetLightlessNotificationLocation(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => _configService.Current.LightlessInfoNotification,
|
||||
NotificationType.Warning => _configService.Current.LightlessWarningNotification,
|
||||
NotificationType.Error => _configService.Current.LightlessErrorNotification,
|
||||
NotificationType.PairRequest => _configService.Current.LightlessPairRequestNotification,
|
||||
NotificationType.Download => _configService.Current.LightlessDownloadNotification,
|
||||
_ => NotificationLocation.LightlessUi
|
||||
};
|
||||
|
||||
private NotificationLocation GetClassicNotificationLocation(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => _configService.Current.InfoNotification,
|
||||
NotificationType.Warning => _configService.Current.WarningNotification,
|
||||
NotificationType.Error => _configService.Current.ErrorNotification,
|
||||
NotificationType.PairRequest => NotificationLocation.Toast,
|
||||
NotificationType.Download => NotificationLocation.Toast,
|
||||
_ => NotificationLocation.Nowhere
|
||||
};
|
||||
|
||||
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
|
||||
{
|
||||
switch (location)
|
||||
{
|
||||
case NotificationLocation.Toast:
|
||||
ShowToast(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Chat:
|
||||
ShowChat(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Both:
|
||||
ShowToast(msg);
|
||||
ShowChat(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.LightlessUi:
|
||||
ShowLightlessNotification(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.ChatAndLightlessUi:
|
||||
ShowChat(msg);
|
||||
ShowLightlessNotification(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Nowhere:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowLightlessNotification(NotificationMessage msg)
|
||||
{
|
||||
var duration = msg.TimeShownOnScreen ?? GetDefaultDurationForType(msg.Type);
|
||||
ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, null);
|
||||
}
|
||||
|
||||
private void ShowToast(NotificationMessage msg)
|
||||
{
|
||||
var dalamudType = ConvertToDalamudNotificationType(msg.Type);
|
||||
|
||||
_notificationManager.AddNotification(new Notification()
|
||||
{
|
||||
Content = msg.Message ?? string.Empty,
|
||||
Title = msg.Title,
|
||||
Type = dalamudType,
|
||||
Minimized = false,
|
||||
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
|
||||
});
|
||||
}
|
||||
|
||||
private Dalamud.Interface.ImGuiNotification.NotificationType
|
||||
ConvertToDalamudNotificationType(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
|
||||
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
|
||||
};
|
||||
|
||||
private void ShowChat(NotificationMessage msg)
|
||||
{
|
||||
switch (msg.Type)
|
||||
@@ -75,67 +508,54 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowNotification(NotificationMessage msg)
|
||||
private void PrintErrorChat(string? message)
|
||||
{
|
||||
Logger.LogInformation("{msg}", msg.ToString());
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
|
||||
_chatGui.PrintError(se.BuiltString);
|
||||
}
|
||||
|
||||
if (!_dalamudUtilService.IsLoggedIn) return;
|
||||
private void PrintInfoChat(string? message)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ")
|
||||
.AddItalics(message ?? string.Empty);
|
||||
_chatGui.Print(se.BuiltString);
|
||||
}
|
||||
|
||||
switch (msg.Type)
|
||||
private void PrintWarnChat(string? message)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
|
||||
.AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
|
||||
_chatGui.Print(se.BuiltString);
|
||||
}
|
||||
|
||||
private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _)
|
||||
{
|
||||
var activeRequests = _pairRequestService.GetActiveRequests();
|
||||
var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet();
|
||||
|
||||
// Dismiss notifications for requests that are no longer active
|
||||
var notificationsToRemove = _shownPairRequestNotifications
|
||||
.Where(hashedCid => !activeRequestIds.Contains(hashedCid))
|
||||
.ToList();
|
||||
|
||||
foreach (var hashedCid in notificationsToRemove)
|
||||
{
|
||||
case NotificationType.Info:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
|
||||
break;
|
||||
var notificationId = $"pair_request_{hashedCid}";
|
||||
Mediator.Publish(new LightlessNotificationDismissMessage(notificationId));
|
||||
_shownPairRequestNotifications.Remove(hashedCid);
|
||||
}
|
||||
|
||||
case NotificationType.Warning:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
|
||||
break;
|
||||
|
||||
case NotificationType.Error:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
|
||||
break;
|
||||
// Show/update notifications for all active requests
|
||||
foreach (var request in activeRequests)
|
||||
{
|
||||
_shownPairRequestNotifications.Add(request.HashedCid);
|
||||
ShowPairRequestNotification(
|
||||
request.DisplayName,
|
||||
request.HashedCid,
|
||||
() => _pairRequestService.AcceptPairRequest(request.HashedCid, request.DisplayName),
|
||||
() => _pairRequestService.DeclinePairRequest(request.HashedCid)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
|
||||
{
|
||||
switch (location)
|
||||
{
|
||||
case NotificationLocation.Toast:
|
||||
ShowToast(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Chat:
|
||||
ShowChat(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Both:
|
||||
ShowToast(msg);
|
||||
ShowChat(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Nowhere:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowToast(NotificationMessage msg)
|
||||
{
|
||||
Dalamud.Interface.ImGuiNotification.NotificationType dalamudType = msg.Type switch
|
||||
{
|
||||
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
|
||||
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||
NotificationType.Info => Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
||||
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
|
||||
};
|
||||
|
||||
_notificationManager.AddNotification(new Notification()
|
||||
{
|
||||
Content = msg.Message ?? string.Empty,
|
||||
Title = msg.Title,
|
||||
Type = dalamudType,
|
||||
Minimized = false,
|
||||
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
227
LightlessSync/Services/PairRequestService.cs
Normal file
227
LightlessSync/Services/PairRequestService.cs
Normal file
@@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
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 Lazy<WebAPI.ApiController> _apiController;
|
||||
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, Lazy<WebAPI.ApiController> apiController)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_pairManager = pairManager;
|
||||
_apiController = apiController;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void AcceptPairRequest(string hashedCid, string displayName)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _apiController.Value.TryPairWithContentId(hashedCid).ConfigureAwait(false);
|
||||
RemoveRequest(hashedCid);
|
||||
|
||||
var displayText = string.IsNullOrEmpty(displayName) ? hashedCid : displayName;
|
||||
Mediator.Publish(new NotificationMessage(
|
||||
"Pair request accepted",
|
||||
$"Sent a pair request back to {displayText}.",
|
||||
NotificationType.Info,
|
||||
TimeSpan.FromSeconds(3)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to accept pair request for {HashedCid}", hashedCid);
|
||||
Mediator.Publish(new NotificationMessage(
|
||||
"Failed to Accept Pair Request",
|
||||
ex.Message,
|
||||
NotificationType.Error,
|
||||
TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void DeclinePairRequest(string hashedCid)
|
||||
{
|
||||
RemoveRequest(hashedCid);
|
||||
Logger.LogDebug("Declined pair request from {HashedCid}", hashedCid);
|
||||
}
|
||||
|
||||
private record struct PairRequestEntry(string HashedCid, string MessageTemplate, DateTime ReceivedAt);
|
||||
|
||||
public readonly record struct PairRequestDisplay(string HashedCid, string DisplayName, string Message, DateTime ReceivedAt);
|
||||
}
|
||||
@@ -23,15 +23,18 @@ public class ServerConfigurationManager
|
||||
private readonly ILogger<ServerConfigurationManager> _logger;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly NotesConfigService _notesConfig;
|
||||
private readonly ServerTagConfigService _serverTagConfig;
|
||||
private readonly PairTagConfigService _pairTagConfig;
|
||||
private readonly SyncshellTagConfigService _syncshellTagConfig;
|
||||
private readonly int _maxCharactersFolder = 20;
|
||||
|
||||
public ServerConfigurationManager(ILogger<ServerConfigurationManager> logger, ServerConfigService configService,
|
||||
ServerTagConfigService serverTagConfig, NotesConfigService notesConfig, DalamudUtilService dalamudUtil,
|
||||
PairTagConfigService pairTagConfig, SyncshellTagConfigService syncshellTagConfig, NotesConfigService notesConfig, DalamudUtilService dalamudUtil,
|
||||
LightlessConfigService lightlessConfigService, HttpClient httpClient, LightlessMediator lightlessMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_serverTagConfig = serverTagConfig;
|
||||
_pairTagConfig = pairTagConfig;
|
||||
_syncshellTagConfig = syncshellTagConfig;
|
||||
_notesConfig = notesConfig;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_lightlessConfigService = lightlessConfigService;
|
||||
@@ -258,7 +261,7 @@ public class ServerConfigurationManager
|
||||
{
|
||||
if (serverSelectionIndex == -1) serverSelectionIndex = CurrentServerIndex;
|
||||
var server = GetServerByIndex(serverSelectionIndex);
|
||||
if (server.Authentications.Any(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), StringComparison.Ordinal)
|
||||
if (server.Authentications.Exists(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), StringComparison.Ordinal)
|
||||
&& c.WorldId == _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult()))
|
||||
return;
|
||||
|
||||
@@ -277,15 +280,15 @@ public class ServerConfigurationManager
|
||||
var server = GetServerByIndex(serverSelectionIndex);
|
||||
server.Authentications.Add(new Authentication()
|
||||
{
|
||||
SecretKeyIdx = server.SecretKeys.Any() ? server.SecretKeys.First().Key : -1,
|
||||
SecretKeyIdx = server.SecretKeys.Count != 0 ? server.SecretKeys.First().Key : -1,
|
||||
});
|
||||
Save();
|
||||
}
|
||||
|
||||
internal void AddOpenPairTag(string tag)
|
||||
{
|
||||
CurrentServerTagStorage().OpenPairTags.Add(tag);
|
||||
_serverTagConfig.Save();
|
||||
CurrentPairTagStorage().OpenPairTags.Add(tag);
|
||||
_pairTagConfig.Save();
|
||||
}
|
||||
|
||||
internal void AddServer(ServerStorage serverStorage)
|
||||
@@ -294,36 +297,79 @@ public class ServerConfigurationManager
|
||||
Save();
|
||||
}
|
||||
|
||||
internal void AddTag(string tag)
|
||||
internal void AddPairTag(string tag)
|
||||
{
|
||||
CurrentServerTagStorage().ServerAvailablePairTags.Add(tag);
|
||||
_serverTagConfig.Save();
|
||||
_lightlessMediator.Publish(new RefreshUiMessage());
|
||||
if (tag.Length <= _maxCharactersFolder)
|
||||
{
|
||||
CurrentPairTagStorage().ServerAvailablePairTags.Add(tag);
|
||||
_pairTagConfig.Save();
|
||||
_lightlessMediator.Publish(new RefreshUiMessage());
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Couldn't save/add {tag}. Name too long to be saved", tag);
|
||||
}
|
||||
}
|
||||
|
||||
internal void AddSyncshellTag(string tag)
|
||||
{
|
||||
if (tag.Length <= _maxCharactersFolder)
|
||||
{
|
||||
CurrentSyncshellTagStorage().ServerAvailableSyncshellTags.Add(tag);
|
||||
_syncshellTagConfig.Save();
|
||||
_lightlessMediator.Publish(new RefreshUiMessage());
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Couldn't save/add {tag}. Name too long to be saved", tag);
|
||||
}
|
||||
}
|
||||
|
||||
internal void AddTagForUid(string uid, string tagName)
|
||||
{
|
||||
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
|
||||
if (CurrentPairTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
|
||||
{
|
||||
tags.Add(tagName);
|
||||
_lightlessMediator.Publish(new RefreshUiMessage());
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentServerTagStorage().UidServerPairedUserTags[uid] = [tagName];
|
||||
CurrentPairTagStorage().UidServerPairedUserTags[uid] = [tagName];
|
||||
}
|
||||
|
||||
_serverTagConfig.Save();
|
||||
_pairTagConfig.Save();
|
||||
}
|
||||
|
||||
internal bool ContainsOpenPairTag(string tag)
|
||||
internal void AddTagForSyncshell(string syncshellName, string tagName)
|
||||
{
|
||||
return CurrentServerTagStorage().OpenPairTags.Contains(tag);
|
||||
if (CurrentSyncshellTagStorage().SyncshellPairedTags.TryGetValue(syncshellName, out var tags))
|
||||
{
|
||||
tags.Add(tagName);
|
||||
_lightlessMediator.Publish(new RefreshUiMessage());
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentSyncshellTagStorage().SyncshellPairedTags[syncshellName] = [tagName];
|
||||
}
|
||||
|
||||
_syncshellTagConfig.Save();
|
||||
}
|
||||
|
||||
internal bool ContainsTag(string uid, string tag)
|
||||
internal bool ContainsOpenPairTag(string tag) => CurrentPairTagStorage().OpenPairTags.Contains(tag);
|
||||
|
||||
internal bool ContainsPairTag(string uid, string tag)
|
||||
{
|
||||
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
|
||||
if (CurrentPairTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
|
||||
{
|
||||
return tags.Contains(tag, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal bool ContainsSyncshellTag(string name, string tag)
|
||||
{
|
||||
if (CurrentSyncshellTagStorage().SyncshellPairedTags.TryGetValue(name, out var tags))
|
||||
{
|
||||
return tags.Contains(tag, StringComparer.Ordinal);
|
||||
}
|
||||
@@ -364,30 +410,19 @@ public class ServerConfigurationManager
|
||||
return null;
|
||||
}
|
||||
|
||||
internal HashSet<string> GetServerAvailablePairTags()
|
||||
{
|
||||
return CurrentServerTagStorage().ServerAvailablePairTags;
|
||||
}
|
||||
internal HashSet<string> GetServerAvailablePairTags() => CurrentPairTagStorage().ServerAvailablePairTags;
|
||||
|
||||
internal Dictionary<string, List<string>> GetUidServerPairedUserTags()
|
||||
{
|
||||
return CurrentServerTagStorage().UidServerPairedUserTags;
|
||||
}
|
||||
internal HashSet<string> GetServerAvailableSyncshellTags() => CurrentSyncshellTagStorage().ServerAvailableSyncshellTags;
|
||||
|
||||
internal HashSet<string> GetUidsForTag(string tag)
|
||||
{
|
||||
return CurrentServerTagStorage().UidServerPairedUserTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal);
|
||||
}
|
||||
internal Dictionary<string, List<string>> GetUidServerPairedUserTags() => CurrentPairTagStorage().UidServerPairedUserTags;
|
||||
|
||||
internal bool HasTags(string uid)
|
||||
{
|
||||
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
|
||||
{
|
||||
return tags.Any();
|
||||
}
|
||||
internal HashSet<string> GetUidsForPairTag(string tag) => CurrentPairTagStorage().UidServerPairedUserTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
return false;
|
||||
}
|
||||
internal HashSet<string> GetNamesForSyncshellTag(string tag) => CurrentSyncshellTagStorage().SyncshellPairedTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
internal bool HasPairTags(string uid) => CurrentPairTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags) && tags.Count != 0;
|
||||
|
||||
internal bool HasSyncshellTags(string name) => CurrentSyncshellTagStorage().SyncshellPairedTags.TryGetValue(name, out var tags) && tags.Count != 0;
|
||||
|
||||
internal void RemoveCharacterFromServer(int serverSelectionIndex, Authentication item)
|
||||
{
|
||||
@@ -398,51 +433,96 @@ public class ServerConfigurationManager
|
||||
|
||||
internal void RemoveOpenPairTag(string tag)
|
||||
{
|
||||
CurrentServerTagStorage().OpenPairTags.Remove(tag);
|
||||
_serverTagConfig.Save();
|
||||
CurrentPairTagStorage().OpenPairTags.Remove(tag);
|
||||
_pairTagConfig.Save();
|
||||
}
|
||||
|
||||
internal void RemoveTag(string tag)
|
||||
internal void RemovePairTag(string tag)
|
||||
{
|
||||
CurrentServerTagStorage().ServerAvailablePairTags.Remove(tag);
|
||||
foreach (var uid in GetUidsForTag(tag))
|
||||
{
|
||||
RemoveTagForUid(uid, tag, save: false);
|
||||
}
|
||||
_serverTagConfig.Save();
|
||||
RemoveTag(CurrentPairTagStorage().ServerAvailablePairTags, tag);
|
||||
_pairTagConfig.Save();
|
||||
_lightlessMediator.Publish(new RefreshUiMessage());
|
||||
}
|
||||
|
||||
internal void RemoveSyncshellTag(string tag)
|
||||
{
|
||||
RemoveTag(CurrentSyncshellTagStorage().ServerAvailableSyncshellTags, tag, true);
|
||||
_syncshellTagConfig.Save();
|
||||
_lightlessMediator.Publish(new RefreshUiMessage());
|
||||
}
|
||||
|
||||
internal void RemoveTag(HashSet<string> storage, string tag, bool syncshell = false)
|
||||
{
|
||||
storage.Remove(tag);
|
||||
if (syncshell)
|
||||
{
|
||||
foreach (var uid in GetNamesForSyncshellTag(tag))
|
||||
{
|
||||
RemoveTagForSyncshell(uid, tag, save: false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var uid in GetUidsForPairTag(tag))
|
||||
{
|
||||
RemoveTagForUid(uid, tag, save: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void RemoveTagForUid(string uid, string tagName, bool save = true)
|
||||
{
|
||||
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
|
||||
if (CurrentPairTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
|
||||
{
|
||||
tags.Remove(tagName);
|
||||
|
||||
if (save)
|
||||
{
|
||||
_serverTagConfig.Save();
|
||||
_pairTagConfig.Save();
|
||||
_lightlessMediator.Publish(new RefreshUiMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void RenameTag(string oldName, string newName)
|
||||
internal void RemoveTagForSyncshell(string name, string tagName, bool save = true)
|
||||
{
|
||||
CurrentServerTagStorage().ServerAvailablePairTags.Remove(oldName);
|
||||
CurrentServerTagStorage().ServerAvailablePairTags.Add(newName);
|
||||
foreach (var existingTags in CurrentServerTagStorage().UidServerPairedUserTags.Select(k => k.Value))
|
||||
if (CurrentSyncshellTagStorage().SyncshellPairedTags.TryGetValue(name, out var tags))
|
||||
{
|
||||
if (existingTags.Remove(oldName))
|
||||
existingTags.Add(newName);
|
||||
tags.Remove(tagName);
|
||||
|
||||
if (save)
|
||||
{
|
||||
_syncshellTagConfig.Save();
|
||||
_lightlessMediator.Publish(new RefreshUiMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void SaveNotes()
|
||||
internal void RenamePairTag(string oldName, string newName) => RenameTag(CurrentPairTagStorage().UidServerPairedUserTags, CurrentPairTagStorage().ServerAvailablePairTags, oldName, newName);
|
||||
|
||||
internal void RenameSyncshellTag(string oldName, string newName) => RenameTag(CurrentSyncshellTagStorage().SyncshellPairedTags, CurrentSyncshellTagStorage().ServerAvailableSyncshellTags, oldName, newName);
|
||||
|
||||
internal void RenameTag(Dictionary<string, List<string>> tags, HashSet<string> storage, string oldName, string newName)
|
||||
{
|
||||
_notesConfig.Save();
|
||||
if (newName.Length < _maxCharactersFolder)
|
||||
{
|
||||
storage.Remove(oldName);
|
||||
storage.Add(newName);
|
||||
foreach (var existingTags in tags.Select(k => k.Value))
|
||||
{
|
||||
if (existingTags.Remove(oldName))
|
||||
existingTags.Add(newName);
|
||||
}
|
||||
_lightlessMediator.Publish(new RefreshUiMessage());
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Couldn't save/add {tag}. Name too long to be saved", newName);
|
||||
}
|
||||
}
|
||||
|
||||
internal void SaveNotes() => _notesConfig.Save();
|
||||
|
||||
internal void SetNoteForGid(string gid, string note, bool save = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(gid)) return;
|
||||
@@ -476,10 +556,16 @@ public class ServerConfigurationManager
|
||||
return _notesConfig.Current.ServerNotes[CurrentApiUrl];
|
||||
}
|
||||
|
||||
private ServerTagStorage CurrentServerTagStorage()
|
||||
private PairTagStorage CurrentPairTagStorage()
|
||||
{
|
||||
TryCreateCurrentServerTagStorage();
|
||||
return _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl];
|
||||
TryCreateCurrentPairTagStorage();
|
||||
return _pairTagConfig.Current.ServerTagStorage[CurrentApiUrl];
|
||||
}
|
||||
|
||||
private SyncshellTagStorage CurrentSyncshellTagStorage()
|
||||
{
|
||||
TryCreateCurrentSyncshellTagStorage();
|
||||
return _syncshellTagConfig.Current.ServerTagStorage[CurrentApiUrl];
|
||||
}
|
||||
|
||||
private void EnsureMainExists()
|
||||
@@ -499,11 +585,19 @@ public class ServerConfigurationManager
|
||||
}
|
||||
}
|
||||
|
||||
private void TryCreateCurrentServerTagStorage()
|
||||
private void TryCreateCurrentPairTagStorage()
|
||||
{
|
||||
if (!_serverTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl))
|
||||
if (!_pairTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl))
|
||||
{
|
||||
_serverTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new();
|
||||
_pairTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new();
|
||||
}
|
||||
}
|
||||
|
||||
private void TryCreateCurrentSyncshellTagStorage()
|
||||
{
|
||||
if (!_syncshellTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl))
|
||||
{
|
||||
_syncshellTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,8 +607,9 @@ public class ServerConfigurationManager
|
||||
{
|
||||
var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
|
||||
var oauthCheckUri = LightlessAuth.GetUIDsFullPath(new Uri(baseUri));
|
||||
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
var response = await _httpClient.GetAsync(oauthCheckUri).ConfigureAwait(false);
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, oauthCheckUri);
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
using var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
|
||||
var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
return await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(responseStream).ConfigureAwait(false) ?? [];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Components.Popup;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Components.Popup;
|
||||
using LightlessSync.UI.Style;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
@@ -23,7 +23,8 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
||||
LightlessConfigService lightlessConfigService, WindowSystem windowSystem,
|
||||
IEnumerable<WindowMediatorSubscriberBase> windows,
|
||||
UiFactory uiFactory, FileDialogManager fileDialogManager,
|
||||
LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
|
||||
LightlessMediator lightlessMediator,
|
||||
NotificationService notificationService) : base(logger, lightlessMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_logger.LogTrace("Creating {type}", GetType().Name);
|
||||
@@ -120,7 +121,15 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
||||
|
||||
private void Draw()
|
||||
{
|
||||
_windowSystem.Draw();
|
||||
_fileDialogManager.Draw();
|
||||
MainStyle.PushStyle();
|
||||
try
|
||||
{
|
||||
_windowSystem.Draw();
|
||||
_fileDialogManager.Draw();
|
||||
}
|
||||
finally
|
||||
{
|
||||
MainStyle.PopStyle();
|
||||
}
|
||||
}
|
||||
}
|
||||
437
LightlessSync/UI/BroadcastUI.cs
Normal file
437
LightlessSync/UI/BroadcastUI.cs
Normal file
@@ -0,0 +1,437 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
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’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();
|
||||
}
|
||||
|
||||
var toggleButtonHeight = ImGui.GetItemRectSize().Y;
|
||||
|
||||
if (isOnCooldown || !_broadcastService.IsLightFinderAvailable)
|
||||
ImGui.EndDisabled();
|
||||
|
||||
ImGui.PopStyleColor();
|
||||
ImGui.PopStyleVar();
|
||||
|
||||
ImGui.SameLine();
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Cog, toggleButtonHeight))
|
||||
{
|
||||
Mediator.Publish(new OpenLightfinderSettingsMessage());
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.TextUnformatted("Open Lightfinder settings.");
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,187 +8,187 @@ namespace LightlessSync.UI;
|
||||
|
||||
internal sealed partial class CharaDataHubUi
|
||||
{
|
||||
private static string GetAccessTypeString(AccessTypeDto dto) => dto switch
|
||||
{
|
||||
AccessTypeDto.AllPairs => "All Pairs",
|
||||
AccessTypeDto.ClosePairs => "Direct Pairs",
|
||||
AccessTypeDto.Individuals => "Specified",
|
||||
AccessTypeDto.Public => "Everyone"
|
||||
};
|
||||
private static string GetAccessTypeString(AccessTypeDto dto) => dto switch
|
||||
{
|
||||
AccessTypeDto.AllPairs => "All Pairs",
|
||||
AccessTypeDto.ClosePairs => "Direct Pairs",
|
||||
AccessTypeDto.Individuals => "Specified",
|
||||
AccessTypeDto.Public => "Everyone"
|
||||
};
|
||||
|
||||
private static string GetShareTypeString(ShareTypeDto dto) => dto switch
|
||||
{
|
||||
ShareTypeDto.Private => "Code Only",
|
||||
ShareTypeDto.Shared => "Shared"
|
||||
};
|
||||
private static string GetShareTypeString(ShareTypeDto dto) => dto switch
|
||||
{
|
||||
ShareTypeDto.Private => "Code Only",
|
||||
ShareTypeDto.Shared => "Shared"
|
||||
};
|
||||
|
||||
private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry)
|
||||
{
|
||||
if (!poseEntry.HasWorldData) return "This Pose has no world data attached.";
|
||||
return poseEntry.WorldDataDescriptor;
|
||||
}
|
||||
private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry)
|
||||
{
|
||||
if (!poseEntry.HasWorldData) return "This Pose has no world data attached.";
|
||||
return poseEntry.WorldDataDescriptor;
|
||||
}
|
||||
|
||||
|
||||
private void GposeMetaInfoAction(Action<CharaDataMetaInfoExtendedDto?> gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
private void GposeMetaInfoAction(Action<CharaDataMetaInfoExtendedDto?> gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine(actionDescription);
|
||||
bool isDisabled = false;
|
||||
sb.AppendLine(actionDescription);
|
||||
bool isDisabled = false;
|
||||
|
||||
void AddErrorStart(StringBuilder sb)
|
||||
{
|
||||
sb.Append(UiSharedService.TooltipSeparator);
|
||||
sb.AppendLine("Cannot execute:");
|
||||
}
|
||||
void AddErrorStart(StringBuilder sb)
|
||||
{
|
||||
sb.Append(UiSharedService.TooltipSeparator);
|
||||
sb.AppendLine("Cannot execute:");
|
||||
}
|
||||
|
||||
if (dto == null)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- No metainfo present");
|
||||
isDisabled = true;
|
||||
}
|
||||
if (!dto?.CanBeDownloaded ?? false)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- Character is not downloadable");
|
||||
isDisabled = true;
|
||||
}
|
||||
if (!_uiSharedService.IsInGpose)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- Requires to be in GPose");
|
||||
isDisabled = true;
|
||||
}
|
||||
if (!hasValidGposeTarget && !isSpawning)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- Requires a valid GPose target");
|
||||
isDisabled = true;
|
||||
}
|
||||
if (isSpawning && !_charaDataManager.BrioAvailable)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- Requires Brio to be installed.");
|
||||
isDisabled = true;
|
||||
}
|
||||
if (dto == null)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- No metainfo present");
|
||||
isDisabled = true;
|
||||
}
|
||||
if (!dto?.CanBeDownloaded ?? false)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- Character is not downloadable");
|
||||
isDisabled = true;
|
||||
}
|
||||
if (!_uiSharedService.IsInGpose)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- Requires to be in GPose");
|
||||
isDisabled = true;
|
||||
}
|
||||
if (!hasValidGposeTarget && !isSpawning)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- Requires a valid GPose target");
|
||||
isDisabled = true;
|
||||
}
|
||||
if (isSpawning && !_charaDataManager.BrioAvailable)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- Requires Brio to be installed.");
|
||||
isDisabled = true;
|
||||
}
|
||||
|
||||
using (ImRaii.Group())
|
||||
{
|
||||
using var dis = ImRaii.Disabled(isDisabled);
|
||||
gposeActionDraw.Invoke(dto);
|
||||
}
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
UiSharedService.AttachToolTip(sb.ToString());
|
||||
}
|
||||
}
|
||||
using (ImRaii.Group())
|
||||
{
|
||||
using var dis = ImRaii.Disabled(isDisabled);
|
||||
gposeActionDraw.Invoke(dto);
|
||||
}
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
UiSharedService.AttachToolTip(sb.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void GposePoseAction(Action poseActionDraw, string poseDescription, bool hasValidGposeTarget)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
private void GposePoseAction(Action poseActionDraw, string poseDescription, bool hasValidGposeTarget)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine(poseDescription);
|
||||
bool isDisabled = false;
|
||||
sb.AppendLine(poseDescription);
|
||||
bool isDisabled = false;
|
||||
|
||||
void AddErrorStart(StringBuilder sb)
|
||||
{
|
||||
sb.Append(UiSharedService.TooltipSeparator);
|
||||
sb.AppendLine("Cannot execute:");
|
||||
}
|
||||
void AddErrorStart(StringBuilder sb)
|
||||
{
|
||||
sb.Append(UiSharedService.TooltipSeparator);
|
||||
sb.AppendLine("Cannot execute:");
|
||||
}
|
||||
|
||||
if (!_uiSharedService.IsInGpose)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- Requires to be in GPose");
|
||||
isDisabled = true;
|
||||
}
|
||||
if (!hasValidGposeTarget)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- Requires a valid GPose target");
|
||||
isDisabled = true;
|
||||
}
|
||||
if (!_charaDataManager.BrioAvailable)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- Requires Brio to be installed.");
|
||||
isDisabled = true;
|
||||
}
|
||||
if (!_uiSharedService.IsInGpose)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- Requires to be in GPose");
|
||||
isDisabled = true;
|
||||
}
|
||||
if (!hasValidGposeTarget)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- Requires a valid GPose target");
|
||||
isDisabled = true;
|
||||
}
|
||||
if (!_charaDataManager.BrioAvailable)
|
||||
{
|
||||
if (!isDisabled) AddErrorStart(sb);
|
||||
sb.AppendLine("- Requires Brio to be installed.");
|
||||
isDisabled = true;
|
||||
}
|
||||
|
||||
using (ImRaii.Group())
|
||||
{
|
||||
using var dis = ImRaii.Disabled(isDisabled);
|
||||
poseActionDraw.Invoke();
|
||||
}
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
UiSharedService.AttachToolTip(sb.ToString());
|
||||
}
|
||||
}
|
||||
using (ImRaii.Group())
|
||||
{
|
||||
using var dis = ImRaii.Disabled(isDisabled);
|
||||
poseActionDraw.Invoke();
|
||||
}
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
UiSharedService.AttachToolTip(sb.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void SetWindowSizeConstraints(bool? inGposeTab = null)
|
||||
{
|
||||
SizeConstraints = new()
|
||||
{
|
||||
MinimumSize = new((inGposeTab ?? false) ? 400 : 1000, 500),
|
||||
MaximumSize = new((inGposeTab ?? false) ? 400 : 1000, 2000)
|
||||
};
|
||||
}
|
||||
private void SetWindowSizeConstraints(bool? inGposeTab = null)
|
||||
{
|
||||
SizeConstraints = new()
|
||||
{
|
||||
MinimumSize = new((inGposeTab ?? false) ? 400 : 1000, 500),
|
||||
MaximumSize = new((inGposeTab ?? false) ? 400 : 1000, 2000)
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateFilteredFavorites()
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
if (_charaDataManager.DownloadMetaInfoTask != null)
|
||||
{
|
||||
await _charaDataManager.DownloadMetaInfoTask.ConfigureAwait(false);
|
||||
}
|
||||
Dictionary<string, (CharaDataFavorite, CharaDataMetaInfoExtendedDto?, bool)> newFiltered = [];
|
||||
foreach (var favorite in _configService.Current.FavoriteCodes)
|
||||
{
|
||||
var uid = favorite.Key.Split(":")[0];
|
||||
var note = _serverConfigurationManager.GetNoteForUid(uid) ?? string.Empty;
|
||||
bool hasMetaInfo = _charaDataManager.TryGetMetaInfo(favorite.Key, out var metaInfo);
|
||||
bool addFavorite =
|
||||
(string.IsNullOrEmpty(_filterCodeNote)
|
||||
|| (note.Contains(_filterCodeNote, StringComparison.OrdinalIgnoreCase)
|
||||
|| uid.Contains(_filterCodeNote, StringComparison.OrdinalIgnoreCase)))
|
||||
&& (string.IsNullOrEmpty(_filterDescription)
|
||||
|| (favorite.Value.CustomDescription.Contains(_filterDescription, StringComparison.OrdinalIgnoreCase)
|
||||
|| (metaInfo != null && metaInfo!.Description.Contains(_filterDescription, StringComparison.OrdinalIgnoreCase))))
|
||||
&& (!_filterPoseOnly
|
||||
|| (metaInfo != null && metaInfo!.HasPoses))
|
||||
&& (!_filterWorldOnly
|
||||
|| (metaInfo != null && metaInfo!.HasWorldData));
|
||||
if (addFavorite)
|
||||
{
|
||||
newFiltered[favorite.Key] = (favorite.Value, metaInfo, hasMetaInfo);
|
||||
}
|
||||
}
|
||||
private void UpdateFilteredFavorites()
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
if (_charaDataManager.DownloadMetaInfoTask != null)
|
||||
{
|
||||
await _charaDataManager.DownloadMetaInfoTask.ConfigureAwait(false);
|
||||
}
|
||||
Dictionary<string, (CharaDataFavorite, CharaDataMetaInfoExtendedDto?, bool)> newFiltered = [];
|
||||
foreach (var favorite in _configService.Current.FavoriteCodes)
|
||||
{
|
||||
var uid = favorite.Key.Split(":")[0];
|
||||
var note = _serverConfigurationManager.GetNoteForUid(uid) ?? string.Empty;
|
||||
bool hasMetaInfo = _charaDataManager.TryGetMetaInfo(favorite.Key, out var metaInfo);
|
||||
bool addFavorite =
|
||||
(string.IsNullOrEmpty(_filterCodeNote)
|
||||
|| (note.Contains(_filterCodeNote, StringComparison.OrdinalIgnoreCase)
|
||||
|| uid.Contains(_filterCodeNote, StringComparison.OrdinalIgnoreCase)))
|
||||
&& (string.IsNullOrEmpty(_filterDescription)
|
||||
|| (favorite.Value.CustomDescription.Contains(_filterDescription, StringComparison.OrdinalIgnoreCase)
|
||||
|| (metaInfo != null && metaInfo!.Description.Contains(_filterDescription, StringComparison.OrdinalIgnoreCase))))
|
||||
&& (!_filterPoseOnly
|
||||
|| (metaInfo != null && metaInfo!.HasPoses))
|
||||
&& (!_filterWorldOnly
|
||||
|| (metaInfo != null && metaInfo!.HasWorldData));
|
||||
if (addFavorite)
|
||||
{
|
||||
newFiltered[favorite.Key] = (favorite.Value, metaInfo, hasMetaInfo);
|
||||
}
|
||||
}
|
||||
|
||||
_filteredFavorites = newFiltered;
|
||||
});
|
||||
}
|
||||
_filteredFavorites = newFiltered;
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateFilteredItems()
|
||||
{
|
||||
if (_charaDataManager.GetSharedWithYouTask == null)
|
||||
{
|
||||
_filteredDict = _charaDataManager.SharedWithYouData
|
||||
.SelectMany(k => k.Value)
|
||||
.Where(k =>
|
||||
(!_sharedWithYouDownloadableFilter || k.CanBeDownloaded)
|
||||
&& (string.IsNullOrEmpty(_sharedWithYouDescriptionFilter) || k.Description.Contains(_sharedWithYouDescriptionFilter, StringComparison.OrdinalIgnoreCase)))
|
||||
.GroupBy(k => k.Uploader)
|
||||
.ToDictionary(k =>
|
||||
{
|
||||
var note = _serverConfigurationManager.GetNoteForUid(k.Key.UID);
|
||||
if (note == null) return k.Key.AliasOrUID;
|
||||
return $"{note} ({k.Key.AliasOrUID})";
|
||||
}, k => k.ToList(), StringComparer.OrdinalIgnoreCase)
|
||||
.Where(k => (string.IsNullOrEmpty(_sharedWithYouOwnerFilter) || k.Key.Contains(_sharedWithYouOwnerFilter, StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToDictionary();
|
||||
}
|
||||
}
|
||||
private void UpdateFilteredItems()
|
||||
{
|
||||
if (_charaDataManager.GetSharedWithYouTask == null)
|
||||
{
|
||||
_filteredDict = _charaDataManager.SharedWithYouData
|
||||
.SelectMany(k => k.Value)
|
||||
.Where(k =>
|
||||
(!_sharedWithYouDownloadableFilter || k.CanBeDownloaded)
|
||||
&& (string.IsNullOrEmpty(_sharedWithYouDescriptionFilter) || k.Description.Contains(_sharedWithYouDescriptionFilter, StringComparison.OrdinalIgnoreCase)))
|
||||
.GroupBy(k => k.Uploader)
|
||||
.ToDictionary(k =>
|
||||
{
|
||||
var note = _serverConfigurationManager.GetNoteForUid(k.Key.UID);
|
||||
if (note == null) return k.Key.AliasOrUID;
|
||||
return $"{note} ({k.Key.AliasOrUID})";
|
||||
}, k => k.ToList(), StringComparer.OrdinalIgnoreCase)
|
||||
.Where(k => (string.IsNullOrEmpty(_sharedWithYouOwnerFilter) || k.Key.Contains(_sharedWithYouOwnerFilter, StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToDictionary();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ internal sealed partial class CharaDataHubUi
|
||||
if (!_uiSharedService.IsInGpose)
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
UiSharedService.DrawGroupedCenteredColorText("Assigning users to characters is only available in GPose.", ImGuiColors.DalamudYellow, 300);
|
||||
UiSharedService.DrawGroupedCenteredColorText("Assigning users to characters is only available in GPose.", UIColors.Get("LightlessYellow"), 300);
|
||||
}
|
||||
UiSharedService.DistanceSeparator();
|
||||
ImGui.TextUnformatted("Users In Lobby");
|
||||
@@ -104,7 +104,7 @@ internal sealed partial class CharaDataHubUi
|
||||
|
||||
if (!_charaDataGposeTogetherManager.UsersInLobby.Any() && !string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId))
|
||||
{
|
||||
UiSharedService.DrawGroupedCenteredColorText("No other users in current GPose lobby", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.DrawGroupedCenteredColorText("No other users in current GPose lobby", UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.Services.CharaData.Models;
|
||||
using System.Numerics;
|
||||
@@ -23,7 +23,7 @@ internal sealed partial class CharaDataHubUi
|
||||
if (dataDto == null)
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
UiSharedService.DrawGroupedCenteredColorText("Select an entry above to edit its data.", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.DrawGroupedCenteredColorText("Select an entry above to edit its data.", UIColors.Get("LightlessYellow"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ internal sealed partial class CharaDataHubUi
|
||||
|
||||
if (updateDto == null)
|
||||
{
|
||||
UiSharedService.DrawGroupedCenteredColorText("Something went awfully wrong and there's no update DTO. Try updating Character Data via the button above.", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.DrawGroupedCenteredColorText("Something went awfully wrong and there's no update DTO. Try updating Character Data via the button above.", UIColors.Get("LightlessYellow"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ internal sealed partial class CharaDataHubUi
|
||||
}
|
||||
if (_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Updating data on server, please wait.", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped("Updating data on server, please wait.", UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ internal sealed partial class CharaDataHubUi
|
||||
{
|
||||
if (_charaDataManager.UploadProgress != null)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(_charaDataManager.UploadProgress.Value ?? string.Empty, ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped(_charaDataManager.UploadProgress.Value ?? string.Empty, UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
if ((!_charaDataManager.UploadTask?.IsCompleted ?? false) && _uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Cancel Upload"))
|
||||
{
|
||||
@@ -112,7 +112,7 @@ internal sealed partial class CharaDataHubUi
|
||||
UiSharedService.DrawGrouped(() =>
|
||||
{
|
||||
ImGui.AlignTextToFramePadding();
|
||||
UiSharedService.ColorTextWrapped($"You have {otherUpdates} other entries with unsaved changes.", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped($"You have {otherUpdates} other entries with unsaved changes.", UIColors.Get("LightlessYellow"));
|
||||
ImGui.SameLine();
|
||||
using (ImRaii.Disabled(_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted))
|
||||
{
|
||||
@@ -259,7 +259,7 @@ internal sealed partial class CharaDataHubUi
|
||||
ImGui.SameLine();
|
||||
ImGuiHelpers.ScaledDummy(20, 1);
|
||||
ImGui.SameLine();
|
||||
UiSharedService.ColorTextWrapped("New data was set. It may contain files that require to be uploaded (will happen on Saving to server)", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped("New data was set. It may contain files that require to be uploaded (will happen on Saving to server)", UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted("Contains Manipulation Data");
|
||||
@@ -414,7 +414,7 @@ internal sealed partial class CharaDataHubUi
|
||||
}
|
||||
}
|
||||
ImGui.SameLine();
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, poseCount == maxPoses))
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"), poseCount == maxPoses))
|
||||
ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached");
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
|
||||
@@ -424,7 +424,7 @@ internal sealed partial class CharaDataHubUi
|
||||
if (!_uiSharedService.IsInGpose && _charaDataManager.BrioAvailable)
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data you need to be in GPose.", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data you need to be in GPose.", UIColors.Get("LightlessYellow"));
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
}
|
||||
else if (!_charaDataManager.BrioAvailable)
|
||||
@@ -443,7 +443,7 @@ internal sealed partial class CharaDataHubUi
|
||||
if (pose.Id == null)
|
||||
{
|
||||
UiSharedService.ScaledSameLine(50);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Plus, ImGuiColors.DalamudYellow);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Plus, UIColors.Get("LightlessYellow"));
|
||||
UiSharedService.AttachToolTip("This pose has not been added to the server yet. Save changes to upload this Pose data.");
|
||||
}
|
||||
|
||||
@@ -451,14 +451,14 @@ internal sealed partial class CharaDataHubUi
|
||||
if (poseHasChanges)
|
||||
{
|
||||
UiSharedService.ScaledSameLine(50);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudYellow);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
|
||||
UiSharedService.AttachToolTip("This pose has changes that have not been saved to the server yet.");
|
||||
}
|
||||
|
||||
UiSharedService.ScaledSameLine(75);
|
||||
if (pose.Description == null && pose.WorldData == null && pose.PoseData == null)
|
||||
{
|
||||
UiSharedService.ColorText("Pose scheduled for deletion", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorText("Pose scheduled for deletion", UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -669,7 +669,7 @@ internal sealed partial class CharaDataHubUi
|
||||
var idText = entry.FullId;
|
||||
if (uDto?.HasChanges ?? false)
|
||||
{
|
||||
UiSharedService.ColorText(idText, ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorText(idText, UIColors.Get("LightlessYellow"));
|
||||
UiSharedService.AttachToolTip("This entry has unsaved changes");
|
||||
}
|
||||
else
|
||||
@@ -724,7 +724,7 @@ internal sealed partial class CharaDataHubUi
|
||||
FontAwesomeIcon eIcon = FontAwesomeIcon.None;
|
||||
if (!Equals(DateTime.MaxValue, entry.ExpiryDate))
|
||||
eIcon = FontAwesomeIcon.Clock;
|
||||
_uiSharedService.IconText(eIcon, ImGuiColors.DalamudYellow);
|
||||
_uiSharedService.IconText(eIcon, UIColors.Get("LightlessYellow"));
|
||||
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
|
||||
if (eIcon != FontAwesomeIcon.None)
|
||||
{
|
||||
@@ -759,13 +759,13 @@ internal sealed partial class CharaDataHubUi
|
||||
if (_charaDataManager.OwnCharaData.Count == _charaDataManager.MaxCreatableCharaData)
|
||||
{
|
||||
ImGui.AlignTextToFramePadding();
|
||||
UiSharedService.ColorTextWrapped("You have reached the maximum Character Data entries and cannot create more.", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped("You have reached the maximum Character Data entries and cannot create more.", UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
}
|
||||
|
||||
if (_charaDataManager.DataCreationTask != null && !_charaDataManager.DataCreationTask.IsCompleted)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Creating new character data entry on server...", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped("Creating new character data entry on server...", UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
else if (_charaDataManager.DataCreationTask != null && _charaDataManager.DataCreationTask.IsCompleted)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using System.Numerics;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
@@ -78,7 +78,7 @@ internal partial class CharaDataHubUi
|
||||
if (!_uiSharedService.IsInGpose)
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
UiSharedService.DrawGroupedCenteredColorText("Spawning and applying pose data is only available in GPose.", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.DrawGroupedCenteredColorText("Spawning and applying pose data is only available in GPose.", UIColors.Get("LightlessYellow"));
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ internal partial class CharaDataHubUi
|
||||
using var indent = ImRaii.PushIndent(5f);
|
||||
if (_charaDataNearbyManager.NearbyData.Count == 0)
|
||||
{
|
||||
UiSharedService.DrawGroupedCenteredColorText("No Shared World Poses found nearby.", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.DrawGroupedCenteredColorText("No Shared World Poses found nearby.", UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
|
||||
bool wasAnythingHovered = false;
|
||||
|
||||
@@ -190,7 +190,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
if (!string.IsNullOrEmpty(_charaDataManager.DataApplicationProgress))
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(_charaDataManager.DataApplicationProgress, ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped(_charaDataManager.DataApplicationProgress, UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
if (_charaDataManager.DataApplicationTask != null)
|
||||
{
|
||||
@@ -436,7 +436,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
if (!_hasValidGposeTarget)
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(3);
|
||||
UiSharedService.DrawGroupedCenteredColorText("Applying data is only available in GPose with a valid selected GPose target.", ImGuiColors.DalamudYellow, 350);
|
||||
UiSharedService.DrawGroupedCenteredColorText("Applying data is only available in GPose with a valid selected GPose target.", UIColors.Get("LightlessYellow"), 350);
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10);
|
||||
@@ -595,7 +595,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
|
||||
if (_configService.Current.FavoriteCodes.Count == 0)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("You have no favorites added. Add Favorites through the other tabs before you can use this tab.", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped("You have no favorites added. Add Favorites through the other tabs before you can use this tab.", UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -644,7 +644,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
ImGui.NewLine();
|
||||
if (!_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Downloading meta info. Please wait.", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped("Downloading meta info. Please wait.", UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
if ((_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false) && !_charaDataManager.DownloadMetaInfoTask.Result.Success)
|
||||
{
|
||||
@@ -850,12 +850,12 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
UiSharedService.ColorTextWrapped("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.",
|
||||
ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorTextWrapped("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. " +
|
||||
"If you received it from someone else have them do the same.", ImGuiColors.DalamudYellow);
|
||||
"If you received it from someone else have them do the same.", UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Loading Character...", ImGuiColors.DalamudYellow);
|
||||
UiSharedService.ColorTextWrapped("Loading Character...", UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -896,7 +896,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
}, Directory.Exists(_configService.Current.LastSavedCharaDataLocation) ? _configService.Current.LastSavedCharaDataLocation : null);
|
||||
}
|
||||
UiSharedService.ColorTextWrapped("Note: For best results make sure you have everything you want to be shared as well as the correct character appearance" +
|
||||
" equipped and redraw your character before exporting.", ImGuiColors.DalamudYellow);
|
||||
" equipped and redraw your character before exporting.", UIColors.Get("LightlessYellow"));
|
||||
|
||||
ImGui.Unindent();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using System;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Utility;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
@@ -15,6 +16,7 @@ using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI.Components;
|
||||
using LightlessSync.UI.Handlers;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using LightlessSync.WebAPI.Files;
|
||||
using LightlessSync.WebAPI.Files.Models;
|
||||
@@ -23,6 +25,7 @@ using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
|
||||
@@ -30,35 +33,61 @@ namespace LightlessSync.UI;
|
||||
|
||||
public class CompactUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
private readonly CharacterAnalyzer _characterAnalyzer;
|
||||
private readonly ApiController _apiController;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
||||
private readonly DrawEntityFactory _drawEntityFactory;
|
||||
private readonly FileUploadManager _fileTransferManager;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly SelectTagForPairUi _selectGroupForPairUi;
|
||||
private readonly SelectTagForPairUi _selectTagForPairUi;
|
||||
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
|
||||
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
|
||||
private readonly RenameSyncshellTagUi _renameSyncshellTagUi;
|
||||
private readonly SelectPairForTagUi _selectPairsForGroupUi;
|
||||
private readonly RenamePairTagUi _renamePairTagUi;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly ServerConfigurationManager _serverManager;
|
||||
private readonly TopTabMenu _tabMenu;
|
||||
private readonly TagHandler _tagHandler;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly BroadcastService _broadcastService;
|
||||
|
||||
private List<IDrawFolder> _drawFolders;
|
||||
private Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>? _cachedAnalysis;
|
||||
private Pair? _lastAddedUser;
|
||||
private string _lastAddedUserComment = string.Empty;
|
||||
private Vector2 _lastPosition = Vector2.One;
|
||||
private Vector2 _lastSize = Vector2.One;
|
||||
private int _secretKeyIdx = -1;
|
||||
private bool _showModalForUserAddition;
|
||||
private float _transferPartHeight;
|
||||
private bool _wasOpen;
|
||||
private float _windowContentWidth;
|
||||
|
||||
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, LightlessConfigService configService, ApiController apiController, PairManager pairManager,
|
||||
ServerConfigurationManager serverManager, LightlessMediator mediator, FileUploadManager fileTransferManager,
|
||||
TagHandler tagHandler, DrawEntityFactory drawEntityFactory, SelectTagForPairUi selectTagForPairUi, SelectPairForTagUi selectPairForTagUi,
|
||||
PerformanceCollectorService performanceCollectorService, IpcManager ipcManager)
|
||||
: base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
|
||||
public CompactUi(
|
||||
ILogger<CompactUi> logger,
|
||||
UiSharedService uiShared,
|
||||
LightlessConfigService configService,
|
||||
ApiController apiController,
|
||||
PairManager pairManager,
|
||||
ServerConfigurationManager serverManager,
|
||||
LightlessMediator mediator,
|
||||
FileUploadManager fileTransferManager,
|
||||
TagHandler tagHandler,
|
||||
DrawEntityFactory drawEntityFactory,
|
||||
SelectTagForPairUi selectTagForPairUi,
|
||||
SelectPairForTagUi selectPairForTagUi,
|
||||
RenamePairTagUi renameTagUi,
|
||||
SelectTagForSyncshellUi selectTagForSyncshellUi,
|
||||
SelectSyncshellForTagUi selectSyncshellForTagUi,
|
||||
RenameSyncshellTagUi renameSyncshellTagUi,
|
||||
PerformanceCollectorService performanceCollectorService,
|
||||
IpcManager ipcManager,
|
||||
BroadcastService broadcastService,
|
||||
CharacterAnalyzer characterAnalyzer,
|
||||
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
|
||||
{
|
||||
_uiSharedService = uiShared;
|
||||
_configService = configService;
|
||||
@@ -68,12 +97,17 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
_fileTransferManager = fileTransferManager;
|
||||
_tagHandler = tagHandler;
|
||||
_drawEntityFactory = drawEntityFactory;
|
||||
_selectGroupForPairUi = selectTagForPairUi;
|
||||
_selectTagForPairUi = selectTagForPairUi;
|
||||
_selectTagForSyncshellUi = selectTagForSyncshellUi;
|
||||
_selectSyncshellForTagUi = selectSyncshellForTagUi;
|
||||
_renameSyncshellTagUi = renameSyncshellTagUi;
|
||||
_selectPairsForGroupUi = selectPairForTagUi;
|
||||
_renamePairTagUi = renameTagUi;
|
||||
_ipcManager = ipcManager;
|
||||
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService);
|
||||
_broadcastService = broadcastService;
|
||||
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService);
|
||||
|
||||
AllowPinning = false;
|
||||
AllowPinning = true;
|
||||
AllowClickthrough = false;
|
||||
TitleBarButtons = new()
|
||||
{
|
||||
@@ -106,10 +140,10 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
ImGui.Text("Open Lightless Event Viewer");
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_drawFolders = GetDrawFolders().ToList();
|
||||
_drawFolders = [.. DrawFolders];
|
||||
|
||||
#if DEBUG
|
||||
string dev = "Dev Build";
|
||||
@@ -126,7 +160,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
|
||||
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
|
||||
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
|
||||
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = GetDrawFolders().ToList());
|
||||
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = DrawFolders.ToList());
|
||||
|
||||
Flags |= ImGuiWindowFlags.NoDocking;
|
||||
|
||||
@@ -135,6 +169,9 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
MinimumSize = new Vector2(375, 400),
|
||||
MaximumSize = new Vector2(375, 2000),
|
||||
};
|
||||
_characterAnalyzer = characterAnalyzer;
|
||||
_playerPerformanceConfig = playerPerformanceConfig;
|
||||
_lightlessMediator = mediator;
|
||||
}
|
||||
|
||||
protected override void DrawInternal()
|
||||
@@ -149,10 +186,10 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
var uidTextSize = ImGui.CalcTextSize(unsupported);
|
||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 - uidTextSize.X / 2);
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextColored(ImGuiColors.DalamudRed, unsupported);
|
||||
ImGui.TextColored(UIColors.Get("DimRed"), unsupported);
|
||||
}
|
||||
UiSharedService.ColorTextWrapped($"Your Lightless Sync installation is out of date, the current version is {ver.Major}.{ver.Minor}.{ver.Build}. " +
|
||||
$"It is highly recommended to keep Lightless Sync up to date. Open /xlplugins and update the plugin.", ImGuiColors.DalamudRed);
|
||||
$"It is highly recommended to keep Lightless Sync up to date. Open /xlplugins and update the plugin.", UIColors.Get("DimRed"));
|
||||
}
|
||||
|
||||
if (!_ipcManager.Initialized)
|
||||
@@ -164,12 +201,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
var uidTextSize = ImGui.CalcTextSize(unsupported);
|
||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 - uidTextSize.X / 2);
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextColored(ImGuiColors.DalamudRed, unsupported);
|
||||
ImGui.TextColored(UIColors.Get("DimRed"), unsupported);
|
||||
}
|
||||
var penumAvailable = _ipcManager.Penumbra.APIAvailable;
|
||||
var glamAvailable = _ipcManager.Glamourer.APIAvailable;
|
||||
|
||||
UiSharedService.ColorTextWrapped($"One or more Plugins essential for Lightless operation are unavailable. Enable or update following plugins:", ImGuiColors.DalamudRed);
|
||||
UiSharedService.ColorTextWrapped($"One or more Plugins essential for Lightless operation are unavailable. Enable or update following plugins:", UIColors.Get("DimRed"));
|
||||
using var indent = ImRaii.PushIndent(10f);
|
||||
if (!penumAvailable)
|
||||
{
|
||||
@@ -185,7 +222,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
|
||||
using (ImRaii.PushId("header")) DrawUIDHeader();
|
||||
ImGui.Separator();
|
||||
_uiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f);
|
||||
using (ImRaii.PushId("serverstatus")) DrawServerStatus();
|
||||
ImGui.Separator();
|
||||
|
||||
@@ -197,8 +234,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
float pairlistEnd = ImGui.GetCursorPosY();
|
||||
using (ImRaii.PushId("transfers")) DrawTransfers();
|
||||
_transferPartHeight = ImGui.GetCursorPosY() - pairlistEnd - ImGui.GetTextLineHeight();
|
||||
using (ImRaii.PushId("group-user-popup")) _selectPairsForGroupUi.Draw(_pairManager.DirectPairs);
|
||||
using (ImRaii.PushId("grouping-popup")) _selectGroupForPairUi.Draw();
|
||||
using (ImRaii.PushId("group-pair-popup")) _selectPairsForGroupUi.Draw(_pairManager.DirectPairs);
|
||||
using (ImRaii.PushId("group-syncshell-popup")) _selectSyncshellForTagUi.Draw([.. _pairManager.Groups.Values]);
|
||||
using (ImRaii.PushId("group-pair-edit")) _renamePairTagUi.Draw();
|
||||
using (ImRaii.PushId("group-syncshell-edit")) _renameSyncshellTagUi.Draw();
|
||||
using (ImRaii.PushId("grouping-pair-popup")) _selectTagForPairUi.Draw();
|
||||
using (ImRaii.PushId("grouping-syncshell-popup")) _selectTagForSyncshellUi.Draw();
|
||||
}
|
||||
|
||||
if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null)
|
||||
@@ -284,7 +325,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
else
|
||||
{
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextColored(ImGuiColors.DalamudRed, "Not connected to any server");
|
||||
ImGui.TextColored(UIColors.Get("DimRed"), "Not connected to any server");
|
||||
}
|
||||
|
||||
if (printShard)
|
||||
@@ -336,7 +377,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
|
||||
private void DrawTransfers()
|
||||
{
|
||||
var currentUploads = _fileTransferManager.CurrentUploads.ToList();
|
||||
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Upload);
|
||||
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
||||
@@ -346,10 +387,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
var totalUploads = currentUploads.Count;
|
||||
|
||||
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 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 textSize = ImGui.CalcTextSize(uploadText);
|
||||
ImGui.SameLine(_windowContentWidth - textSize.X);
|
||||
@@ -362,7 +405,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
ImGui.TextUnformatted("No uploads in progress");
|
||||
}
|
||||
|
||||
var currentDownloads = _currentDownloads.SelectMany(d => d.Value.Values).ToList();
|
||||
var currentDownloads = BuildCurrentDownloadSnapshot();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Download);
|
||||
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
||||
@@ -389,35 +432,250 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private List<FileDownloadStatus> BuildCurrentDownloadSnapshot()
|
||||
{
|
||||
List<FileDownloadStatus> snapshot = new();
|
||||
|
||||
foreach (var kvp in _currentDownloads.ToArray())
|
||||
{
|
||||
var value = kvp.Value;
|
||||
if (value == null || value.Count == 0)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
snapshot.AddRange(value.Values.ToArray());
|
||||
}
|
||||
catch (System.ArgumentException)
|
||||
{
|
||||
// skibidi
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private void DrawUIDHeader()
|
||||
{
|
||||
var uidText = GetUidText();
|
||||
|
||||
Vector4? vanityTextColor = null;
|
||||
Vector4? vanityGlowColor = null;
|
||||
bool useVanityColors = false;
|
||||
|
||||
if (_configService.Current.useColoredUIDs && _apiController.HasVanity)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_apiController.TextColorHex))
|
||||
{
|
||||
vanityTextColor = UIColors.HexToRgba(_apiController.TextColorHex);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_apiController.TextGlowColorHex))
|
||||
{
|
||||
vanityGlowColor = UIColors.HexToRgba(_apiController.TextGlowColorHex);
|
||||
}
|
||||
|
||||
useVanityColors = vanityTextColor is not null || vanityGlowColor is not null;
|
||||
}
|
||||
|
||||
//Getting information of character and triangles threshold to show overlimit status in UID bar.
|
||||
_cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone();
|
||||
|
||||
Vector2 uidTextSize, iconSize;
|
||||
using (_uiSharedService.UidFont.Push())
|
||||
uidTextSize = ImGui.CalcTextSize(uidText);
|
||||
|
||||
using (_uiSharedService.IconFont.Push())
|
||||
iconSize = ImGui.CalcTextSize(FontAwesomeIcon.PersonCirclePlus.ToIconString());
|
||||
|
||||
float contentWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
||||
float uidStartX = (contentWidth - uidTextSize.X) / 2f;
|
||||
float cursorY = ImGui.GetCursorPosY();
|
||||
|
||||
if (_configService.Current.BroadcastEnabled && _apiController.IsConnected)
|
||||
{
|
||||
float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f;
|
||||
var buttonSize = new Vector2(iconSize.X, uidTextSize.Y);
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(ImGui.GetStyle().ItemSpacing.X + 5f, cursorY));
|
||||
ImGui.InvisibleButton("BroadcastIcon", buttonSize);
|
||||
|
||||
var iconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset);
|
||||
using (_uiSharedService.IconFont.Push())
|
||||
ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.PersonCirclePlus.ToIconString());
|
||||
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("PairBlue"));
|
||||
ImGui.Text("Lightfinder");
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
ImGui.TextWrapped("Use Lightfinder when you're okay with being visible to other users and understand that you are responsible for your own experience.");
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
ImGuiHelpers.ScaledDummy(0.2f);
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
||||
|
||||
if (_configService.Current.BroadcastEnabled)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGreen"));
|
||||
ImGui.Text("The Lightfinder calls, and somewhere, a soul may answer."); // cringe..
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
var ttl = _broadcastService.RemainingTtl;
|
||||
if (ttl is { } remaining && remaining > TimeSpan.Zero)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
|
||||
ImGui.Text($"Still shining, for {remaining:hh\\:mm\\:ss}");
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
ImGui.Text("The Lightfinder's light wanes, but not in vain."); // cringe..
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
ImGui.Text("The Lightfinder rests, waiting to shine again."); // cringe..
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
||||
var cooldown = _broadcastService.RemainingCooldown;
|
||||
if (cooldown is { } cd)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
ImGui.Text($"The Lightfinder gathers its strength... ({Math.Ceiling(cd.TotalSeconds)}s)");
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
if (ImGui.IsItemClicked())
|
||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
|
||||
}
|
||||
|
||||
ImGui.SetCursorPosY(cursorY);
|
||||
ImGui.SetCursorPosX(uidStartX);
|
||||
|
||||
bool headerItemClicked;
|
||||
using (_uiSharedService.UidFont.Push())
|
||||
{
|
||||
var uidTextSize = ImGui.CalcTextSize(uidText);
|
||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (uidTextSize.X / 2));
|
||||
ImGui.TextColored(GetUidColor(), uidText);
|
||||
if (useVanityColors)
|
||||
{
|
||||
var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor);
|
||||
var cursorPos = ImGui.GetCursorScreenPos();
|
||||
var fontPtr = ImGui.GetFont();
|
||||
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextColored(GetUidColor(), uidText);
|
||||
}
|
||||
}
|
||||
|
||||
headerItemClicked = ImGui.IsItemClicked();
|
||||
|
||||
if (headerItemClicked)
|
||||
{
|
||||
ImGui.SetClipboardText(uidText);
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip("Click to copy");
|
||||
|
||||
if (_cachedAnalysis != null && _apiController.ServerState is ServerState.Connected)
|
||||
{
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_apiController.ServerState is ServerState.Connected)
|
||||
{
|
||||
if (ImGui.IsItemClicked())
|
||||
if (headerItemClicked)
|
||||
{
|
||||
ImGui.SetClipboardText(_apiController.DisplayName);
|
||||
}
|
||||
UiSharedService.AttachToolTip("Click to copy");
|
||||
|
||||
if (!string.Equals(_apiController.DisplayName, _apiController.UID, StringComparison.Ordinal))
|
||||
{
|
||||
var origTextSize = ImGui.CalcTextSize(_apiController.UID);
|
||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (origTextSize.X / 2));
|
||||
ImGui.TextColored(GetUidColor(), _apiController.UID);
|
||||
if (ImGui.IsItemClicked())
|
||||
|
||||
if (useVanityColors)
|
||||
{
|
||||
var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.UID, vanityTextColor, vanityGlowColor);
|
||||
var cursorPos = ImGui.GetCursorScreenPos();
|
||||
var fontPtr = ImGui.GetFont();
|
||||
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextColored(GetUidColor(), _apiController.UID);
|
||||
}
|
||||
|
||||
bool uidFooterClicked = ImGui.IsItemClicked();
|
||||
UiSharedService.AttachToolTip("Click to copy");
|
||||
if (uidFooterClicked)
|
||||
{
|
||||
ImGui.SetClipboardText(_apiController.UID);
|
||||
}
|
||||
UiSharedService.AttachToolTip("Click to copy");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -426,138 +684,164 @@ 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
|
||||
.ToDictionary(k => k.Key, k => k.Value);
|
||||
var filteredPairs = allPairs
|
||||
.Where(p =>
|
||||
var allPairs = _pairManager.PairsWithGroups.ToDictionary(k => k.Key, k => k.Value);
|
||||
var filteredPairs = allPairs.Where(p => PassesFilter(p.Key, filter)).ToDictionary(k => k.Key, k => k.Value);
|
||||
|
||||
//Filter of online/visible pairs
|
||||
if (_configService.Current.ShowVisibleUsersSeparately)
|
||||
{
|
||||
if (_tabMenu.Filter.IsNullOrEmpty()) return true;
|
||||
return p.Key.UserData.AliasOrUID.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ||
|
||||
(p.Key.GetNote()?.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ?? false) ||
|
||||
(p.Key.PlayerName?.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
})
|
||||
.ToDictionary(k => k.Key, k => k.Value);
|
||||
var allVisiblePairs = ImmutablePairList(allPairs.Where(p => FilterVisibleUsers(p.Key)));
|
||||
var filteredVisiblePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterVisibleUsers(p.Key)));
|
||||
|
||||
string? AlphabeticalSort(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
|
||||
=> (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(u.Key.PlayerName)
|
||||
? (_configService.Current.PreferNotesOverNamesForVisible ? u.Key.GetNote() : u.Key.PlayerName)
|
||||
: (u.Key.GetNote() ?? u.Key.UserData.AliasOrUID));
|
||||
bool FilterOnlineOrPausedSelf(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
|
||||
=> (u.Key.IsOnline || (!u.Key.IsOnline && !_configService.Current.ShowOfflineUsersSeparately)
|
||||
|| u.Key.UserPair.OwnPermissions.IsPaused());
|
||||
Dictionary<Pair, List<GroupFullInfoDto>> BasicSortedDictionary(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> u)
|
||||
=> u.OrderByDescending(u => u.Key.IsVisible)
|
||||
.ThenByDescending(u => u.Key.IsOnline)
|
||||
.ThenBy(AlphabeticalSort, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(u => u.Key, u => u.Value);
|
||||
ImmutableList<Pair> ImmutablePairList(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> u)
|
||||
=> u.Select(k => k.Key).ToImmutableList();
|
||||
bool FilterVisibleUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
|
||||
=> u.Key.IsVisible
|
||||
&& (_configService.Current.ShowSyncshellUsersInVisible || !(!_configService.Current.ShowSyncshellUsersInVisible && !u.Key.IsDirectlyPaired));
|
||||
bool FilterTagusers(KeyValuePair<Pair, List<GroupFullInfoDto>> u, string tag)
|
||||
=> u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && _tagHandler.HasTag(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.HasAnyTag(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());
|
||||
|
||||
|
||||
if (_configService.Current.ShowVisibleUsersSeparately)
|
||||
{
|
||||
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))
|
||||
{
|
||||
var allGroupPairs = ImmutablePairList(allPairs
|
||||
.Where(u => FilterGroupUsers(u, group)));
|
||||
|
||||
var 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 (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(AlphabeticalSort, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(k => k.Key, k => k.Value);
|
||||
|
||||
groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs));
|
||||
}
|
||||
|
||||
if (_configService.Current.GroupUpSyncshells)
|
||||
drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler, _uiSharedService));
|
||||
else
|
||||
drawFolders.AddRange(groupFolders);
|
||||
|
||||
var tags = _tagHandler.GetAllTagsSorted();
|
||||
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 allOnlineNotTaggedPairs = ImmutablePairList(allPairs
|
||||
.Where(FilterNotTaggedUsers));
|
||||
var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs
|
||||
.Where(u => FilterNotTaggedUsers(u) && FilterOnlineOrPausedSelf(u)));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag),
|
||||
onlineNotTaggedPairs, allOnlineNotTaggedPairs));
|
||||
|
||||
if (_configService.Current.ShowOfflineUsersSeparately)
|
||||
{
|
||||
var allOfflinePairs = ImmutablePairList(allPairs
|
||||
.Where(FilterOfflineUsers));
|
||||
var filteredOfflinePairs = BasicSortedDictionary(filteredPairs
|
||||
.Where(FilterOfflineUsers));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs));
|
||||
if (_configService.Current.ShowSyncshellOfflineUsersSeparately)
|
||||
{
|
||||
var allOfflineSyncshellUsers = ImmutablePairList(allPairs
|
||||
.Where(FilterOfflineSyncshellUsers));
|
||||
var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs
|
||||
.Where(FilterOfflineSyncshellUsers));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag,
|
||||
filteredOfflineSyncshellUsers,
|
||||
allOfflineSyncshellUsers));
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs));
|
||||
}
|
||||
|
||||
//Filter of not foldered syncshells
|
||||
var groupFolders = new List<IDrawFolder>();
|
||||
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
GetGroups(allPairs, filteredPairs, group, out ImmutableList<Pair> allGroupPairs, out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
|
||||
|
||||
if (FilterNotTaggedSyncshells(group))
|
||||
{
|
||||
groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs));
|
||||
}
|
||||
}
|
||||
|
||||
//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())
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
//Filter of grouped/foldered syncshells
|
||||
foreach (var syncshellTag in _tagHandler.GetAllSyncshellTagsSorted())
|
||||
{
|
||||
var syncshellFolderTags = new List<IDrawFolder>();
|
||||
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (_tagHandler.HasSyncshellTag(group.GID, syncshellTag))
|
||||
{
|
||||
GetGroups(allPairs, filteredPairs, group,
|
||||
out ImmutableList<Pair> allGroupPairs,
|
||||
out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
|
||||
|
||||
syncshellFolderTags.Add(_drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs));
|
||||
}
|
||||
}
|
||||
|
||||
drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshellTag));
|
||||
}
|
||||
|
||||
//Filter of not grouped/foldered and offline pairs
|
||||
var allOnlineNotTaggedPairs = ImmutablePairList(allPairs.Where(p => FilterNotTaggedUsers(p.Key)));
|
||||
var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterNotTaggedUsers(p.Key) && FilterOnlineOrPausedSelf(p.Key)));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag), onlineNotTaggedPairs, allOnlineNotTaggedPairs));
|
||||
|
||||
if (_configService.Current.ShowOfflineUsersSeparately)
|
||||
{
|
||||
var allOfflinePairs = ImmutablePairList(allPairs.Where(p => FilterOfflineUsers(p.Key, p.Value)));
|
||||
var filteredOfflinePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineUsers(p.Key, p.Value)));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs));
|
||||
|
||||
if (_configService.Current.ShowSyncshellOfflineUsersSeparately)
|
||||
{
|
||||
var allOfflineSyncshellUsers = ImmutablePairList(allPairs.Where(p => FilterOfflineSyncshellUsers(p.Key)));
|
||||
var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineSyncshellUsers(p.Key)));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag, filteredOfflineSyncshellUsers, allOfflineSyncshellUsers));
|
||||
}
|
||||
}
|
||||
|
||||
//Unpaired
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag,
|
||||
BasicSortedDictionary(filteredPairs.Where(p => p.Key.IsOneSidedPair)),
|
||||
ImmutablePairList(allPairs.Where(p => p.Key.IsOneSidedPair))));
|
||||
|
||||
return drawFolders;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool PassesFilter(Pair pair, string filter)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filter)) return true;
|
||||
|
||||
return pair.UserData.AliasOrUID.Contains(filter, StringComparison.OrdinalIgnoreCase) || (pair.GetNote()?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false) || (pair.PlayerName?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
}
|
||||
|
||||
private string AlphabeticalSortKey(Pair pair)
|
||||
{
|
||||
if (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(pair.PlayerName))
|
||||
{
|
||||
return _configService.Current.PreferNotesOverNamesForVisible ? (pair.GetNote() ?? string.Empty) : pair.PlayerName;
|
||||
}
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag,
|
||||
BasicSortedDictionary(filteredPairs.Where(u => u.Key.IsOneSidedPair)),
|
||||
ImmutablePairList(allPairs.Where(u => u.Key.IsOneSidedPair))));
|
||||
return pair.GetNote() ?? pair.UserData.AliasOrUID;
|
||||
}
|
||||
|
||||
return drawFolders;
|
||||
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()
|
||||
@@ -587,21 +871,21 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
return _apiController.ServerState switch
|
||||
{
|
||||
ServerState.Connecting => ImGuiColors.DalamudYellow,
|
||||
ServerState.Reconnecting => ImGuiColors.DalamudRed,
|
||||
ServerState.Connecting => UIColors.Get("LightlessYellow"),
|
||||
ServerState.Reconnecting => UIColors.Get("DimRed"),
|
||||
ServerState.Connected => UIColors.Get("LightlessPurple"),
|
||||
ServerState.Disconnected => ImGuiColors.DalamudYellow,
|
||||
ServerState.Disconnecting => ImGuiColors.DalamudYellow,
|
||||
ServerState.Unauthorized => ImGuiColors.DalamudRed,
|
||||
ServerState.VersionMisMatch => ImGuiColors.DalamudRed,
|
||||
ServerState.Offline => ImGuiColors.DalamudRed,
|
||||
ServerState.RateLimited => ImGuiColors.DalamudYellow,
|
||||
ServerState.NoSecretKey => ImGuiColors.DalamudYellow,
|
||||
ServerState.MultiChara => ImGuiColors.DalamudYellow,
|
||||
ServerState.OAuthMisconfigured => ImGuiColors.DalamudRed,
|
||||
ServerState.OAuthLoginTokenStale => ImGuiColors.DalamudRed,
|
||||
ServerState.NoAutoLogon => ImGuiColors.DalamudYellow,
|
||||
_ => ImGuiColors.DalamudRed
|
||||
ServerState.Disconnected => UIColors.Get("LightlessYellow"),
|
||||
ServerState.Disconnecting => UIColors.Get("LightlessYellow"),
|
||||
ServerState.Unauthorized => UIColors.Get("DimRed"),
|
||||
ServerState.VersionMisMatch => UIColors.Get("DimRed"),
|
||||
ServerState.Offline => UIColors.Get("DimRed"),
|
||||
ServerState.RateLimited => UIColors.Get("LightlessYellow"),
|
||||
ServerState.NoSecretKey => UIColors.Get("LightlessYellow"),
|
||||
ServerState.MultiChara => UIColors.Get("LightlessYellow"),
|
||||
ServerState.OAuthMisconfigured => UIColors.Get("DimRed"),
|
||||
ServerState.OAuthLoginTokenStale => UIColors.Get("DimRed"),
|
||||
ServerState.NoAutoLogon => UIColors.Get("LightlessYellow"),
|
||||
_ => UIColors.Get("DimRed")
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,16 +19,18 @@ public class DrawFolderGroup : DrawFolderBase
|
||||
private readonly GroupFullInfoDto _groupFullInfoDto;
|
||||
private readonly IdDisplayHandler _idDisplayHandler;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
|
||||
|
||||
public DrawFolderGroup(string id, GroupFullInfoDto groupFullInfoDto, ApiController apiController,
|
||||
IImmutableList<DrawUserPair> drawPairs, IImmutableList<Pair> allPairs, TagHandler tagHandler, IdDisplayHandler idDisplayHandler,
|
||||
LightlessMediator lightlessMediator, UiSharedService uiSharedService) :
|
||||
LightlessMediator lightlessMediator, UiSharedService uiSharedService, SelectTagForSyncshellUi selectTagForSyncshellUi) :
|
||||
base(id, drawPairs, allPairs, tagHandler, uiSharedService)
|
||||
{
|
||||
_groupFullInfoDto = groupFullInfoDto;
|
||||
_apiController = apiController;
|
||||
_idDisplayHandler = idDisplayHandler;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_selectTagForSyncshellUi = selectTagForSyncshellUi;
|
||||
}
|
||||
|
||||
protected override bool RenderIfEmpty => true;
|
||||
@@ -99,6 +101,13 @@ public class DrawFolderGroup : DrawFolderBase
|
||||
}
|
||||
UiSharedService.AttachToolTip("Copies all your notes for all users in this Syncshell to the clipboard." + Environment.NewLine + "They can be imported via Settings -> General -> Notes -> Import notes from clipboard");
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Folder, "Syncshell Groups", menuWidth, true))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
_selectTagForSyncshellUi.Open(_groupFullInfoDto);
|
||||
}
|
||||
UiSharedService.AttachToolTip("Choose syncshell groups for " + _groupFullInfoDto.GID);
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell", menuWidth, true) && UiSharedService.CtrlPressed())
|
||||
{
|
||||
_ = _apiController.GroupLeave(_groupFullInfoDto);
|
||||
@@ -185,7 +194,7 @@ public class DrawFolderGroup : DrawFolderBase
|
||||
|
||||
_uiSharedService.IconText(FontAwesomeIcon.UsersCog, (_groupFullInfoDto.GroupPermissions.IsPreferDisableAnimations() != individualAnimDisabled
|
||||
|| _groupFullInfoDto.GroupPermissions.IsPreferDisableSounds() != individualSoundsDisabled
|
||||
|| _groupFullInfoDto.GroupPermissions.IsPreferDisableVFX() != individualVFXDisabled) ? ImGuiColors.DalamudYellow : null);
|
||||
|| _groupFullInfoDto.GroupPermissions.IsPreferDisableVFX() != individualVFXDisabled) ? UIColors.Get("LightlessYellow") : null);
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
|
||||
@@ -13,13 +13,15 @@ public class DrawFolderTag : DrawFolderBase
|
||||
{
|
||||
private readonly ApiController _apiController;
|
||||
private readonly SelectPairForTagUi _selectPairForTagUi;
|
||||
private readonly RenamePairTagUi _renameTagUi;
|
||||
|
||||
public DrawFolderTag(string id, IImmutableList<DrawUserPair> drawPairs, IImmutableList<Pair> allPairs,
|
||||
TagHandler tagHandler, ApiController apiController, SelectPairForTagUi selectPairForTagUi, UiSharedService uiSharedService)
|
||||
TagHandler tagHandler, ApiController apiController, SelectPairForTagUi selectPairForTagUi, RenamePairTagUi renameTagUi, UiSharedService uiSharedService)
|
||||
: base(id, drawPairs, allPairs, tagHandler, uiSharedService)
|
||||
{
|
||||
_apiController = apiController;
|
||||
_selectPairForTagUi = selectPairForTagUi;
|
||||
_renameTagUi = renameTagUi;
|
||||
}
|
||||
|
||||
protected override bool RenderIfEmpty => _id switch
|
||||
@@ -100,14 +102,17 @@ public class DrawFolderTag : DrawFolderBase
|
||||
protected override void DrawMenu(float menuWidth)
|
||||
{
|
||||
ImGui.TextUnformatted("Group Menu");
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Select Pairs", menuWidth, true))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Select Pairs", menuWidth, isInPopup: true))
|
||||
{
|
||||
_selectPairForTagUi.Open(_id);
|
||||
}
|
||||
UiSharedService.AttachToolTip("Select Individual Pairs for this Pair Group");
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Pair Group", menuWidth, true) && UiSharedService.CtrlPressed())
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Edit, "Rename Pair Group", menuWidth, isInPopup: true))
|
||||
{
|
||||
_tagHandler.RemoveTag(_id);
|
||||
_renameTagUi.Open(_id);
|
||||
}
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Pair Group", menuWidth, isInPopup: true) && UiSharedService.CtrlPressed())
|
||||
{
|
||||
_tagHandler.RemovePairTag(_id);
|
||||
}
|
||||
UiSharedService.AttachToolTip("Hold CTRL to remove this Group permanently." + Environment.NewLine +
|
||||
"Note: this will not unpair with users in this Group.");
|
||||
|
||||
@@ -9,27 +9,37 @@ namespace LightlessSync.UI.Components;
|
||||
|
||||
public class DrawGroupedGroupFolder : IDrawFolder
|
||||
{
|
||||
private readonly string _tag;
|
||||
private readonly IEnumerable<IDrawFolder> _groups;
|
||||
private readonly TagHandler _tagHandler;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
|
||||
private readonly RenameSyncshellTagUi _renameSyncshellTagUi;
|
||||
private bool _wasHovered = false;
|
||||
private float _menuWidth;
|
||||
|
||||
public IImmutableList<DrawUserPair> DrawPairs => throw new NotSupportedException();
|
||||
public int OnlinePairs => _groups.SelectMany(g => g.DrawPairs).Where(g => g.Pair.IsOnline).DistinctBy(g => g.Pair.UserData.UID).Count();
|
||||
public int TotalPairs => _groups.Sum(g => g.TotalPairs);
|
||||
|
||||
public DrawGroupedGroupFolder(IEnumerable<IDrawFolder> groups, TagHandler tagHandler, UiSharedService uiSharedService)
|
||||
public DrawGroupedGroupFolder(IEnumerable<IDrawFolder> groups, TagHandler tagHandler, UiSharedService uiSharedService, SelectSyncshellForTagUi selectSyncshellForTagUi, RenameSyncshellTagUi renameSyncshellTagUi, string tag)
|
||||
{
|
||||
_groups = groups;
|
||||
_tagHandler = tagHandler;
|
||||
_uiSharedService = uiSharedService;
|
||||
_selectSyncshellForTagUi = selectSyncshellForTagUi;
|
||||
_renameSyncshellTagUi = renameSyncshellTagUi;
|
||||
_tag = tag;
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
if (!_groups.Any()) return;
|
||||
|
||||
string _id = "__folder_syncshells";
|
||||
if (_tag != "")
|
||||
{
|
||||
_id = $"__folder_{_tag}";
|
||||
}
|
||||
|
||||
using var id = ImRaii.PushId(_id);
|
||||
var color = ImRaii.PushColor(ImGuiCol.ChildBg, ImGui.GetColorU32(ImGuiCol.FrameBgHovered), _wasHovered);
|
||||
using (ImRaii.Child("folder__" + _id, new System.Numerics.Vector2(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX(), ImGui.GetFrameHeight())))
|
||||
@@ -49,18 +59,36 @@ public class DrawGroupedGroupFolder : IDrawFolder
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.UsersRectangle);
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X / 2f }))
|
||||
|
||||
if (_tag != "")
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("[" + OnlinePairs.ToString() + "]");
|
||||
_uiSharedService.IconText(FontAwesomeIcon.FolderPlus);
|
||||
}
|
||||
else
|
||||
{
|
||||
_uiSharedService.IconText(FontAwesomeIcon.UsersRectangle);
|
||||
}
|
||||
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X / 2f }))
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("[" + OnlinePairs.ToString() + "]");
|
||||
}
|
||||
UiSharedService.AttachToolTip(OnlinePairs + " online in all of your joined syncshells" + Environment.NewLine +
|
||||
TotalPairs + " pairs combined in all of your joined syncshells");
|
||||
ImGui.SameLine();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("All Syncshells");
|
||||
if (_tag != "")
|
||||
{
|
||||
ImGui.TextUnformatted(_tag);
|
||||
|
||||
ImGui.SameLine();
|
||||
DrawMenu();
|
||||
} else
|
||||
{
|
||||
ImGui.TextUnformatted("All Syncshells");
|
||||
}
|
||||
}
|
||||
color.Dispose();
|
||||
_wasHovered = ImGui.IsItemHovered();
|
||||
@@ -76,4 +104,40 @@ public class DrawGroupedGroupFolder : IDrawFolder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void DrawMenu()
|
||||
{
|
||||
var barButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.EllipsisV);
|
||||
var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
|
||||
|
||||
ImGui.SameLine(windowEndX - barButtonSize.X);
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.EllipsisV))
|
||||
{
|
||||
ImGui.OpenPopup("User Flyout Menu");
|
||||
}
|
||||
if (ImGui.BeginPopup("User Flyout Menu"))
|
||||
{
|
||||
using (ImRaii.PushId($"buttons-syncshell-{_tag}")) GroupMenu(_menuWidth);
|
||||
_menuWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
protected void GroupMenu(float menuWidth)
|
||||
{
|
||||
ImGui.TextUnformatted("Syncshell Group Menu");
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Select Syncshells", menuWidth, isInPopup: true))
|
||||
{
|
||||
_selectSyncshellForTagUi.Open(_tag);
|
||||
}
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Edit, "Rename Syncshell Group", menuWidth, isInPopup: true))
|
||||
{
|
||||
_renameSyncshellTagUi.Open(_tag);
|
||||
}
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Syncshell Group", menuWidth, isInPopup: true) && UiSharedService.CtrlPressed())
|
||||
{
|
||||
_tagHandler.RemoveSyncshellTag(_tag);
|
||||
}
|
||||
UiSharedService.AttachToolTip("Hold CTRL to remove this Group permanently.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
@@ -12,6 +11,7 @@ using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI.Handlers;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
|
||||
namespace LightlessSync.UI.Components;
|
||||
@@ -196,7 +196,7 @@ public class DrawUserPair
|
||||
|
||||
if (_pair.IsPaused)
|
||||
{
|
||||
using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow);
|
||||
using var _ = ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
|
||||
_uiSharedService.IconText(FontAwesomeIcon.PauseCircle);
|
||||
userPairText = _pair.UserData.AliasOrUID + " is paused";
|
||||
}
|
||||
@@ -274,7 +274,7 @@ public class DrawUserPair
|
||||
{
|
||||
ImGui.SameLine();
|
||||
|
||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudYellow);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
|
||||
|
||||
string userWarningText = "WARNING: This user exceeds one or more of your defined thresholds:" + UiSharedService.TooltipSeparator;
|
||||
bool shownVram = false;
|
||||
@@ -295,6 +295,31 @@ public class DrawUserPair
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (_pair.UserData.IsAdmin || _pair.UserData.IsModerator)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
|
||||
var iconId = _pair.UserData.IsAdmin ? 67 : 68;
|
||||
var colorKey = _pair.UserData.IsAdmin ? "LightlessAdminText" : "LightlessModeratorText";
|
||||
var roleColor = UIColors.Get(colorKey);
|
||||
|
||||
var iconPos = ImGui.GetCursorScreenPos();
|
||||
SeStringUtils.RenderIconWithHitbox(iconId, iconPos);
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, roleColor))
|
||||
{
|
||||
ImGui.TextUnformatted(_pair.UserData.IsAdmin
|
||||
? "Official Lightless Developer"
|
||||
: "Official Lightless Moderator");
|
||||
}
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void DrawName(float leftSide, float rightSide)
|
||||
@@ -376,7 +401,7 @@ public class DrawUserPair
|
||||
currentRightSide -= (_uiSharedService.GetIconSize(individualIcon).X + spacingX);
|
||||
|
||||
ImGui.SameLine(currentRightSide);
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled))
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"), individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled))
|
||||
_uiSharedService.IconText(individualIcon);
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
|
||||
79
LightlessSync/UI/Components/RenamePairTagUi.cs
Normal file
79
LightlessSync/UI/Components/RenamePairTagUi.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.UI.Handlers;
|
||||
|
||||
using System.Numerics;
|
||||
|
||||
namespace LightlessSync.UI.Components;
|
||||
|
||||
public class RenamePairTagUi
|
||||
{
|
||||
private readonly TagHandler _tagHandler;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private string _desiredName = string.Empty;
|
||||
private bool _opened = false;
|
||||
private bool _show = false;
|
||||
private string _tag = string.Empty;
|
||||
|
||||
public RenamePairTagUi(TagHandler tagHandler, UiSharedService uiSharedService)
|
||||
{
|
||||
_tagHandler = tagHandler;
|
||||
_uiSharedService = uiSharedService;
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
var workHeight = ImGui.GetMainViewport().WorkSize.Y / ImGuiHelpers.GlobalScale;
|
||||
var minSize = new Vector2(300, workHeight < 110 ? workHeight : 110) * ImGuiHelpers.GlobalScale;
|
||||
var maxSize = new Vector2(300, 110) * ImGuiHelpers.GlobalScale;
|
||||
|
||||
var popupName = $"Renaming Pair Group {_tag}";
|
||||
|
||||
if (!_show)
|
||||
{
|
||||
_opened = false;
|
||||
}
|
||||
|
||||
if (_show && !_opened)
|
||||
{
|
||||
ImGui.SetNextWindowSize(minSize);
|
||||
UiSharedService.CenterNextWindow(minSize.X, minSize.Y, ImGuiCond.Always);
|
||||
ImGui.OpenPopup(popupName);
|
||||
_opened = true;
|
||||
}
|
||||
|
||||
ImGui.SetNextWindowSizeConstraints(minSize, maxSize);
|
||||
if (ImGui.BeginPopupModal(popupName, ref _show, ImGuiWindowFlags.Popup | ImGuiWindowFlags.Modal))
|
||||
{
|
||||
ImGui.TextUnformatted($"Renaming {_tag}");
|
||||
|
||||
ImGui.InputTextWithHint("##desiredname", "Enter new group name", ref _desiredName, 20, ImGuiInputTextFlags.None);
|
||||
using (ImRaii.Disabled(string.IsNullOrEmpty(_desiredName)))
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, "Rename Group"))
|
||||
{
|
||||
RenameTag(_tag, _desiredName);
|
||||
_show = false;
|
||||
}
|
||||
}
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
else
|
||||
{
|
||||
_show = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Open(string tag)
|
||||
{
|
||||
_tag = tag;
|
||||
_desiredName = "";
|
||||
_show = true;
|
||||
}
|
||||
|
||||
public void RenameTag(string oldTag, string newTag)
|
||||
{
|
||||
_tagHandler.RenamePairTag(oldTag, newTag);
|
||||
}
|
||||
}
|
||||
79
LightlessSync/UI/Components/RenameSyncshellTagUi.cs
Normal file
79
LightlessSync/UI/Components/RenameSyncshellTagUi.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.UI.Handlers;
|
||||
|
||||
using System.Numerics;
|
||||
|
||||
namespace LightlessSync.UI.Components;
|
||||
|
||||
public class RenameSyncshellTagUi
|
||||
{
|
||||
private readonly TagHandler _tagHandler;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private string _desiredName = string.Empty;
|
||||
private bool _opened = false;
|
||||
private bool _show = false;
|
||||
private string _tag = string.Empty;
|
||||
|
||||
public RenameSyncshellTagUi(TagHandler tagHandler, UiSharedService uiSharedService)
|
||||
{
|
||||
_tagHandler = tagHandler;
|
||||
_uiSharedService = uiSharedService;
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
var workHeight = ImGui.GetMainViewport().WorkSize.Y / ImGuiHelpers.GlobalScale;
|
||||
var minSize = new Vector2(300, workHeight < 110 ? workHeight : 110) * ImGuiHelpers.GlobalScale;
|
||||
var maxSize = new Vector2(300, 110) * ImGuiHelpers.GlobalScale;
|
||||
|
||||
var popupName = $"Renaming Syncshell Group {_tag}";
|
||||
|
||||
if (!_show)
|
||||
{
|
||||
_opened = false;
|
||||
}
|
||||
|
||||
if (_show && !_opened)
|
||||
{
|
||||
ImGui.SetNextWindowSize(minSize);
|
||||
UiSharedService.CenterNextWindow(minSize.X, minSize.Y, ImGuiCond.Always);
|
||||
ImGui.OpenPopup(popupName);
|
||||
_opened = true;
|
||||
}
|
||||
|
||||
ImGui.SetNextWindowSizeConstraints(minSize, maxSize);
|
||||
if (ImGui.BeginPopupModal(popupName, ref _show, ImGuiWindowFlags.Popup | ImGuiWindowFlags.Modal))
|
||||
{
|
||||
ImGui.TextUnformatted($"Renaming {_tag}");
|
||||
|
||||
ImGui.InputTextWithHint("##desiredname", "Enter new group name", ref _desiredName, 20, ImGuiInputTextFlags.None);
|
||||
using (ImRaii.Disabled(string.IsNullOrEmpty(_desiredName)))
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, "Rename Group"))
|
||||
{
|
||||
RenameTag(_tag, _desiredName);
|
||||
_show = false;
|
||||
}
|
||||
}
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
else
|
||||
{
|
||||
_show = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Open(string tag)
|
||||
{
|
||||
_tag = tag;
|
||||
_desiredName = "";
|
||||
_show = true;
|
||||
}
|
||||
|
||||
public void RenameTag(string oldTag, string newTag)
|
||||
{
|
||||
_tagHandler.RenameSyncshellTag(oldTag, newTag);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user