Compare commits
214 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb83ca90cb | ||
|
|
35f0f6da5e | ||
| 7d151dac2b | |||
|
|
2eba5a1f30 | ||
| 0a6cb05883 | |||
|
|
838495810e | ||
| a207c8994b | |||
|
|
9b4e48ad3e | ||
| fb4810980e | |||
| 51e107d30a | |||
|
|
cc011743af | ||
|
|
f47fbda0d9 | ||
| fd3b42eff1 | |||
| 3262664d1c | |||
| afa0d9f101 | |||
|
|
a66a9407f5 | ||
|
|
34bbc34b5b | ||
|
|
be068ed6d1 | ||
|
|
3c3c8fd90b | ||
| 835a0a637d | |||
| 906f401940 | |||
| 5abc297a94 | |||
|
|
8aad714918 | ||
| b5bdededae | |||
|
|
487156e4f9 | ||
| 90d8f691d2 | |||
|
|
764bb8bae2 | ||
| 5d2c58bf3e | |||
|
|
6bb00c50d8 | ||
|
|
1a89c2caee | ||
|
|
1e97f27cb8 | ||
|
|
7aadbcec10 | ||
|
|
a32ac02c6d | ||
| f48698373b | |||
| ee20b6fa5f | |||
| f11741225b | |||
|
|
147baa4c1b | ||
|
|
4f5ef8ff4b | ||
|
|
fae6d31792 | ||
|
|
b4dd0ee0e1 | ||
| f5458c7f97 | |||
| 923f118a47 | |||
|
|
0cb71e5444 | ||
|
|
268fd471fe | ||
|
|
d517a21f5d | ||
| cac94374d9 | |||
|
|
b513e0555b | ||
|
|
de8c9cf035 | ||
|
|
7f8872cbe0 | ||
|
|
5626a34755 | ||
|
|
a98afdda01 | ||
| 4373092d44 | |||
| 7b5c61371e | |||
| 9bd997f699 | |||
| e2511a5c1f | |||
|
|
e8760a8937 | ||
|
|
7d4e097be8 | ||
|
|
2cba1ccfe0 | ||
| 06921e1dd1 | |||
|
|
68ba5f4b06 | ||
|
|
217c160ec7 | ||
|
|
ff010fa1f8 | ||
| f7145339b3 | |||
|
|
aa2b828386 | ||
|
|
547db3a76b | ||
|
|
4ac8b24524 | ||
|
|
d72cc207e1 | ||
|
|
477f5aa6e7 | ||
|
|
edb7232b17 | ||
|
|
44177ab7bd | ||
|
|
47b7ecd521 | ||
|
|
8a3902ec2b | ||
|
|
ea8f8e3895 | ||
|
|
f1af6601cc | ||
|
|
2d094404df | ||
|
|
8fdff1eb18 | ||
|
|
9170b5205c | ||
|
|
dccd2cdc36 | ||
|
|
6d01d47c2f | ||
|
|
280c80d89f | ||
|
|
92b8d4a1cd | ||
|
|
f77d109d00 | ||
|
|
823dd39a9b | ||
|
|
d5c12e81c3 | ||
|
|
04c00af92e | ||
|
|
a66a43dda8 | ||
| 77ff8ae372 | |||
|
|
011cf7951b | ||
|
|
7d480b9e2c | ||
|
|
cf27a67296 | ||
|
|
d6a4595bb8 | ||
|
|
f202818b55 | ||
|
|
3f2e4d6640 | ||
|
|
90b483e4ea | ||
| 434c7d5f4a | |||
|
|
a4eb840589 | ||
|
|
e80806ef9d | ||
|
|
c447c33b7a | ||
|
|
bcb524df52 | ||
| b64bb66119 | |||
|
|
118edb9dea | ||
|
|
b43ceb9f7e | ||
|
|
6c0d00dc39 | ||
|
|
be847c16b8 | ||
| 02a680f8cc | |||
|
|
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 | ||
|
|
98c3a2c7f8 | ||
|
|
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 |
289
.gitea/workflows/lightless-tag-and-release.yml
Normal file
289
.gitea/workflows/lightless-tag-and-release.yml
Normal file
@@ -0,0 +1,289 @@
|
||||
name: Tag and Release Lightless
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, dev ]
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: LightlessSync
|
||||
DOTNET_VERSION: |
|
||||
10.x.x
|
||||
9.x.x
|
||||
|
||||
jobs:
|
||||
tag-and-release:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout Lightless
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup .NET 10 SDK
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: |
|
||||
10.x.x
|
||||
9.x.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
|
||||
20
.gitmodules
vendored
20
.gitmodules
vendored
@@ -1,6 +1,18 @@
|
||||
[submodule "LightlessAPI"]
|
||||
path = LightlessAPI
|
||||
url = https://github.com/Light-Public-Syncshells/LightlessAPI
|
||||
[submodule "PenumbraAPI"]
|
||||
path = PenumbraAPI
|
||||
url = https://github.com/Ottermandias/Penumbra.Api.git
|
||||
url = https://git.lightless-sync.org/Lightless-Sync/LightlessAPI.git
|
||||
[submodule "Penumbra.GameData"]
|
||||
path = Penumbra.GameData
|
||||
url = https://github.com/Ottermandias/Penumbra.GameData
|
||||
[submodule "Penumbra.Api"]
|
||||
path = Penumbra.Api
|
||||
url = https://github.com/Ottermandias/Penumbra.Api
|
||||
[submodule "Penumbra.String"]
|
||||
path = Penumbra.String
|
||||
url = https://github.com/Ottermandias/Penumbra.String
|
||||
[submodule "OtterGui"]
|
||||
path = OtterGui
|
||||
url = https://github.com/Ottermandias/OtterGui
|
||||
[submodule "ffxiv_pictomancy"]
|
||||
path = ffxiv_pictomancy
|
||||
url = https://github.com/sourpuh/ffxiv_pictomancy
|
||||
|
||||
Submodule LightlessAPI updated: 3a69c94f7f...56566003e0
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.1.32328.378
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.0.11217.181
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{585B740D-BA2C-429B-9CF3-B2D223423748}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
@@ -12,40 +12,110 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessSync", "LightlessS
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessSync.API", "LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj", "{A4E42AFA-5045-7E81-937F-3A320AC52987}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.Api", "PenumbraAPI\Penumbra.Api.csproj", "{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.Api", "Penumbra.Api\Penumbra.Api.csproj", "{22AE06C8-5139-45D2-A5F9-E76C019050D9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{3C016B19-2A2C-4068-9378-B9B805605EFB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OtterGui", "OtterGui\OtterGui.csproj", "{C77A2833-3FE4-405B-811D-439B1FF859D9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pictomancy", "ffxiv_pictomancy\Pictomancy\Pictomancy.csproj", "{825F17D8-2704-24F6-DF8B-2542AC92C765}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.ActiveCfg = Release|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.Build.0 = Release|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|x64.Build.0 = Debug|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|Any CPU.Build.0 = Release|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|x64.ActiveCfg = Release|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|x64.Build.0 = Release|x64
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|x64.Build.0 = Debug|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Release|Any CPU.Build.0 = Release|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Release|x64.ActiveCfg = Release|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Release|x64.Build.0 = Release|x64
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|x86.Build.0 = Release|Any CPU
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|x64.Build.0 = Debug|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|x86.Build.0 = Debug|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|Any CPU.Build.0 = Release|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|x64.ActiveCfg = Release|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|x64.Build.0 = Release|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|x86.ActiveCfg = Release|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|x86.Build.0 = Release|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|x64.Build.0 = Debug|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|x86.Build.0 = Debug|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|Any CPU.Build.0 = Release|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|x64.ActiveCfg = Release|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|x64.Build.0 = Release|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|x86.ActiveCfg = Release|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|x86.Build.0 = Release|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|x64.Build.0 = Debug|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|x86.Build.0 = Debug|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|Any CPU.Build.0 = Release|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|x64.ActiveCfg = Release|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|x64.Build.0 = Release|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|x86.ActiveCfg = Release|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|x86.Build.0 = Release|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|x64.Build.0 = Debug|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|x86.Build.0 = Debug|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|Any CPU.Build.0 = Release|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x64.ActiveCfg = Release|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x64.Build.0 = Release|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x86.ActiveCfg = Release|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x86.Build.0 = Release|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x64.Build.0 = Debug|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x86.Build.0 = Debug|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|Any CPU.Build.0 = Release|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x64.ActiveCfg = Release|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x64.Build.0 = Release|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.ActiveCfg = Release|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
312
LightlessSync/Changelog/changelog.yaml
Normal file
312
LightlessSync/Changelog/changelog.yaml
Normal file
@@ -0,0 +1,312 @@
|
||||
tagline: "Lightless Sync v2.0.1"
|
||||
subline: "LIGHTLESS IS EVOLVING!!"
|
||||
changelog:
|
||||
- name: "v2.0.1"
|
||||
tagline: "Some Fixes"
|
||||
date: "December 23 2025"
|
||||
# be sure to set this every new version
|
||||
isCurrent: true
|
||||
versions:
|
||||
- number: "Chat"
|
||||
icon: ""
|
||||
items:
|
||||
- "You can turn off the syncshell chat as Owner by going to the Syncshell Admin panel -> Owner -> Enable/Disable Chat."
|
||||
- "Fixed an issue where you can't chat due to regions being in a different language."
|
||||
- number: "LightFinder"
|
||||
icon: ""
|
||||
items:
|
||||
- "The icon/Lightfinder Text will be hidden when Game UI is hidden and behind game elements/UI"
|
||||
- "Able to select an icon for the selected list or a custom glyph if you know the code."
|
||||
- "Smoothing and reducing jitter on the icon/Lightfinder Text."
|
||||
- "Fixed so higher scaled UI options (100/150/200% UI scale) wouldn't break the element."
|
||||
- "Detects if GPose is active, wouldn't render the elements"
|
||||
- number: "Miscellaneous fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Fixed the null error given on GetCID when transferring between zones/housing."
|
||||
- "Added push/pop on certain ImGUI elements to remove them after being used. "
|
||||
- "Having all tabs open in the Main UI wouldn't lag out the game anymore."
|
||||
- "Cycle pause has been adjusted to the old function. There is a separate button to pause normally, now called 'Toggle (Un)Pause State'."
|
||||
- "Changes have been made to the character redraw to address the issues with the building character data constantly being redrawn and the redrawn behavior with Honorific titles."
|
||||
- "GPose characters should appear again in the actor screen"
|
||||
- "Lightspeed download console messages are no longer shown as warnings."
|
||||
- number: "Server Updates"
|
||||
icon: ""
|
||||
items:
|
||||
- "Changes have been made to the disabling of your profile. It should save again."
|
||||
- "Ability added to toggle chats from syncshell to be disabled."
|
||||
- "Files are continuously being deleted due to high volumes in storage, potentially causing MCDOs to have missing files. We have increased the limit of the storage in our configurations to see if that helps."
|
||||
- name: "v2.0.0"
|
||||
tagline: "Thank you for 4 months!"
|
||||
date: "December 2025"
|
||||
versions:
|
||||
- number: "Lightless Chat"
|
||||
icon: ""
|
||||
items:
|
||||
- "Chat has been added to the top of the main UI. It will work in certain Zones or in Syncshells!"
|
||||
- "You will only be able to use the chat feature after enabling it and accepting the rules. If you're not interested, don't use it!"
|
||||
- "Breaking the rules may result in a mute or ban from chat. Serious offenses may result in a ban from the Lightless service altogether."
|
||||
- "You can right click the offender in the chat and report them within the chat, reports will be reviewed asap."
|
||||
- "Syncshells can enforce their own chat rules and moderate their own chat. This however does not apply to serious offenses."
|
||||
- "Your name in chat will not be shown unless you are paired with the person OR you are in the same syncshell. Otherwise, you will be anonymous."
|
||||
- "Refer to #release-notes in the Discord for more information. Feel free to ask questions in the Discord as well."
|
||||
- number: "Changes to LightFinder"
|
||||
icon: ""
|
||||
items:
|
||||
- "We have recieve quite a bit of reports of users crashing due to how Nameplates are handled across various plugins. As a result, we have moved the LightFinder icon and text to Imgui."
|
||||
- "This should resolve the crashing issues, however, it may not look as nice as before. We are looking into ways to improve the Imgui experience in the future."
|
||||
- "We will always prioritize stability and safety over visuals."
|
||||
- "Refer to #release-notes in the Discord for an example of the error."
|
||||
- number: "User Profiles, ShellFinder, Syncshells, Syncshell Profiles"
|
||||
icon: ""
|
||||
items:
|
||||
- "Both User Profiles and Syncshell Profiles have been revamped for 2.0.0."
|
||||
- "We have added profile tags to both Users and Syncshells that will show when a profile is being viewed"
|
||||
- "Syncshell Admin Panel has been reworked to make it a friendlier experience"
|
||||
- "Syncshell Moderators can now also broadcast on ShellFinder"
|
||||
- "ShellFinder has been revamped to be more visually friends and also show more information (Tags) about the Syncshell"
|
||||
- "Syncshells has an auto-prune feature now that will remove inactive members after a set amount of time, options available are 1, 3, 7, and 14 days that runs in 1 hour intervals"
|
||||
- "IF YOUR SYNCSHELL IS NSFW, PLEASE MARK IT AS NSFW!"
|
||||
- "Refer to #release-notes in the Discord for pretty pictures or try it yourself!."
|
||||
- number: "Texture Optimization"
|
||||
icon: ""
|
||||
items:
|
||||
- "In 2.0.0, we've added the option for Texture Optimization to improve the performance of scenarios such as overwhelmingly big "
|
||||
- "NOTE: ALL OF THESE ARE OPTIONAL AND DISABLED BY DEFAULT"
|
||||
- "Within Texture Optimization, you will be able to safely downscale all textures of new downloads around you."
|
||||
- "This downscale DOES NOT APPLY to DIRECT PAIRS or those who've updated their preferred settings to not be downscaled"
|
||||
- "The first time this is enabled, you may experience some lag or frame drops, but in the long run, it will help performance."
|
||||
- "This can be found in Lightless Settings > Performance > Texture Optimization"
|
||||
- "Like a broken record, please refer to #release-notes in the Discord for more information."
|
||||
- number: "Character Analysis - The big scary UI no one knew about"
|
||||
icon: ""
|
||||
items:
|
||||
- "We have made the Character Analysis UI more user friendly. This includes a revamp of the look and functionality"
|
||||
- "You can now see more information about your character and how it affects performance"
|
||||
- "It will show you the Textures tab by default with an option for \"Other file types\""
|
||||
- "You can now choose if you want to BC7/BC5/BC4/BC3/BC1 compress a certain texture."
|
||||
- "The UI will give you a recommendation on what BC compression to use based on the file."
|
||||
- "Shows a small preview of what the texture looks like with some general info about it."
|
||||
- "Shows you how much VRAM you would take up."
|
||||
- "This can be found in Lightless Settings > Performance > Character Analysis"
|
||||
- number: "Performance"
|
||||
icon: ""
|
||||
items:
|
||||
- "Moved to the internal object table to have improved overall plugin performance."
|
||||
- "Compactor is now running on a multi-threaded level instead of single-threaded; This should increase the speed of compacting files."
|
||||
- "Penumbra Collections are now only made when people are visible, reducing the load on boot-up when having many Syncshells in your list."
|
||||
- "Pairing system has been revamped to make pausing and unpausing faster, and loading people should be faster as well."
|
||||
- number: "Miscellaneous Changes and Bugfixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "UI has been updated to look more modern"
|
||||
- "We have started on file compression for Linux with the option for BTRFS or ZFS but it's not very great yet and will release later."
|
||||
- "Nameplate colours now use sigs to client structs as an alternative to the Nameplate Handler, also preventing crashes on that from our end."
|
||||
- "Notifications now work with the \"Enable multi-monitor windows\" settings of Dalamud."
|
||||
- "Fixed a bug where nothing above the notifications was clickable in certain cases."
|
||||
- "Added a check that prevents small messages from going below 0 resulting in an ArgumentOutOfRangeException."
|
||||
- name: "v1.12.4"
|
||||
tagline: "Preparation for future features"
|
||||
date: "November 11th 2025"
|
||||
versions:
|
||||
- number: "Syncshells"
|
||||
icon: ""
|
||||
items:
|
||||
- "Added a pause button for syncshells in grouped folders"
|
||||
- number: "Notifications"
|
||||
icon: ""
|
||||
items:
|
||||
- "Fixed download notifications getting stuck at times"
|
||||
- "Added more offset positions for the notifications"
|
||||
- number: "Lightfinder"
|
||||
icon: ""
|
||||
items:
|
||||
- "Pair button will now show up when you're not directly paired. ie. You are technically paired in a syncshell, but you may not be directly paired'"
|
||||
- "Fixed a problem where the number of LightFinder users were cached, displaying the wrong information"
|
||||
- "When LightFinder is enabled, if you hover over the number of users, it will show you how many users are also using LightFinder in your area"
|
||||
-
|
||||
- number: "Bugfixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Added even more checks to nameplate handler to help not only us debug, but also other plugins writing to the plate"
|
||||
- number: "Miscellaneous Changes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Default Linux to Websockets"
|
||||
- "Revised Brio warning"
|
||||
- "Added /lightless command to open Lightless UI (you can still use /light)"
|
||||
- "Initial groundwork for future features"
|
||||
- name: "v1.12.3"
|
||||
tagline: "LightSpeed, Welcome Screen, and More!"
|
||||
date: "October 15th 2025"
|
||||
versions:
|
||||
- number: "LightSpeed"
|
||||
icon: ""
|
||||
items:
|
||||
- "New way to download that will download mods directly from the file server"
|
||||
- "LightSpeed is in BETA and should be faster than the batch downloading"
|
||||
- number: "Welcome Screen + Additional Features"
|
||||
icon: ""
|
||||
items:
|
||||
- "New in-game Patch Notes window."
|
||||
- "Credits section to thank contributors and supporters."
|
||||
- "Patch notes only show after updates, not during first-time setup."
|
||||
- "Syncshell Rework stared: Profiles have been added (more features using this will come later)."
|
||||
- number: "Notifications"
|
||||
icon: ""
|
||||
items:
|
||||
- "More customizable notification options."
|
||||
- "Perfomance limiter shows as notifications."
|
||||
- "All notifications can be configured or disabled in Settings → Notifications."
|
||||
- "Cleaning up notifications implementation"
|
||||
- number: "Bugfixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Added more safety checks to nameplates"
|
||||
- "Removed a line in SyncshellUI potentially causing NullPointers"
|
||||
- "Additional safety checks in PlayerData.Factory"
|
||||
- name: "v1.12.2"
|
||||
tagline: "LightFinder fixes, Notifications overhaul"
|
||||
date: "October 12th 2025"
|
||||
versions:
|
||||
- number: "LightFinder"
|
||||
icon: ""
|
||||
items:
|
||||
- "Server-side improvements for LightFinder functionality."
|
||||
- "Command changed from '/light lightfinder' to '/light finder'."
|
||||
- "Option to enable LightFinder on connection (opt-in, refreshes every 3 hours)."
|
||||
- "LightFinder indicator can now be shown on the server info bar."
|
||||
- number: "Notifications"
|
||||
icon: ""
|
||||
items:
|
||||
- "Completely reworked notification system with new UI."
|
||||
- "Pair requests now show as notifications."
|
||||
- "Download progress shows as notifications."
|
||||
- "Customizable notification sounds, size, position, and duration."
|
||||
- "All notifications can be configured or disabled in Settings → Notifications."
|
||||
- number: "Bug Fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Fixed nameplate alignment issues with LightFinder and icons."
|
||||
- "Icons now properly apply instead of swapping on choice."
|
||||
- "Updated Discord URL."
|
||||
- "File cache logic improvements."
|
||||
|
||||
- name: "v1.12.1"
|
||||
tagline: "LightFinder customization and download limiter"
|
||||
date: "October 8th 2025"
|
||||
versions:
|
||||
- number: "New Features"
|
||||
icon: ""
|
||||
items:
|
||||
- "LightFinder text can be modified to an icon with customizable positioning."
|
||||
- "Option to hide your own indicator or paired player indicators."
|
||||
- "Pair Download Limiter: Limit simultaneous downloads to 1-6 users to reduce network strain."
|
||||
- "Added '/light lightfinder' command to open LightFinder UI."
|
||||
- number: "Improvements"
|
||||
icon: ""
|
||||
items:
|
||||
- "Right-click menu option for Send Pair Request can be disabled."
|
||||
- "Syncshell finder improvements."
|
||||
- "Download limiter settings available in Settings → Transfers."
|
||||
|
||||
- name: "v1.12.0"
|
||||
tagline: "LightFinder - Major feature release"
|
||||
date: "October 5th 2025"
|
||||
versions:
|
||||
- number: "Major Features"
|
||||
icon: ""
|
||||
items:
|
||||
- "Introduced LightFinder: Optional feature inspired by FFXIV's Party Finder."
|
||||
- "Find fellow Lightless users and advertise your Syncshell to others."
|
||||
- "When enabled, you're visible to other LightFinder users for 3 hours."
|
||||
- "LightFinder tag displays above your nameplate when active."
|
||||
- "Receive pair requests directly in UI without exchanging UIDs."
|
||||
- "Syncshell Finder allows joining indexed Syncshells."
|
||||
- "[L] Send Pair Request added to player context menus."
|
||||
- number: "Vanity Features"
|
||||
icon: ""
|
||||
items:
|
||||
- "Supporters can now customize their name color in the Lightless UI."
|
||||
- "Color changes visible to all users."
|
||||
- number: "General Improvements"
|
||||
icon: ""
|
||||
items:
|
||||
- "Pairing nameplate color override can now override FC tags."
|
||||
- "Added .kdb as whitelisted filetype for uploads."
|
||||
- "Various UI fixes, updates, and improvements."
|
||||
|
||||
- name: "v1.11.12"
|
||||
tagline: "Syncshell grouping and performance options"
|
||||
date: "September 16th 2025"
|
||||
versions:
|
||||
- number: "New Features"
|
||||
icon: ""
|
||||
items:
|
||||
- "Ability to show grouped syncshells in main UI/all syncshells (default ON)."
|
||||
- "Transfer ownership button available in Admin Panel user list."
|
||||
- "Self-threshold warning now opens character analysis screen when clicked."
|
||||
- number: "Performance"
|
||||
icon: ""
|
||||
items:
|
||||
- "Auto-pause combat and auto-pause performance are now optional settings."
|
||||
- "Both options are auto-enabled by default - disable at your own risk."
|
||||
- number: "Bug Fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Reworked file caching to reduce errors for some users."
|
||||
- "Fixed bug where exiting PvP could desync some users."
|
||||
|
||||
- name: "v1.11.9"
|
||||
tagline: "File cache improvements"
|
||||
date: "September 13th 2025"
|
||||
versions:
|
||||
- number: "Bug Fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Identified and fixed potential file cache problems."
|
||||
- "Improved cache error handling and stability."
|
||||
|
||||
- name: "v1.11.8"
|
||||
tagline: "Hotfix - UI and exception handling"
|
||||
date: "September 12th 2025"
|
||||
versions:
|
||||
- number: "Bug Fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Attempted fix for NullReferenceException spam."
|
||||
- "Fixed additional UI edge cases preventing loading for some users."
|
||||
- "Fixed color bar UI issues."
|
||||
|
||||
- name: "v1.11.7"
|
||||
tagline: "Hotfix - UI loading and warnings"
|
||||
date: "September 12th 2025"
|
||||
versions:
|
||||
- number: "Bug Fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Fixed UI not loading for some users."
|
||||
- "Self warnings now behind 'Warn on loading in players exceeding performance thresholds' setting."
|
||||
|
||||
- name: "v1.11.6"
|
||||
tagline: "Admin panel rework and new features"
|
||||
date: "September 11th 2025"
|
||||
versions:
|
||||
- number: "New Features"
|
||||
icon: ""
|
||||
items:
|
||||
- "Reworked Syncshell Admin Page with improved styling."
|
||||
- "Right-click on Server Top Bar button to disconnect from Lightless."
|
||||
- "Shift+Left click on Server Top Bar button to open settings."
|
||||
- "Added colors section in settings to change accent colors."
|
||||
- "Ability to pause syncing while in Instance/Duty."
|
||||
- "Functionality to create syncshell folders."
|
||||
- "Added self-threshold warning."
|
||||
- number: "Bug Fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Fixed owners being visible in moderator list view."
|
||||
- "Removed Pin/Remove/Ban buttons on Owners when viewing as moderator."
|
||||
- "Fixed nameplate bug in PvP."
|
||||
- "Added 1 or 3 day options for inactive check."
|
||||
- "Fixed bug where some users could not see their own syncshell folders."
|
||||
68
LightlessSync/Changelog/credits.yaml
Normal file
68
LightlessSync/Changelog/credits.yaml
Normal file
@@ -0,0 +1,68 @@
|
||||
credits:
|
||||
- category: "Development Team"
|
||||
items:
|
||||
- name: "Abel"
|
||||
role: "Developer"
|
||||
- name: "Cake"
|
||||
role: "Developer"
|
||||
- name: "Celine"
|
||||
role: "Developer"
|
||||
- name: "Choco"
|
||||
role: "Developer"
|
||||
- name: "Kenny"
|
||||
role: "Developer"
|
||||
- name: "Zura"
|
||||
role: "Developer"
|
||||
- name: "Additional Contributors"
|
||||
role: "Community Contributors & Bug Reporters"
|
||||
|
||||
- category: "Moderation Team"
|
||||
items:
|
||||
- name: "Crow"
|
||||
role: "Moderator"
|
||||
- name: "Faith"
|
||||
role: "Moderator"
|
||||
- name: "Kiwiwiwi"
|
||||
role: "Moderator"
|
||||
- name: "Kruwu"
|
||||
role: "Moderator"
|
||||
- name: "Lexi"
|
||||
role: "Moderator"
|
||||
- name: "Maya"
|
||||
role: "Moderator"
|
||||
- name: "Metaknight"
|
||||
role: "Moderator"
|
||||
- name: "Minmoose"
|
||||
role: "Moderator"
|
||||
- name: "Nihal"
|
||||
role: "Moderator"
|
||||
- name: "Tani"
|
||||
role: "Moderator"
|
||||
|
||||
- category: "Plugin Integration & IPC Support"
|
||||
items:
|
||||
- name: "Penumbra Team"
|
||||
role: "Mod framework integration"
|
||||
- name: "Glamourer Team"
|
||||
role: "Customization system integration"
|
||||
- name: "Customize+ Team"
|
||||
role: "Body scaling integration"
|
||||
- name: "Simple Heels Team"
|
||||
role: "Height offset integration"
|
||||
- name: "Honorific Team"
|
||||
role: "Title system integration"
|
||||
- name: "Glyceri"
|
||||
role: "Moodles - Status effect integration"
|
||||
- name: "Glyceri"
|
||||
role: "PetNicknames - Pet naming integration"
|
||||
- name: "Minmoose"
|
||||
role: "Brio - GPose enhancement integration"
|
||||
|
||||
- category: "Special Thanks"
|
||||
items:
|
||||
- name: "Dalamud & XIVLauncher Teams"
|
||||
role: "Plugin framework and infrastructure"
|
||||
- name: "Community Supporters"
|
||||
role: "Testing, feedback, and financial support"
|
||||
- name: "Beta Testers"
|
||||
role: "Early testing and bug reporting"
|
||||
@@ -6,6 +6,7 @@ using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
|
||||
namespace LightlessSync.FileCache;
|
||||
|
||||
@@ -20,7 +21,8 @@ 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"];
|
||||
private static readonly HashSet<string> AllowedFileExtensionSet = new(AllowedFileExtensions, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
|
||||
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
|
||||
@@ -72,7 +74,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
while (_dalamudUtil.IsOnFrameworkThread && !token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(1).ConfigureAwait(false);
|
||||
await Task.Delay(1, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
RecalculateFileCacheSize(token);
|
||||
@@ -101,8 +103,8 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
}
|
||||
|
||||
record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null);
|
||||
private readonly Dictionary<string, WatcherChange> _watcherChanges = new Dictionary<string, WatcherChange>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, WatcherChange> _lightlessChanges = new Dictionary<string, WatcherChange>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, WatcherChange> _watcherChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, WatcherChange> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public void StopMonitoring()
|
||||
{
|
||||
@@ -115,6 +117,8 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
|
||||
public bool StorageisNTFS { get; private set; } = false;
|
||||
|
||||
public bool StorageIsBtrfs { get ; private set; } = false;
|
||||
|
||||
public void StartLightlessWatcher(string? lightlessPath)
|
||||
{
|
||||
LightlessWatcher?.Dispose();
|
||||
@@ -124,10 +128,19 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
Logger.LogWarning("Lightless file path is not set, cannot start the FSW for Lightless.");
|
||||
return;
|
||||
}
|
||||
var fsType = FileSystemHelper.GetFilesystemType(_configService.Current.CacheFolder, _dalamudUtil.IsWine);
|
||||
|
||||
DriveInfo di = new(new DirectoryInfo(_configService.Current.CacheFolder).Root.FullName);
|
||||
StorageisNTFS = string.Equals("NTFS", di.DriveFormat, StringComparison.OrdinalIgnoreCase);
|
||||
Logger.LogInformation("Lightless Storage is on NTFS drive: {isNtfs}", StorageisNTFS);
|
||||
if (fsType == FileSystemHelper.FilesystemType.NTFS && !_dalamudUtil.IsWine)
|
||||
{
|
||||
StorageisNTFS = true;
|
||||
Logger.LogInformation("Lightless Storage is on NTFS drive: {isNtfs}", StorageisNTFS);
|
||||
}
|
||||
|
||||
if (fsType == FileSystemHelper.FilesystemType.Btrfs)
|
||||
{
|
||||
StorageIsBtrfs = true;
|
||||
Logger.LogInformation("Lightless Storage is on BTRFS drive: {isBtrfs}", StorageIsBtrfs);
|
||||
}
|
||||
|
||||
Logger.LogDebug("Initializing Lightless FSW on {path}", lightlessPath);
|
||||
LightlessWatcher = new()
|
||||
@@ -152,7 +165,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
Logger.LogTrace("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
|
||||
|
||||
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||
if (!HasAllowedExtension(e.FullPath)) return;
|
||||
|
||||
lock (_watcherChanges)
|
||||
{
|
||||
@@ -196,7 +209,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
private void Fs_Changed(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
if (Directory.Exists(e.FullPath)) return;
|
||||
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||
if (!HasAllowedExtension(e.FullPath)) return;
|
||||
|
||||
if (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created))
|
||||
return;
|
||||
@@ -220,7 +233,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
foreach (var file in directoryFiles)
|
||||
{
|
||||
if (!AllowedFileExtensions.Any(ext => file.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) continue;
|
||||
if (!HasAllowedExtension(file)) continue;
|
||||
var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_watcherChanges.Remove(oldPath);
|
||||
@@ -232,7 +245,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||
if (!HasAllowedExtension(e.FullPath)) return;
|
||||
|
||||
lock (_watcherChanges)
|
||||
{
|
||||
@@ -248,9 +261,21 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
|
||||
private CancellationTokenSource _penumbraFswCts = new();
|
||||
private CancellationTokenSource _lightlessFswCts = new();
|
||||
|
||||
public FileSystemWatcher? PenumbraWatcher { get; private set; }
|
||||
public FileSystemWatcher? LightlessWatcher { get; private set; }
|
||||
|
||||
private static bool HasAllowedExtension(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
return !string.IsNullOrEmpty(extension) && AllowedFileExtensionSet.Contains(extension);
|
||||
}
|
||||
|
||||
private async Task LightlessWatcherExecution()
|
||||
{
|
||||
_lightlessFswCts = _lightlessFswCts.CancelRecreate();
|
||||
@@ -383,7 +408,7 @@ 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;
|
||||
@@ -392,51 +417,140 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
|
||||
public void RecalculateFileCacheSize(CancellationToken token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder))
|
||||
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) ||
|
||||
!Directory.Exists(_configService.Current.CacheFolder))
|
||||
{
|
||||
FileCacheSize = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
FileCacheSize = -1;
|
||||
DriveInfo di = new(new DirectoryInfo(_configService.Current.CacheFolder).Root.FullName);
|
||||
bool isWine = _dalamudUtil?.IsWine ?? false;
|
||||
|
||||
try
|
||||
{
|
||||
FileCacheDriveFree = di.AvailableFreeSpace;
|
||||
var drive = DriveInfo.GetDrives()
|
||||
.FirstOrDefault(d => _configService.Current.CacheFolder
|
||||
.StartsWith(d.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (drive != null)
|
||||
FileCacheDriveFree = drive.AvailableFreeSpace;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Could not determine drive size for Storage Folder {folder}", _configService.Current.CacheFolder);
|
||||
Logger.LogWarning(ex, "Could not determine drive size for storage folder {folder}", _configService.Current.CacheFolder);
|
||||
}
|
||||
|
||||
var files = Directory.EnumerateFiles(_configService.Current.CacheFolder).Select(f => new FileInfo(f))
|
||||
.OrderBy(f => f.LastAccessTime).ToList();
|
||||
FileCacheSize = files
|
||||
.Sum(f =>
|
||||
var files = Directory.EnumerateFiles(_configService.Current.CacheFolder)
|
||||
.Select(f => new FileInfo(f))
|
||||
.OrderBy(f => f.LastAccessTime)
|
||||
.ToList();
|
||||
|
||||
long totalSize = 0;
|
||||
|
||||
foreach (var f in files)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
long size = 0;
|
||||
|
||||
if (!isWine)
|
||||
{
|
||||
try
|
||||
{
|
||||
size = _fileCompactor.GetFileSizeOnDisk(f);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName);
|
||||
size = f.Length;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
size = f.Length;
|
||||
}
|
||||
|
||||
totalSize += size;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogTrace(ex, "Error getting size for {file}", f.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
FileCacheSize = totalSize;
|
||||
|
||||
if (Directory.Exists(_configService.Current.CacheFolder + "/downscaled"))
|
||||
{
|
||||
var filesDownscaled = Directory.EnumerateFiles(_configService.Current.CacheFolder + "/downscaled").Select(f => new FileInfo(f)).OrderBy(f => f.LastAccessTime).ToList();
|
||||
|
||||
long totalSizeDownscaled = 0;
|
||||
|
||||
foreach (var f in filesDownscaled)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
return _fileCompactor.GetFileSizeOnDisk(f, StorageisNTFS);
|
||||
long size = 0;
|
||||
|
||||
if (!isWine)
|
||||
{
|
||||
try
|
||||
{
|
||||
size = _fileCompactor.GetFileSizeOnDisk(f);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName);
|
||||
size = f.Length;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
size = f.Length;
|
||||
}
|
||||
|
||||
totalSizeDownscaled += size;
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
return 0;
|
||||
Logger.LogTrace(ex, "Error getting size for {file}", f.FullName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FileCacheSize = (totalSize + totalSizeDownscaled);
|
||||
}
|
||||
else
|
||||
{
|
||||
FileCacheSize = totalSize;
|
||||
}
|
||||
|
||||
var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d);
|
||||
|
||||
if (FileCacheSize < maxCacheInBytes) return;
|
||||
if (FileCacheSize < maxCacheInBytes)
|
||||
return;
|
||||
|
||||
var maxCacheBuffer = maxCacheInBytes * 0.05d;
|
||||
while (FileCacheSize > maxCacheInBytes - (long)maxCacheBuffer)
|
||||
|
||||
while (FileCacheSize > maxCacheInBytes - (long)maxCacheBuffer && files.Count > 0)
|
||||
{
|
||||
var oldestFile = files[0];
|
||||
FileCacheSize -= _fileCompactor.GetFileSizeOnDisk(oldestFile);
|
||||
File.Delete(oldestFile.FullName);
|
||||
files.Remove(oldestFile);
|
||||
|
||||
try
|
||||
{
|
||||
long fileSize = oldestFile.Length;
|
||||
File.Delete(oldestFile.FullName);
|
||||
FileCacheSize -= fileSize;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogTrace(ex, "Failed to delete old file {file}", oldestFile.FullName);
|
||||
}
|
||||
|
||||
files.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,12 +570,19 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
_scanCancellationTokenSource?.Cancel();
|
||||
// Disposing of file system watchers
|
||||
PenumbraWatcher?.Dispose();
|
||||
LightlessWatcher?.Dispose();
|
||||
|
||||
// Disposing of cancellation token sources
|
||||
_scanCancellationTokenSource?.CancelDispose();
|
||||
_scanCancellationTokenSource?.Dispose();
|
||||
_penumbraFswCts?.CancelDispose();
|
||||
_penumbraFswCts?.Dispose();
|
||||
_lightlessFswCts?.CancelDispose();
|
||||
_lightlessFswCts?.Dispose();
|
||||
_periodicCalculationTokenSource?.CancelDispose();
|
||||
_periodicCalculationTokenSource?.Dispose();
|
||||
}
|
||||
|
||||
private void FullFileScan(CancellationToken ct)
|
||||
@@ -498,7 +619,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
[
|
||||
.. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
|
||||
.AsParallel()
|
||||
.Where(f => AllowedFileExtensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(f => HasAllowedExtension(f)
|
||||
&& !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
|
||||
&& !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
|
||||
&& !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)),
|
||||
@@ -539,7 +660,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
|
||||
List<FileCacheEntity> entitiesToRemove = [];
|
||||
List<FileCacheEntity> entitiesToUpdate = [];
|
||||
object sync = new();
|
||||
Lock sync = new();
|
||||
Thread[] workerThreads = new Thread[threadCount];
|
||||
|
||||
ConcurrentQueue<FileCacheEntity> fileCaches = new(_fileDbManager.GetAllFileCaches());
|
||||
@@ -583,7 +704,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 +740,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
return;
|
||||
}
|
||||
|
||||
if (entitiesToUpdate.Any() || entitiesToRemove.Any())
|
||||
if (entitiesToUpdate.Count != 0 || entitiesToRemove.Count != 0)
|
||||
{
|
||||
foreach (var entity in entitiesToUpdate)
|
||||
{
|
||||
@@ -637,38 +765,44 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
|
||||
if (ct.IsCancellationRequested) return;
|
||||
|
||||
// scan new files
|
||||
if (allScannedFiles.Any(c => !c.Value))
|
||||
var newFiles = allScannedFiles.Where(c => !c.Value).Select(c => c.Key).ToList();
|
||||
foreach (var cachePath in newFiles)
|
||||
{
|
||||
Parallel.ForEach(allScannedFiles.Where(c => !c.Value).Select(c => c.Key),
|
||||
new ParallelOptions()
|
||||
{
|
||||
MaxDegreeOfParallelism = threadCount,
|
||||
CancellationToken = ct
|
||||
}, (cachePath) =>
|
||||
{
|
||||
if (ct.IsCancellationRequested) return;
|
||||
if (ct.IsCancellationRequested) break;
|
||||
ProcessOne(cachePath);
|
||||
Interlocked.Increment(ref _currentFileProgress);
|
||||
}
|
||||
|
||||
if (!_ipcManager.Penumbra.APIAvailable)
|
||||
{
|
||||
Logger.LogWarning("Penumbra not available");
|
||||
return;
|
||||
}
|
||||
Logger.LogTrace("Scanner added {count} new files to db", newFiles.Count);
|
||||
|
||||
try
|
||||
{
|
||||
var entry = _fileDbManager.CreateFileEntry(cachePath);
|
||||
if (entry == null) _ = _fileDbManager.CreateCacheEntry(cachePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed adding {file}", cachePath);
|
||||
}
|
||||
void ProcessOne(string? 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;
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _currentFileProgress);
|
||||
});
|
||||
if (!_ipcManager.Penumbra.APIAvailable)
|
||||
{
|
||||
Logger.LogWarning("Penumbra not available");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogTrace("Scanner added {notScanned} new files to db", allScannedFiles.Count(c => !c.Value));
|
||||
try
|
||||
{
|
||||
var entry = _fileDbManager.CreateFileEntry(cachePath);
|
||||
if (entry == null) _ = _fileDbManager.CreateCacheEntry(cachePath);
|
||||
}
|
||||
catch (IOException ioex)
|
||||
{
|
||||
Logger.LogDebug(ioex, "File busy or locked: {file}", cachePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed adding {file}", cachePath);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogDebug("Scan complete");
|
||||
|
||||
@@ -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,14 +16,19 @@ 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 SemaphoreSlim _fileWriteSemaphore = new(1, 1);
|
||||
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;
|
||||
private bool _csvHeaderEnsured;
|
||||
public string CacheFolder => _configService.Current.CacheFolder;
|
||||
|
||||
public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, LightlessConfigService configService, LightlessMediator lightlessMediator)
|
||||
@@ -37,15 +42,112 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
private string CsvBakPath => _csvPath + ".bak";
|
||||
|
||||
private static string NormalizeSeparators(string path) => 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) && cacheMatch > chosenLength)
|
||||
{
|
||||
chosenPrefixed = cachePrefixed;
|
||||
}
|
||||
|
||||
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;
|
||||
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
||||
}
|
||||
|
||||
public FileCacheEntity? CreateFileEntry(string path)
|
||||
@@ -53,69 +155,141 @@ 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))];
|
||||
|
||||
public List<FileCacheEntity> GetAllFileCachesByHash(string hash, bool ignoreCacheEntries = false, bool validate = true)
|
||||
{
|
||||
List<FileCacheEntity> output = [];
|
||||
if (_fileCaches.TryGetValue(hash, out var fileCacheEntities))
|
||||
var output = new List<FileCacheEntity>();
|
||||
|
||||
if (!_fileCaches.TryGetValue(hash, out var fileCacheEntities))
|
||||
return output;
|
||||
|
||||
foreach (var fileCache in fileCacheEntities.Values
|
||||
.Where(c => !ignoreCacheEntries || !c.IsCacheEntry))
|
||||
{
|
||||
foreach (var fileCache in fileCacheEntities.Where(c => ignoreCacheEntries ? !c.IsCacheEntry : true).ToList())
|
||||
if (!validate)
|
||||
{
|
||||
if (!validate) output.Add(fileCache);
|
||||
else
|
||||
{
|
||||
var validated = GetValidatedFileCache(fileCache);
|
||||
if (validated != null) output.Add(validated);
|
||||
}
|
||||
output.Add(fileCache);
|
||||
continue;
|
||||
}
|
||||
|
||||
var validated = GetValidatedFileCache(fileCache);
|
||||
if (validated != null)
|
||||
output.Add(validated);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public async Task<List<FileCacheEntity>> GetAllFileCachesByHashAsync(string hash, bool ignoreCacheEntries = false, bool validate = true,CancellationToken token = default)
|
||||
{
|
||||
var output = new List<FileCacheEntity>();
|
||||
|
||||
if (!_fileCaches.TryGetValue(hash, out var fileCacheEntities))
|
||||
return output;
|
||||
|
||||
foreach (var fileCache in fileCacheEntities.Values.Where(c => !ignoreCacheEntries || !c.IsCacheEntry))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (!validate)
|
||||
{
|
||||
output.Add(fileCache);
|
||||
}
|
||||
else
|
||||
{
|
||||
var validated = await GetValidatedFileCacheAsync(fileCache, token).ConfigureAwait(false);
|
||||
|
||||
if (validated != null)
|
||||
output.Add(validated);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public Task<List<FileCacheEntity>> ValidateLocalIntegrity(IProgress<(int, int, FileCacheEntity)> progress, CancellationToken cancellationToken)
|
||||
public async Task<List<FileCacheEntity>> ValidateLocalIntegrity(IProgress<(int completed, int total, FileCacheEntity current)> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
_lightlessMediator.Publish(new HaltScanMessage(nameof(ValidateLocalIntegrity)));
|
||||
_logger.LogInformation("Validating local storage");
|
||||
var cacheEntries = _fileCaches.SelectMany(v => v.Value).Where(v => v.IsCacheEntry).ToList();
|
||||
List<FileCacheEntity> brokenEntities = [];
|
||||
int i = 0;
|
||||
foreach (var fileCache in cacheEntries)
|
||||
|
||||
var cacheEntries = _fileCaches.Values
|
||||
.SelectMany(v => v.Values)
|
||||
.Where(v => v.IsCacheEntry)
|
||||
.ToList();
|
||||
|
||||
int total = cacheEntries.Count;
|
||||
int processed = 0;
|
||||
var brokenEntities = new ConcurrentBag<FileCacheEntity>();
|
||||
|
||||
_logger.LogInformation("Checking {count} cache entries...", total);
|
||||
|
||||
await Parallel.ForEachAsync(cacheEntries, new ParallelOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = Environment.ProcessorCount,
|
||||
CancellationToken = cancellationToken
|
||||
},
|
||||
async (fileCache, token) =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested) break;
|
||||
|
||||
_logger.LogInformation("Validating {file}", fileCache.ResolvedFilepath);
|
||||
|
||||
progress.Report((i, cacheEntries.Count, fileCache));
|
||||
i++;
|
||||
if (!File.Exists(fileCache.ResolvedFilepath))
|
||||
{
|
||||
brokenEntities.Add(fileCache);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var computedHash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
|
||||
int current = Interlocked.Increment(ref processed);
|
||||
if (current % 10 == 0)
|
||||
progress.Report((current, total, fileCache));
|
||||
|
||||
if (!File.Exists(fileCache.ResolvedFilepath))
|
||||
{
|
||||
brokenEntities.Add(fileCache);
|
||||
return;
|
||||
}
|
||||
|
||||
var algo = Crypto.DetectAlgo(fileCache.Hash);
|
||||
string computedHash;
|
||||
try
|
||||
{
|
||||
computedHash = await Crypto.ComputeFileHashAsync(fileCache.ResolvedFilepath, Crypto.HashAlgo.Sha1, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error hashing {file}", fileCache.ResolvedFilepath);
|
||||
brokenEntities.Add(fileCache);
|
||||
return;
|
||||
}
|
||||
|
||||
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(
|
||||
"Hash mismatch: {file} (got {computedHash}, expected {expected} : hash {hash})",
|
||||
fileCache.ResolvedFilepath, computedHash, fileCache.Hash, algo);
|
||||
|
||||
brokenEntities.Add(fileCache);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning(e, "Error during validation of {file}", fileCache.ResolvedFilepath);
|
||||
_logger.LogError("Validation got cancelled for {file}", fileCache.ResolvedFilepath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unexpected error validating {file}", fileCache.ResolvedFilepath);
|
||||
brokenEntities.Add(fileCache);
|
||||
}
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
foreach (var brokenEntity in brokenEntities)
|
||||
{
|
||||
@@ -127,12 +301,14 @@ public sealed class FileCacheManager : IHostedService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not delete {file}", brokenEntity.ResolvedFilepath);
|
||||
_logger.LogWarning(ex, "Failed to delete invalid cache file {file}", brokenEntity.ResolvedFilepath);
|
||||
}
|
||||
}
|
||||
|
||||
_lightlessMediator.Publish(new ResumeScanMessage(nameof(ValidateLocalIntegrity)));
|
||||
return Task.FromResult(brokenEntities);
|
||||
_logger.LogInformation("Validation complete. Found {count} invalid entries.", brokenEntities.Count);
|
||||
|
||||
return [.. brokenEntities];
|
||||
}
|
||||
|
||||
public string GetCacheFilePath(string hash, string extension)
|
||||
@@ -149,29 +325,40 @@ 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)
|
||||
@@ -180,34 +367,52 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
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;
|
||||
@@ -220,16 +425,24 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
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)
|
||||
@@ -242,7 +455,7 @@ public sealed class FileCacheManager : IHostedService
|
||||
var fi = new FileInfo(fileCache.ResolvedFilepath);
|
||||
fileCache.Size = fi.Length;
|
||||
fileCache.CompressedSize = null;
|
||||
fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
|
||||
fileCache.Hash = Crypto.ComputeFileHash(fileCache.ResolvedFilepath, Crypto.HashAlgo.Sha1);
|
||||
fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
RemoveHashedFile(oldHash, prefixedPath);
|
||||
@@ -270,7 +483,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 +506,104 @@ public sealed class FileCacheManager : IHostedService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WriteOutFullCsvAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _fileWriteSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(BuildVersionHeader());
|
||||
|
||||
foreach (var entry in _fileCaches.Values
|
||||
.SelectMany(k => k.Values)
|
||||
.OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
sb.AppendLine(entry.CsvEntry);
|
||||
}
|
||||
|
||||
if (File.Exists(_csvPath))
|
||||
{
|
||||
File.Copy(_csvPath, CsvBakPath, overwrite: true);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(_csvPath, sb.ToString(), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
File.Delete(CsvBakPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await File.WriteAllTextAsync(CsvBakPath, sb.ToString(), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fileWriteSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
_csvHeaderEnsured = true;
|
||||
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());
|
||||
_csvHeaderEnsured = true;
|
||||
}
|
||||
|
||||
private void EnsureCsvHeaderLockedCached()
|
||||
{
|
||||
if (_csvHeaderEnsured)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureCsvHeaderLocked();
|
||||
_csvHeaderEnsured = true;
|
||||
}
|
||||
|
||||
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,27 +627,31 @@ 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)
|
||||
{
|
||||
hash ??= Crypto.GetFileHash(fileInfo.FullName);
|
||||
hash ??= Crypto.ComputeFileHash(fileInfo.FullName, Crypto.HashAlgo.Sha1);
|
||||
var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileInfo.Length);
|
||||
entity = ReplacePathPrefixes(entity);
|
||||
AddHashedFile(entity);
|
||||
lock (_fileWriteLock)
|
||||
{
|
||||
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
||||
if (!File.Exists(_csvPath))
|
||||
{
|
||||
File.WriteAllLines(_csvPath, [BuildVersionHeader(), entity.CsvEntry]);
|
||||
_csvHeaderEnsured = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureCsvHeaderLockedCached();
|
||||
File.AppendAllLines(_csvPath, [entity.CsvEntry]);
|
||||
}
|
||||
}
|
||||
var result = GetFileCacheByPath(fileInfo.FullName);
|
||||
_logger.LogTrace("Creating cache entity for {name} success: {success}", fileInfo.FullName, (result != null));
|
||||
@@ -345,11 +661,17 @@ public sealed class FileCacheManager : IHostedService
|
||||
private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache)
|
||||
{
|
||||
var resultingFileCache = ReplacePathPrefixes(fileCache);
|
||||
//_logger.LogTrace("Validating {path}", fileCache.PrefixedFilePath);
|
||||
resultingFileCache = Validate(resultingFileCache);
|
||||
return resultingFileCache;
|
||||
}
|
||||
|
||||
private async Task<FileCacheEntity?> GetValidatedFileCacheAsync(FileCacheEntity fileCache, CancellationToken token = default)
|
||||
{
|
||||
var resultingFileCache = ReplacePathPrefixes(fileCache);
|
||||
resultingFileCache = await ValidateAsync(resultingFileCache, token).ConfigureAwait(false);
|
||||
return resultingFileCache;
|
||||
}
|
||||
|
||||
private FileCacheEntity ReplacePathPrefixes(FileCacheEntity fileCache)
|
||||
{
|
||||
if (fileCache.PrefixedFilePath.StartsWith(PenumbraPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -366,6 +688,13 @@ 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)
|
||||
{
|
||||
@@ -373,7 +702,8 @@ public sealed class FileCacheManager : IHostedService
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
|
||||
var lastWriteTicks = file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
|
||||
if (!string.Equals(lastWriteTicks, fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
|
||||
{
|
||||
UpdateHashedFile(fileCache);
|
||||
}
|
||||
@@ -381,7 +711,34 @@ public sealed class FileCacheManager : IHostedService
|
||||
return fileCache;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
private async Task<FileCacheEntity?> ValidateAsync(FileCacheEntity fileCache, CancellationToken token)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
var file = new FileInfo(fileCache.ResolvedFilepath);
|
||||
if (!file.Exists)
|
||||
{
|
||||
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
|
||||
{
|
||||
UpdateHashedFile(fileCache);
|
||||
}
|
||||
|
||||
return fileCache;
|
||||
}, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Starting FileCacheManager");
|
||||
|
||||
@@ -432,14 +789,14 @@ public sealed class FileCacheManager : IHostedService
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Attempting to read {csvPath}", _csvPath);
|
||||
entries = File.ReadAllLines(_csvPath);
|
||||
entries = await File.ReadAllLinesAsync(_csvPath, cancellationToken).ConfigureAwait(false);
|
||||
success = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
attempts++;
|
||||
_logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath);
|
||||
Thread.Sleep(100);
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,62 +805,124 @@ 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 = [];
|
||||
}
|
||||
|
||||
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 = [];
|
||||
}
|
||||
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)
|
||||
{
|
||||
WriteOutFullCsv();
|
||||
_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)
|
||||
{
|
||||
await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Started FileCacheManager");
|
||||
|
||||
return Task.CompletedTask;
|
||||
_lightlessMediator.Publish(new FileCacheInitializedMessage());
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
WriteOutFullCsv();
|
||||
return Task.CompletedTask;
|
||||
await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false);
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,4 +5,5 @@ public enum FileState
|
||||
Valid,
|
||||
RequireUpdate,
|
||||
RequireDeletion,
|
||||
RequireRehash
|
||||
}
|
||||
@@ -3,11 +3,17 @@ using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
using LightlessSync.PlayerData.Data;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
|
||||
namespace LightlessSync.FileCache;
|
||||
|
||||
@@ -17,21 +23,29 @@ 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 ActorObjectService _actorObjectService;
|
||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||
private readonly object _ownedHandlerLock = new();
|
||||
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 = [];
|
||||
private readonly Dictionary<nint, GameObjectHandler> _ownedHandlers = new();
|
||||
private ConcurrentDictionary<nint, ObjectKind> _cachedFrameAddresses = new();
|
||||
private ConcurrentDictionary<ObjectKind, HashSet<string>>? _semiTransientResources = null;
|
||||
private uint _lastClassJobId = uint.MaxValue;
|
||||
public bool IsTransientRecording { get; private set; } = false;
|
||||
|
||||
public TransientResourceManager(ILogger<TransientResourceManager> logger, TransientConfigService configurationService,
|
||||
DalamudUtilService dalamudUtil, LightlessMediator mediator) : base(logger, mediator)
|
||||
DalamudUtilService dalamudUtil, LightlessMediator mediator, ActorObjectService actorObjectService, GameObjectHandlerFactory gameObjectHandlerFactory) : base(logger, mediator)
|
||||
{
|
||||
_configurationService = configurationService;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_actorObjectService = actorObjectService;
|
||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||
|
||||
Mediator.Subscribe<PenumbraResourceLoadMessage>(this, Manager_PenumbraResourceLoadEvent);
|
||||
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
|
||||
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => HandleActorUntracked(msg.Descriptor));
|
||||
Mediator.Subscribe<PenumbraModSettingChangedMessage>(this, (_) => Manager_PenumbraModSettingChanged());
|
||||
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, (_) => DalamudUtil_FrameworkUpdate());
|
||||
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
||||
@@ -44,6 +58,11 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
if (!msg.OwnedObject) return;
|
||||
_playerRelatedPointers.Remove(msg.GameObjectHandler);
|
||||
});
|
||||
|
||||
foreach (var descriptor in _actorObjectService.ObjectDescriptors)
|
||||
{
|
||||
HandleActorTracked(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
private TransientConfig.TransientPlayerConfig PlayerConfig
|
||||
@@ -123,12 +142,21 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
return;
|
||||
}
|
||||
|
||||
var transientResources = resources.ToList();
|
||||
Logger.LogDebug("Persisting {count} transient resources", transientResources.Count);
|
||||
List<string> newlyAddedGamePaths = resources.Except(semiTransientResources, StringComparer.Ordinal).ToList();
|
||||
foreach (var gamePath in transientResources)
|
||||
List<string> transientResources;
|
||||
lock (resources)
|
||||
{
|
||||
semiTransientResources.Add(gamePath);
|
||||
transientResources = resources.ToList();
|
||||
}
|
||||
|
||||
Logger.LogDebug("Persisting {count} transient resources", transientResources.Count);
|
||||
List<string> newlyAddedGamePaths;
|
||||
lock (semiTransientResources)
|
||||
{
|
||||
newlyAddedGamePaths = transientResources.Except(semiTransientResources, StringComparer.Ordinal).ToList();
|
||||
foreach (var gamePath in transientResources)
|
||||
{
|
||||
semiTransientResources.Add(gamePath);
|
||||
}
|
||||
}
|
||||
|
||||
bool saveConfig = false;
|
||||
@@ -161,7 +189,10 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
_configurationService.Save();
|
||||
}
|
||||
|
||||
TransientResources[objectKind].Clear();
|
||||
lock (resources)
|
||||
{
|
||||
resources.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveTransientResource(ObjectKind objectKind, string path)
|
||||
@@ -241,16 +272,46 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
|
||||
TransientResources.Clear();
|
||||
SemiTransientResources.Clear();
|
||||
|
||||
lock (_ownedHandlerLock)
|
||||
{
|
||||
foreach (var handler in _ownedHandlers.Values)
|
||||
{
|
||||
handler.Dispose();
|
||||
}
|
||||
_ownedHandlers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void DalamudUtil_FrameworkUpdate()
|
||||
{
|
||||
_cachedFrameAddresses = new(_playerRelatedPointers.Where(k => k.Address != nint.Zero).ToDictionary(c => c.Address, c => c.ObjectKind));
|
||||
lock (_cacheAdditionLock)
|
||||
{
|
||||
_cachedHandledPaths.Clear();
|
||||
}
|
||||
|
||||
var activeDescriptors = new Dictionary<nint, ObjectKind>();
|
||||
foreach (var descriptor in _actorObjectService.ObjectDescriptors)
|
||||
{
|
||||
if (TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||
{
|
||||
activeDescriptors[descriptor.Address] = resolvedKind;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var address in _cachedFrameAddresses.Keys.ToList())
|
||||
{
|
||||
if (!activeDescriptors.ContainsKey(address))
|
||||
{
|
||||
_cachedFrameAddresses.TryRemove(address, out _);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var descriptor in activeDescriptors)
|
||||
{
|
||||
_cachedFrameAddresses[descriptor.Key] = descriptor.Value;
|
||||
}
|
||||
|
||||
if (_lastClassJobId != _dalamudUtil.ClassJobId)
|
||||
{
|
||||
_lastClassJobId = _dalamudUtil.ClassJobId;
|
||||
@@ -259,16 +320,15 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
value?.Clear();
|
||||
}
|
||||
|
||||
// reload config for current new classjob
|
||||
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
||||
SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
|
||||
SemiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []];
|
||||
}
|
||||
|
||||
foreach (var kind in Enum.GetValues(typeof(ObjectKind)))
|
||||
foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast<ObjectKind>())
|
||||
{
|
||||
if (!_cachedFrameAddresses.Any(k => k.Value == (ObjectKind)kind) && TransientResources.Remove((ObjectKind)kind, out _))
|
||||
if (!_cachedFrameAddresses.Any(k => k.Value == kind) && TransientResources.Remove(kind, out _))
|
||||
{
|
||||
Logger.LogDebug("Object not present anymore: {kind}", kind.ToString());
|
||||
}
|
||||
@@ -292,6 +352,119 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
_semiTransientResources = null;
|
||||
}
|
||||
|
||||
private static bool TryResolveObjectKind(ActorObjectService.ActorDescriptor descriptor, out ObjectKind resolvedKind)
|
||||
{
|
||||
if (descriptor.OwnedKind is ObjectKind ownedKind)
|
||||
{
|
||||
resolvedKind = ownedKind;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||
{
|
||||
resolvedKind = ObjectKind.Player;
|
||||
return true;
|
||||
}
|
||||
|
||||
resolvedKind = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.IsInGpose)
|
||||
return;
|
||||
|
||||
if (!TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||
return;
|
||||
|
||||
if (Logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
Logger.LogDebug("ActorObject tracked: {kind} addr={address:X} name={name}", resolvedKind, descriptor.Address, descriptor.Name);
|
||||
}
|
||||
|
||||
_cachedFrameAddresses[descriptor.Address] = resolvedKind;
|
||||
|
||||
if (descriptor.OwnedKind is not ObjectKind ownedKind)
|
||||
return;
|
||||
|
||||
lock (_ownedHandlerLock)
|
||||
{
|
||||
if (_ownedHandlers.ContainsKey(descriptor.Address))
|
||||
return;
|
||||
|
||||
_ = CreateOwnedHandlerAsync(descriptor, ownedKind);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleActorUntracked(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
if (Logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
var kindLabel = descriptor.OwnedKind?.ToString()
|
||||
?? (descriptor.ObjectKind == DalamudObjectKind.Player ? ObjectKind.Player.ToString() : "<none>");
|
||||
Logger.LogDebug("ActorObject untracked: addr={address:X} name={name} kind={kind}", descriptor.Address, descriptor.Name, kindLabel);
|
||||
}
|
||||
|
||||
_cachedFrameAddresses.TryRemove(descriptor.Address, out _);
|
||||
|
||||
if (descriptor.OwnedKind is not ObjectKind)
|
||||
return;
|
||||
|
||||
lock (_ownedHandlerLock)
|
||||
{
|
||||
if (_ownedHandlers.Remove(descriptor.Address, out var handler))
|
||||
{
|
||||
handler.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateOwnedHandlerAsync(ActorObjectService.ActorDescriptor descriptor, ObjectKind kind)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handler = await _gameObjectHandlerFactory.Create(
|
||||
kind,
|
||||
() =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(descriptor.HashedContentId) &&
|
||||
_actorObjectService.TryGetValidatedActorByHash(descriptor.HashedContentId, out var current) &&
|
||||
current.OwnedKind == kind)
|
||||
{
|
||||
return current.Address;
|
||||
}
|
||||
|
||||
return descriptor.Address;
|
||||
},
|
||||
true).ConfigureAwait(false);
|
||||
|
||||
if (handler.Address == IntPtr.Zero)
|
||||
{
|
||||
handler.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_ownedHandlerLock)
|
||||
{
|
||||
if (!_cachedFrameAddresses.ContainsKey(descriptor.Address))
|
||||
{
|
||||
Logger.LogDebug("ActorObject handler discarded (stale): addr={address:X}", descriptor.Address);
|
||||
handler.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
_ownedHandlers[descriptor.Address] = handler;
|
||||
}
|
||||
|
||||
Logger.LogDebug("ActorObject handler created: {kind} addr={address:X}", kind, descriptor.Address);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to create owned handler for {kind} at {address:X}", kind, descriptor.Address);
|
||||
}
|
||||
}
|
||||
|
||||
private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
|
||||
{
|
||||
var gamePath = msg.GamePath.ToLowerInvariant();
|
||||
@@ -383,21 +556,30 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
|
||||
private void SendTransients(nint gameObject, ObjectKind objectKind)
|
||||
{
|
||||
_sendTransientCts.Cancel();
|
||||
_sendTransientCts = new();
|
||||
var token = _sendTransientCts.Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
_sendTransientCts?.Cancel();
|
||||
_sendTransientCts?.Dispose();
|
||||
_sendTransientCts = new();
|
||||
var token = _sendTransientCts.Token;
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), token).ConfigureAwait(false);
|
||||
foreach (var kvp in TransientResources)
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), token).ConfigureAwait(false);
|
||||
|
||||
if (TransientResources.TryGetValue(objectKind, out var values) && values.Any())
|
||||
{
|
||||
Logger.LogTrace("Sending Transients for {kind}", objectKind);
|
||||
Mediator.Publish(new TransientResourceChangedMessage(gameObject));
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
catch (System.OperationCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ internal sealed class DalamudLogger : ILogger
|
||||
_hasModifiedGameFiles = hasModifiedGameFiles;
|
||||
}
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state) => default!;
|
||||
IDisposable? ILogger.BeginScope<TState>(TState state)
|
||||
{
|
||||
return default!;
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
|
||||
196
LightlessSync/Interop/Ipc/Framework/IpcFramework.cs
Normal file
196
LightlessSync/Interop/Ipc/Framework/IpcFramework.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Linq;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc.Framework;
|
||||
|
||||
public enum IpcConnectionState
|
||||
{
|
||||
Unknown = 0,
|
||||
MissingPlugin = 1,
|
||||
VersionMismatch = 2,
|
||||
PluginDisabled = 3,
|
||||
NotReady = 4,
|
||||
Available = 5,
|
||||
Error = 6,
|
||||
}
|
||||
|
||||
public sealed record IpcServiceDescriptor(string InternalName, string DisplayName, Version MinimumVersion)
|
||||
{
|
||||
public override string ToString()
|
||||
=> $"{DisplayName} (>= {MinimumVersion})";
|
||||
}
|
||||
|
||||
public interface IIpcService : IDisposable
|
||||
{
|
||||
IpcServiceDescriptor Descriptor { get; }
|
||||
IpcConnectionState State { get; }
|
||||
IDalamudPluginInterface PluginInterface { get; }
|
||||
bool APIAvailable { get; }
|
||||
void CheckAPI();
|
||||
}
|
||||
|
||||
public interface IIpcInterop : IDisposable
|
||||
{
|
||||
string Name { get; }
|
||||
void OnConnectionStateChanged(IpcConnectionState state);
|
||||
}
|
||||
|
||||
public abstract class IpcInteropBase : IIpcInterop
|
||||
{
|
||||
protected IpcInteropBase(ILogger logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
protected IpcConnectionState State { get; private set; } = IpcConnectionState.Unknown;
|
||||
|
||||
protected bool IsAvailable => State == IpcConnectionState.Available;
|
||||
|
||||
public abstract string Name { get; }
|
||||
|
||||
public void OnConnectionStateChanged(IpcConnectionState state)
|
||||
{
|
||||
if (State == state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var previous = State;
|
||||
State = state;
|
||||
HandleStateChange(previous, state);
|
||||
}
|
||||
|
||||
protected abstract void HandleStateChange(IpcConnectionState previous, IpcConnectionState current);
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class IpcServiceBase : DisposableMediatorSubscriberBase, IIpcService
|
||||
{
|
||||
private readonly List<IIpcInterop> _interops = new();
|
||||
|
||||
protected IpcServiceBase(
|
||||
ILogger logger,
|
||||
LightlessMediator mediator,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
IpcServiceDescriptor descriptor) : base(logger, mediator)
|
||||
{
|
||||
PluginInterface = pluginInterface;
|
||||
Descriptor = descriptor;
|
||||
}
|
||||
|
||||
protected IDalamudPluginInterface PluginInterface { get; }
|
||||
|
||||
IDalamudPluginInterface IIpcService.PluginInterface => PluginInterface;
|
||||
|
||||
protected IpcServiceDescriptor Descriptor { get; }
|
||||
|
||||
IpcServiceDescriptor IIpcService.Descriptor => Descriptor;
|
||||
|
||||
public IpcConnectionState State { get; private set; } = IpcConnectionState.Unknown;
|
||||
|
||||
public bool APIAvailable => State == IpcConnectionState.Available;
|
||||
|
||||
public virtual void CheckAPI()
|
||||
{
|
||||
var newState = EvaluateState();
|
||||
UpdateState(newState);
|
||||
}
|
||||
|
||||
protected virtual IpcConnectionState EvaluateState()
|
||||
{
|
||||
try
|
||||
{
|
||||
var plugin = PluginInterface.InstalledPlugins
|
||||
.Where(p => string.Equals(p.InternalName, Descriptor.InternalName, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(p => p.IsLoaded)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (plugin == null)
|
||||
{
|
||||
return IpcConnectionState.MissingPlugin;
|
||||
}
|
||||
|
||||
if (plugin.Version < Descriptor.MinimumVersion)
|
||||
{
|
||||
return IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
|
||||
if (!IsPluginEnabled(plugin))
|
||||
{
|
||||
return IpcConnectionState.PluginDisabled;
|
||||
}
|
||||
|
||||
if (!IsPluginReady())
|
||||
{
|
||||
return IpcConnectionState.NotReady;
|
||||
}
|
||||
|
||||
return IpcConnectionState.Available;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Failed to evaluate IPC state for {Service}", Descriptor.DisplayName);
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool IsPluginEnabled(IExposedPlugin plugin)
|
||||
=> plugin.IsLoaded;
|
||||
|
||||
protected virtual bool IsPluginReady()
|
||||
=> true;
|
||||
|
||||
protected TInterop RegisterInterop<TInterop>(TInterop interop)
|
||||
where TInterop : IIpcInterop
|
||||
{
|
||||
_interops.Add(interop);
|
||||
interop.OnConnectionStateChanged(State);
|
||||
return interop;
|
||||
}
|
||||
|
||||
private void UpdateState(IpcConnectionState newState)
|
||||
{
|
||||
if (State == newState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var previous = State;
|
||||
State = newState;
|
||||
OnConnectionStateChanged(previous, newState);
|
||||
|
||||
foreach (var interop in _interops)
|
||||
{
|
||||
interop.OnConnectionStateChanged(newState);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
Logger.LogTrace("{Service} IPC state transitioned from {Previous} to {Current}", Descriptor.DisplayName, previous, current);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = _interops.Count - 1; i >= 0; --i)
|
||||
{
|
||||
_interops[i].Dispose();
|
||||
}
|
||||
|
||||
_interops.Clear();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public interface IIpcCaller : IDisposable
|
||||
{
|
||||
bool APIAvailable { get; }
|
||||
void CheckAPI();
|
||||
}
|
||||
@@ -1,69 +1,63 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Brio.API;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerBrio : IIpcCaller
|
||||
public sealed class IpcCallerBrio : IpcServiceBase
|
||||
{
|
||||
private static readonly IpcServiceDescriptor BrioDescriptor = new("Brio", "Brio", new Version(0, 0, 0, 0));
|
||||
|
||||
private readonly ILogger<IpcCallerBrio> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly ICallGateSubscriber<(int, int)> _brioApiVersion;
|
||||
|
||||
private readonly ICallGateSubscriber<bool, bool, bool, Task<IGameObject>> _brioSpawnActorAsync;
|
||||
private readonly ICallGateSubscriber<IGameObject, bool> _brioDespawnActor;
|
||||
private readonly ICallGateSubscriber<IGameObject, Vector3?, Quaternion?, Vector3?, bool, bool> _brioSetModelTransform;
|
||||
private readonly ICallGateSubscriber<IGameObject, (Vector3?, Quaternion?, Vector3?)> _brioGetModelTransform;
|
||||
private readonly ICallGateSubscriber<IGameObject, string> _brioGetPoseAsJson;
|
||||
private readonly ICallGateSubscriber<IGameObject, string, bool, bool> _brioSetPoseFromJson;
|
||||
private readonly ICallGateSubscriber<IGameObject, bool> _brioFreezeActor;
|
||||
private readonly ICallGateSubscriber<bool> _brioFreezePhysics;
|
||||
private readonly ApiVersion _apiVersion;
|
||||
|
||||
private readonly SpawnActor _spawnActor;
|
||||
private readonly DespawnActor _despawnActor;
|
||||
private readonly SetModelTransform _setModelTransform;
|
||||
private readonly GetModelTransform _getModelTransform;
|
||||
|
||||
public bool APIAvailable { get; private set; }
|
||||
private readonly GetPoseAsJson _getPoseAsJson;
|
||||
private readonly LoadPoseFromJson _setPoseFromJson;
|
||||
|
||||
private readonly FreezeActor _freezeActor;
|
||||
private readonly FreezePhysics _freezePhysics;
|
||||
|
||||
public IpcCallerBrio(ILogger<IpcCallerBrio> logger, IDalamudPluginInterface dalamudPluginInterface,
|
||||
DalamudUtilService dalamudUtilService)
|
||||
DalamudUtilService dalamudUtilService, LightlessMediator mediator) : base(logger, mediator, dalamudPluginInterface, BrioDescriptor)
|
||||
{
|
||||
_logger = logger;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
|
||||
_brioApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("Brio.ApiVersion");
|
||||
_brioSpawnActorAsync = dalamudPluginInterface.GetIpcSubscriber<bool, bool, bool, Task<IGameObject>>("Brio.Actor.SpawnExAsync");
|
||||
_brioDespawnActor = dalamudPluginInterface.GetIpcSubscriber<IGameObject, bool>("Brio.Actor.Despawn");
|
||||
_brioSetModelTransform = dalamudPluginInterface.GetIpcSubscriber<IGameObject, Vector3?, Quaternion?, Vector3?, bool, bool>("Brio.Actor.SetModelTransform");
|
||||
_brioGetModelTransform = dalamudPluginInterface.GetIpcSubscriber<IGameObject, (Vector3?, Quaternion?, Vector3?)>("Brio.Actor.GetModelTransform");
|
||||
_brioGetPoseAsJson = dalamudPluginInterface.GetIpcSubscriber<IGameObject, string>("Brio.Actor.Pose.GetPoseAsJson");
|
||||
_brioSetPoseFromJson = dalamudPluginInterface.GetIpcSubscriber<IGameObject, string, bool, bool>("Brio.Actor.Pose.LoadFromJson");
|
||||
_brioFreezeActor = dalamudPluginInterface.GetIpcSubscriber<IGameObject, bool>("Brio.Actor.Freeze");
|
||||
_brioFreezePhysics = dalamudPluginInterface.GetIpcSubscriber<bool>("Brio.FreezePhysics");
|
||||
_apiVersion = new ApiVersion(dalamudPluginInterface);
|
||||
_spawnActor = new SpawnActor(dalamudPluginInterface);
|
||||
_despawnActor = new DespawnActor(dalamudPluginInterface);
|
||||
|
||||
_setModelTransform = new SetModelTransform(dalamudPluginInterface);
|
||||
_getModelTransform = new GetModelTransform(dalamudPluginInterface);
|
||||
|
||||
_getPoseAsJson = new GetPoseAsJson(dalamudPluginInterface);
|
||||
_setPoseFromJson = new LoadPoseFromJson(dalamudPluginInterface);
|
||||
|
||||
_freezeActor = new FreezeActor(dalamudPluginInterface);
|
||||
_freezePhysics = new FreezePhysics(dalamudPluginInterface);
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
try
|
||||
{
|
||||
var version = _brioApiVersion.InvokeFunc();
|
||||
APIAvailable = (version.Item1 == 2 && version.Item2 >= 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IGameObject?> SpawnActorAsync()
|
||||
{
|
||||
if (!APIAvailable) return null;
|
||||
_logger.LogDebug("Spawning Brio Actor");
|
||||
return await _brioSpawnActorAsync.InvokeFunc(false, false, true).ConfigureAwait(false);
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _spawnActor.Invoke(Brio.API.Enums.SpawnFlags.Default, true)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> DespawnActorAsync(nint address)
|
||||
@@ -72,7 +66,7 @@ public sealed class IpcCallerBrio : IIpcCaller
|
||||
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
|
||||
if (gameObject == null) return false;
|
||||
_logger.LogDebug("Despawning Brio Actor {actor}", gameObject.Name.TextValue);
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioDespawnActor.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _despawnActor.Invoke(gameObject)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> ApplyTransformAsync(nint address, WorldData data)
|
||||
@@ -82,7 +76,7 @@ public sealed class IpcCallerBrio : IIpcCaller
|
||||
if (gameObject == null) return false;
|
||||
_logger.LogDebug("Applying Transform to Actor {actor}", gameObject.Name.TextValue);
|
||||
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetModelTransform.InvokeFunc(gameObject,
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _setModelTransform.Invoke(gameObject,
|
||||
new Vector3(data.PositionX, data.PositionY, data.PositionZ),
|
||||
new Quaternion(data.RotationX, data.RotationY, data.RotationZ, data.RotationW),
|
||||
new Vector3(data.ScaleX, data.ScaleY, data.ScaleZ), false)).ConfigureAwait(false);
|
||||
@@ -93,8 +87,7 @@ public sealed class IpcCallerBrio : IIpcCaller
|
||||
if (!APIAvailable) return default;
|
||||
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
|
||||
if (gameObject == null) return default;
|
||||
var data = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetModelTransform.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||
//_logger.LogDebug("Getting Transform from Actor {actor}", gameObject.Name.TextValue);
|
||||
var data = await _dalamudUtilService.RunOnFrameworkThread(() => _getModelTransform.Invoke(gameObject)).ConfigureAwait(false);
|
||||
|
||||
return new WorldData()
|
||||
{
|
||||
@@ -118,7 +111,7 @@ public sealed class IpcCallerBrio : IIpcCaller
|
||||
if (gameObject == null) return null;
|
||||
_logger.LogDebug("Getting Pose from Actor {actor}", gameObject.Name.TextValue);
|
||||
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetPoseAsJson.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _getPoseAsJson.Invoke(gameObject)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> SetPoseAsync(nint address, string pose)
|
||||
@@ -129,18 +122,41 @@ public sealed class IpcCallerBrio : IIpcCaller
|
||||
_logger.LogDebug("Setting Pose to Actor {actor}", gameObject.Name.TextValue);
|
||||
|
||||
var applicablePose = JsonNode.Parse(pose)!;
|
||||
var currentPose = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetPoseAsJson.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||
var currentPose = await _dalamudUtilService.RunOnFrameworkThread(() => _getPoseAsJson.Invoke(gameObject)).ConfigureAwait(false);
|
||||
applicablePose["ModelDifference"] = JsonNode.Parse(JsonNode.Parse(currentPose)!["ModelDifference"]!.ToJsonString());
|
||||
|
||||
await _dalamudUtilService.RunOnFrameworkThread(() =>
|
||||
{
|
||||
_brioFreezeActor.InvokeFunc(gameObject);
|
||||
_brioFreezePhysics.InvokeFunc();
|
||||
_freezeActor.Invoke(gameObject);
|
||||
_freezePhysics.Invoke();
|
||||
}).ConfigureAwait(false);
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetPoseFromJson.InvokeFunc(gameObject, applicablePose.ToJsonString(), false)).ConfigureAwait(false);
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _setPoseFromJson.Invoke(gameObject, applicablePose.ToJsonString(), false)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
protected override IpcConnectionState EvaluateState()
|
||||
{
|
||||
var state = base.EvaluateState();
|
||||
if (state != IpcConnectionState.Available)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var version = _apiVersion.Invoke();
|
||||
return version.Breaking == 3 && version.Feature >= 0
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to query Brio IPC version");
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using Dalamud.Utility;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -9,8 +10,10 @@ using System.Text;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerCustomize : IIpcCaller
|
||||
public sealed class IpcCallerCustomize : IpcServiceBase
|
||||
{
|
||||
private static readonly IpcServiceDescriptor CustomizeDescriptor = new("CustomizePlus", "Customize+", new Version(0, 0, 0, 0));
|
||||
|
||||
private readonly ICallGateSubscriber<(int, int)> _customizePlusApiVersion;
|
||||
private readonly ICallGateSubscriber<ushort, (int, Guid?)> _customizePlusGetActiveProfile;
|
||||
private readonly ICallGateSubscriber<Guid, (int, string?)> _customizePlusGetProfileById;
|
||||
@@ -23,7 +26,7 @@ public sealed class IpcCallerCustomize : IIpcCaller
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
|
||||
public IpcCallerCustomize(ILogger<IpcCallerCustomize> logger, IDalamudPluginInterface dalamudPluginInterface,
|
||||
DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator)
|
||||
DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator) : base(logger, lightlessMediator, dalamudPluginInterface, CustomizeDescriptor)
|
||||
{
|
||||
_customizePlusApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("CustomizePlus.General.GetApiVersion");
|
||||
_customizePlusGetActiveProfile = dalamudPluginInterface.GetIpcSubscriber<ushort, (int, Guid?)>("CustomizePlus.Profile.GetActiveProfileIdOnCharacter");
|
||||
@@ -41,8 +44,6 @@ public sealed class IpcCallerCustomize : IIpcCaller
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public async Task RevertAsync(nint character)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
@@ -113,16 +114,25 @@ public sealed class IpcCallerCustomize : IIpcCaller
|
||||
return Convert.ToBase64String(Encoding.UTF8.GetBytes(scale));
|
||||
}
|
||||
|
||||
public void CheckAPI()
|
||||
protected override IpcConnectionState EvaluateState()
|
||||
{
|
||||
var state = base.EvaluateState();
|
||||
if (state != IpcConnectionState.Available)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var version = _customizePlusApiVersion.InvokeFunc();
|
||||
APIAvailable = (version.Item1 == 6 && version.Item2 >= 0);
|
||||
return version.Item1 == 6 && version.Item2 >= 0
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
APIAvailable = false;
|
||||
Logger.LogDebug(ex, "Failed to query Customize+ API version");
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,8 +142,14 @@ public sealed class IpcCallerCustomize : IIpcCaller
|
||||
_lightlessMediator.Publish(new CustomizePlusMessage(obj?.Address ?? null));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_customizePlusOnScaleUpdate.Unsubscribe(OnCustomizePlusScaleChange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Dalamud.Plugin;
|
||||
using Glamourer.Api.Helpers;
|
||||
using Glamourer.Api.IpcSubscribers;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
@@ -10,8 +11,9 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcCaller
|
||||
public sealed class IpcCallerGlamourer : IpcServiceBase
|
||||
{
|
||||
private static readonly IpcServiceDescriptor GlamourerDescriptor = new("Glamourer", "Glamourer", new Version(1, 3, 0, 10));
|
||||
private readonly ILogger<IpcCallerGlamourer> _logger;
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
@@ -31,7 +33,7 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC
|
||||
private readonly uint LockCode = 0x6D617265;
|
||||
|
||||
public IpcCallerGlamourer(ILogger<IpcCallerGlamourer> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator,
|
||||
RedrawManager redrawManager) : base(logger, lightlessMediator)
|
||||
RedrawManager redrawManager) : base(logger, lightlessMediator, pi, GlamourerDescriptor)
|
||||
{
|
||||
_glamourerApiVersions = new ApiVersion(pi);
|
||||
_glamourerGetAllCustomization = new GetStateBase64(pi);
|
||||
@@ -62,47 +64,6 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC
|
||||
_glamourerStateChanged?.Dispose();
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; }
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
bool apiAvailable = false;
|
||||
try
|
||||
{
|
||||
bool versionValid = (_pi.InstalledPlugins
|
||||
.FirstOrDefault(p => string.Equals(p.InternalName, "Glamourer", StringComparison.OrdinalIgnoreCase))
|
||||
?.Version ?? new Version(0, 0, 0, 0)) >= new Version(1, 3, 0, 10);
|
||||
try
|
||||
{
|
||||
var version = _glamourerApiVersions.Invoke();
|
||||
if (version is { Major: 1, Minor: >= 1 } && versionValid)
|
||||
{
|
||||
apiAvailable = true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
_shownGlamourerUnavailable = _shownGlamourerUnavailable && !apiAvailable;
|
||||
|
||||
APIAvailable = apiAvailable;
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = apiAvailable;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!apiAvailable && !_shownGlamourerUnavailable)
|
||||
{
|
||||
_shownGlamourerUnavailable = true;
|
||||
_lightlessMediator.Publish(new NotificationMessage("Glamourer inactive", "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Lightless. If you just updated Glamourer, ignore this message.",
|
||||
NotificationType.Error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ApplyAllAsync(ILogger logger, GameObjectHandler handler, string? customization, Guid applicationId, CancellationToken token, bool fireAndForget = false)
|
||||
{
|
||||
if (!APIAvailable || string.IsNullOrEmpty(customization) || _dalamudUtil.IsZoning) return;
|
||||
@@ -210,6 +171,49 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC
|
||||
}
|
||||
}
|
||||
|
||||
protected override IpcConnectionState EvaluateState()
|
||||
{
|
||||
var state = base.EvaluateState();
|
||||
if (state != IpcConnectionState.Available)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var version = _glamourerApiVersions.Invoke();
|
||||
return version is { Major: 1, Minor: >= 1 }
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to query Glamourer API version");
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
base.OnConnectionStateChanged(previous, current);
|
||||
|
||||
if (current == IpcConnectionState.Available)
|
||||
{
|
||||
_shownGlamourerUnavailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_shownGlamourerUnavailable || current == IpcConnectionState.Unknown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_shownGlamourerUnavailable = true;
|
||||
_lightlessMediator.Publish(new NotificationMessage("Glamourer inactive",
|
||||
"Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Lightless. If you just updated Glamourer, ignore this message.",
|
||||
NotificationType.Error));
|
||||
}
|
||||
|
||||
private void GlamourerChanged(nint address)
|
||||
{
|
||||
_lightlessMediator.Publish(new GlamourerChangedMessage(address));
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerHeels : IIpcCaller
|
||||
public sealed class IpcCallerHeels : IpcServiceBase
|
||||
{
|
||||
private static readonly IpcServiceDescriptor HeelsDescriptor = new("SimpleHeels", "Simple Heels", new Version(0, 0, 0, 0));
|
||||
|
||||
private readonly ILogger<IpcCallerHeels> _logger;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
@@ -18,6 +21,7 @@ public sealed class IpcCallerHeels : IIpcCaller
|
||||
private readonly ICallGateSubscriber<int, object?> _heelsUnregisterPlayer;
|
||||
|
||||
public IpcCallerHeels(ILogger<IpcCallerHeels> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator)
|
||||
: base(logger, lightlessMediator, pi, HeelsDescriptor)
|
||||
{
|
||||
_logger = logger;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
@@ -32,8 +36,26 @@ public sealed class IpcCallerHeels : IIpcCaller
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
protected override IpcConnectionState EvaluateState()
|
||||
{
|
||||
var state = base.EvaluateState();
|
||||
if (state != IpcConnectionState.Available)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
try
|
||||
{
|
||||
return _heelsGetApiVersion.InvokeFunc() is { Item1: 2, Item2: >= 1 }
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to query SimpleHeels API version");
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
private void HeelsOffsetChange(string offset)
|
||||
{
|
||||
@@ -74,20 +96,14 @@ public sealed class IpcCallerHeels : IIpcCaller
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void CheckAPI()
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
try
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
{
|
||||
APIAvailable = _heelsGetApiVersion.InvokeFunc() is { Item1: 2, Item2: >= 1 };
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -8,8 +9,10 @@ using System.Text;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerHonorific : IIpcCaller
|
||||
public sealed class IpcCallerHonorific : IpcServiceBase
|
||||
{
|
||||
private static readonly IpcServiceDescriptor HonorificDescriptor = new("Honorific", "Honorific", new Version(0, 0, 0, 0));
|
||||
|
||||
private readonly ICallGateSubscriber<(uint major, uint minor)> _honorificApiVersion;
|
||||
private readonly ICallGateSubscriber<int, object> _honorificClearCharacterTitle;
|
||||
private readonly ICallGateSubscriber<object> _honorificDisposing;
|
||||
@@ -22,7 +25,7 @@ public sealed class IpcCallerHonorific : IIpcCaller
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
|
||||
public IpcCallerHonorific(ILogger<IpcCallerHonorific> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||
LightlessMediator lightlessMediator)
|
||||
LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, HonorificDescriptor)
|
||||
{
|
||||
_logger = logger;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
@@ -41,23 +44,14 @@ public sealed class IpcCallerHonorific : IIpcCaller
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public void CheckAPI()
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
try
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
{
|
||||
APIAvailable = _honorificApiVersion.InvokeFunc() is { Item1: 3, Item2: >= 1 };
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_honorificLocalCharacterTitleChanged.Unsubscribe(OnHonorificLocalCharacterTitleChanged);
|
||||
_honorificDisposing.Unsubscribe(OnHonorificDisposing);
|
||||
_honorificReady.Unsubscribe(OnHonorificReady);
|
||||
@@ -113,6 +107,27 @@ public sealed class IpcCallerHonorific : IIpcCaller
|
||||
}
|
||||
}
|
||||
|
||||
protected override IpcConnectionState EvaluateState()
|
||||
{
|
||||
var state = base.EvaluateState();
|
||||
if (state != IpcConnectionState.Available)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return _honorificApiVersion.InvokeFunc() is { Item1: 3, Item2: >= 1 }
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to query Honorific API version");
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHonorificDisposing()
|
||||
{
|
||||
_lightlessMediator.Publish(new HonorificMessage(string.Empty));
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerMoodles : IIpcCaller
|
||||
public sealed class IpcCallerMoodles : IpcServiceBase
|
||||
{
|
||||
private static readonly IpcServiceDescriptor MoodlesDescriptor = new("Moodles", "Moodles", new Version(0, 0, 0, 0));
|
||||
|
||||
private readonly ICallGateSubscriber<int> _moodlesApiVersion;
|
||||
private readonly ICallGateSubscriber<IPlayerCharacter, object> _moodlesOnChange;
|
||||
private readonly ICallGateSubscriber<nint, object> _moodlesOnChange;
|
||||
private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus;
|
||||
private readonly ICallGateSubscriber<nint, string, object> _moodlesSetStatus;
|
||||
private readonly ICallGateSubscriber<nint, object> _moodlesRevertStatus;
|
||||
@@ -19,44 +21,36 @@ public sealed class IpcCallerMoodles : IIpcCaller
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
|
||||
public IpcCallerMoodles(ILogger<IpcCallerMoodles> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||
LightlessMediator lightlessMediator)
|
||||
LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, MoodlesDescriptor)
|
||||
{
|
||||
_logger = logger;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
|
||||
_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");
|
||||
_moodlesOnChange = pi.GetIpcSubscriber<nint, object>("Moodles.StatusManagerModified");
|
||||
_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);
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
private void OnMoodlesChange(IPlayerCharacter character)
|
||||
private void OnMoodlesChange(nint address)
|
||||
{
|
||||
_lightlessMediator.Publish(new MoodlesMessage(character.Address));
|
||||
_lightlessMediator.Publish(new MoodlesMessage(address));
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public void CheckAPI()
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
try
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
{
|
||||
APIAvailable = _moodlesApiVersion.InvokeFunc() == 1;
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_moodlesOnChange.Unsubscribe(OnMoodlesChange);
|
||||
}
|
||||
|
||||
@@ -101,4 +95,25 @@ public sealed class IpcCallerMoodles : IIpcCaller
|
||||
_logger.LogWarning(e, "Could not Set Moodles Status");
|
||||
}
|
||||
}
|
||||
|
||||
protected override IpcConnectionState EvaluateState()
|
||||
{
|
||||
var state = base.EvaluateState();
|
||||
if (state != IpcConnectionState.Available)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return _moodlesApiVersion.InvokeFunc() >= 4
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to query Moodles API version");
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,146 +1,205 @@
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Interop.Ipc.Penumbra;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCaller
|
||||
public sealed class IpcCallerPenumbra : IpcServiceBase
|
||||
{
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly RedrawManager _redrawManager;
|
||||
private bool _shownPenumbraUnavailable = false;
|
||||
private string? _penumbraModDirectory;
|
||||
public string? ModDirectory
|
||||
{
|
||||
get => _penumbraModDirectory;
|
||||
private set
|
||||
{
|
||||
if (!string.Equals(_penumbraModDirectory, value, StringComparison.Ordinal))
|
||||
{
|
||||
_penumbraModDirectory = value;
|
||||
_lightlessMediator.Publish(new PenumbraDirectoryChangedMessage(_penumbraModDirectory));
|
||||
}
|
||||
}
|
||||
}
|
||||
private static readonly IpcServiceDescriptor PenumbraDescriptor = new("Penumbra", "Penumbra", new Version(1, 2, 0, 22));
|
||||
|
||||
private readonly ConcurrentDictionary<IntPtr, bool> _penumbraRedrawRequests = new();
|
||||
private readonly PenumbraCollections _collections;
|
||||
private readonly PenumbraResource _resources;
|
||||
private readonly PenumbraRedraw _redraw;
|
||||
private readonly PenumbraTexture _textures;
|
||||
|
||||
private readonly EventSubscriber _penumbraDispose;
|
||||
private readonly EventSubscriber<nint, string, string> _penumbraGameObjectResourcePathResolved;
|
||||
private readonly EventSubscriber _penumbraInit;
|
||||
private readonly EventSubscriber<ModSettingChange, Guid, string, bool> _penumbraModSettingChanged;
|
||||
private readonly EventSubscriber<nint, int> _penumbraObjectIsRedrawn;
|
||||
|
||||
private readonly AddTemporaryMod _penumbraAddTemporaryMod;
|
||||
private readonly AssignTemporaryCollection _penumbraAssignTemporaryCollection;
|
||||
private readonly ConvertTextureFile _penumbraConvertTextureFile;
|
||||
private readonly CreateTemporaryCollection _penumbraCreateNamedTemporaryCollection;
|
||||
private readonly GetEnabledState _penumbraEnabled;
|
||||
private readonly GetPlayerMetaManipulations _penumbraGetMetaManipulations;
|
||||
private readonly RedrawObject _penumbraRedraw;
|
||||
private readonly DeleteTemporaryCollection _penumbraRemoveTemporaryCollection;
|
||||
private readonly RemoveTemporaryMod _penumbraRemoveTemporaryMod;
|
||||
private readonly GetModDirectory _penumbraResolveModDir;
|
||||
private readonly ResolvePlayerPathsAsync _penumbraResolvePaths;
|
||||
private readonly GetGameObjectResourcePaths _penumbraResourcePaths;
|
||||
private readonly GetModDirectory _penumbraGetModDirectory;
|
||||
private readonly EventSubscriber _penumbraInit;
|
||||
private readonly EventSubscriber _penumbraDispose;
|
||||
private readonly EventSubscriber<ModSettingChange, Guid, string, bool> _penumbraModSettingChanged;
|
||||
|
||||
public IpcCallerPenumbra(ILogger<IpcCallerPenumbra> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||
LightlessMediator lightlessMediator, RedrawManager redrawManager) : base(logger, lightlessMediator)
|
||||
private bool _shownPenumbraUnavailable;
|
||||
private string? _modDirectory;
|
||||
|
||||
public IpcCallerPenumbra(
|
||||
ILogger<IpcCallerPenumbra> logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator,
|
||||
RedrawManager redrawManager,
|
||||
ActorObjectService actorObjectService) : base(logger, mediator, pluginInterface, PenumbraDescriptor)
|
||||
{
|
||||
_pi = pi;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_redrawManager = redrawManager;
|
||||
_penumbraInit = Initialized.Subscriber(pi, PenumbraInit);
|
||||
_penumbraDispose = Disposed.Subscriber(pi, PenumbraDispose);
|
||||
_penumbraResolveModDir = new GetModDirectory(pi);
|
||||
_penumbraRedraw = new RedrawObject(pi);
|
||||
_penumbraObjectIsRedrawn = GameObjectRedrawn.Subscriber(pi, RedrawEvent);
|
||||
_penumbraGetMetaManipulations = new GetPlayerMetaManipulations(pi);
|
||||
_penumbraRemoveTemporaryMod = new RemoveTemporaryMod(pi);
|
||||
_penumbraAddTemporaryMod = new AddTemporaryMod(pi);
|
||||
_penumbraCreateNamedTemporaryCollection = new CreateTemporaryCollection(pi);
|
||||
_penumbraRemoveTemporaryCollection = new DeleteTemporaryCollection(pi);
|
||||
_penumbraAssignTemporaryCollection = new AssignTemporaryCollection(pi);
|
||||
_penumbraResolvePaths = new ResolvePlayerPathsAsync(pi);
|
||||
_penumbraEnabled = new GetEnabledState(pi);
|
||||
_penumbraModSettingChanged = ModSettingChanged.Subscriber(pi, (change, arg1, arg, b) =>
|
||||
{
|
||||
if (change == ModSettingChange.EnableState)
|
||||
_lightlessMediator.Publish(new PenumbraModSettingChangedMessage());
|
||||
});
|
||||
_penumbraConvertTextureFile = new ConvertTextureFile(pi);
|
||||
_penumbraResourcePaths = new GetGameObjectResourcePaths(pi);
|
||||
_penumbraEnabled = new GetEnabledState(pluginInterface);
|
||||
_penumbraGetModDirectory = new GetModDirectory(pluginInterface);
|
||||
_penumbraInit = Initialized.Subscriber(pluginInterface, HandlePenumbraInitialized);
|
||||
_penumbraDispose = Disposed.Subscriber(pluginInterface, HandlePenumbraDisposed);
|
||||
_penumbraModSettingChanged = ModSettingChanged.Subscriber(pluginInterface, HandlePenumbraModSettingChanged);
|
||||
|
||||
_penumbraGameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded);
|
||||
_collections = RegisterInterop(new PenumbraCollections(logger, pluginInterface, dalamudUtil, mediator));
|
||||
_resources = RegisterInterop(new PenumbraResource(logger, pluginInterface, dalamudUtil, mediator, actorObjectService));
|
||||
_redraw = RegisterInterop(new PenumbraRedraw(logger, pluginInterface, dalamudUtil, mediator, redrawManager));
|
||||
_textures = RegisterInterop(new PenumbraTexture(logger, pluginInterface, dalamudUtil, mediator, _redraw));
|
||||
|
||||
SubscribeMediatorEvents();
|
||||
|
||||
CheckAPI();
|
||||
CheckModDirectory();
|
||||
|
||||
Mediator.Subscribe<PenumbraRedrawCharacterMessage>(this, (msg) =>
|
||||
{
|
||||
_penumbraRedraw.Invoke(msg.Character.ObjectIndex, RedrawType.AfterGPose);
|
||||
});
|
||||
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, (msg) => _shownPenumbraUnavailable = false);
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public void CheckAPI()
|
||||
public string? ModDirectory
|
||||
{
|
||||
bool penumbraAvailable = false;
|
||||
try
|
||||
get => _modDirectory;
|
||||
private set
|
||||
{
|
||||
var penumbraVersion = (_pi.InstalledPlugins
|
||||
.FirstOrDefault(p => string.Equals(p.InternalName, "Penumbra", StringComparison.OrdinalIgnoreCase))
|
||||
?.Version ?? new Version(0, 0, 0, 0));
|
||||
penumbraAvailable = penumbraVersion >= new Version(1, 2, 0, 22);
|
||||
try
|
||||
if (string.Equals(_modDirectory, value, StringComparison.Ordinal))
|
||||
{
|
||||
penumbraAvailable &= _penumbraEnabled.Invoke();
|
||||
}
|
||||
catch
|
||||
{
|
||||
penumbraAvailable = false;
|
||||
}
|
||||
_shownPenumbraUnavailable = _shownPenumbraUnavailable && !penumbraAvailable;
|
||||
APIAvailable = penumbraAvailable;
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = penumbraAvailable;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!penumbraAvailable && !_shownPenumbraUnavailable)
|
||||
{
|
||||
_shownPenumbraUnavailable = true;
|
||||
_lightlessMediator.Publish(new NotificationMessage("Penumbra inactive",
|
||||
"Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Lightless. If you just updated Penumbra, ignore this message.",
|
||||
NotificationType.Error));
|
||||
return;
|
||||
}
|
||||
|
||||
_modDirectory = value;
|
||||
Mediator.Publish(new PenumbraDirectoryChangedMessage(_modDirectory));
|
||||
}
|
||||
}
|
||||
|
||||
public Task AssignTemporaryCollectionAsync(ILogger logger, Guid collectionId, int objectIndex)
|
||||
=> _collections.AssignTemporaryCollectionAsync(logger, collectionId, objectIndex);
|
||||
|
||||
public Task<Guid> CreateTemporaryCollectionAsync(ILogger logger, string uid)
|
||||
=> _collections.CreateTemporaryCollectionAsync(logger, uid);
|
||||
|
||||
public Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collectionId)
|
||||
=> _collections.RemoveTemporaryCollectionAsync(logger, applicationId, collectionId);
|
||||
|
||||
public Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths)
|
||||
=> _collections.SetTemporaryModsAsync(logger, applicationId, collectionId, modPaths);
|
||||
|
||||
public Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collectionId, string manipulationData)
|
||||
=> _collections.SetManipulationDataAsync(logger, applicationId, collectionId, manipulationData);
|
||||
|
||||
public Task<Dictionary<string, HashSet<string>>?> GetCharacterData(ILogger logger, GameObjectHandler handler)
|
||||
=> _resources.GetCharacterDataAsync(logger, handler);
|
||||
|
||||
public string GetMetaManipulations()
|
||||
=> _resources.GetMetaManipulations();
|
||||
|
||||
public Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse)
|
||||
=> _resources.ResolvePathsAsync(forward, reverse);
|
||||
|
||||
public Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
||||
=> _redraw.RedrawAsync(logger, handler, applicationId, token);
|
||||
|
||||
public Task ConvertTextureFiles(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token)
|
||||
=> _textures.ConvertTextureFilesAsync(logger, jobs, progress, token);
|
||||
|
||||
public Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token)
|
||||
=> _textures.ConvertTextureFileDirectAsync(job, token);
|
||||
|
||||
public void CheckModDirectory()
|
||||
{
|
||||
if (!APIAvailable)
|
||||
{
|
||||
ModDirectory = string.Empty;
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
try
|
||||
{
|
||||
ModDirectory = _penumbraResolveModDir!.Invoke().ToLowerInvariant();
|
||||
ModDirectory = _penumbraGetModDirectory.Invoke().ToLowerInvariant();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to resolve Penumbra mod directory");
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool IsPluginEnabled(IExposedPlugin plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _penumbraEnabled.Invoke();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
base.OnConnectionStateChanged(previous, current);
|
||||
|
||||
if (current == IpcConnectionState.Available)
|
||||
{
|
||||
_shownPenumbraUnavailable = false;
|
||||
if (string.IsNullOrEmpty(ModDirectory))
|
||||
{
|
||||
CheckModDirectory();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ModDirectory = string.Empty;
|
||||
_redraw.CancelPendingRedraws();
|
||||
|
||||
if (_shownPenumbraUnavailable || current == IpcConnectionState.Unknown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_shownPenumbraUnavailable = true;
|
||||
Mediator.Publish(new NotificationMessage(
|
||||
"Penumbra inactive",
|
||||
"Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Lightless. If you just updated Penumbra, ignore this message.",
|
||||
NotificationType.Error));
|
||||
}
|
||||
|
||||
private void SubscribeMediatorEvents()
|
||||
{
|
||||
Mediator.Subscribe<PenumbraRedrawCharacterMessage>(this, msg =>
|
||||
{
|
||||
_redraw.RequestImmediateRedraw(msg.Character.ObjectIndex, RedrawType.AfterGPose);
|
||||
});
|
||||
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, _ => _shownPenumbraUnavailable = false);
|
||||
|
||||
Mediator.Subscribe<ActorTrackedMessage>(this, msg => _resources.TrackActor(msg.Descriptor.Address));
|
||||
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => _resources.UntrackActor(msg.Descriptor.Address));
|
||||
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, msg => _resources.TrackActor(msg.GameObjectHandler.Address));
|
||||
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, msg => _resources.UntrackActor(msg.GameObjectHandler.Address));
|
||||
}
|
||||
|
||||
private void HandlePenumbraInitialized()
|
||||
{
|
||||
Mediator.Publish(new PenumbraInitializedMessage());
|
||||
CheckModDirectory();
|
||||
_redraw.RequestImmediateRedraw(0, RedrawType.Redraw);
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
private void HandlePenumbraDisposed()
|
||||
{
|
||||
_redraw.CancelPendingRedraws();
|
||||
ModDirectory = string.Empty;
|
||||
Mediator.Publish(new PenumbraDisposedMessage());
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
private void HandlePenumbraModSettingChanged(ModSettingChange change, Guid _, string __, bool ___)
|
||||
{
|
||||
if (change == ModSettingChange.EnableState)
|
||||
{
|
||||
Mediator.Publish(new PenumbraModSettingChangedMessage());
|
||||
CheckAPI();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,196 +207,13 @@ public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCa
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
_redrawManager.Cancel();
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_penumbraModSettingChanged.Dispose();
|
||||
_penumbraGameObjectResourcePathResolved.Dispose();
|
||||
_penumbraDispose.Dispose();
|
||||
_penumbraInit.Dispose();
|
||||
_penumbraObjectIsRedrawn.Dispose();
|
||||
}
|
||||
|
||||
public async Task AssignTemporaryCollectionAsync(ILogger logger, Guid collName, int idx)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var retAssign = _penumbraAssignTemporaryCollection.Invoke(collName, idx, forceAssignment: true);
|
||||
logger.LogTrace("Assigning Temp Collection {collName} to index {idx}, Success: {ret}", collName, idx, retAssign);
|
||||
return collName;
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task ConvertTextureFiles(ILogger logger, Dictionary<string, string[]> textures, IProgress<(string, int)> progress, CancellationToken token)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
|
||||
_lightlessMediator.Publish(new HaltScanMessage(nameof(ConvertTextureFiles)));
|
||||
int currentTexture = 0;
|
||||
foreach (var texture in textures)
|
||||
{
|
||||
if (token.IsCancellationRequested) break;
|
||||
|
||||
progress.Report((texture.Key, ++currentTexture));
|
||||
|
||||
logger.LogInformation("Converting Texture {path} to {type}", texture.Key, TextureType.Bc7Tex);
|
||||
var convertTask = _penumbraConvertTextureFile.Invoke(texture.Key, texture.Key, TextureType.Bc7Tex, mipMaps: true);
|
||||
await convertTask.ConfigureAwait(false);
|
||||
if (convertTask.IsCompletedSuccessfully && texture.Value.Any())
|
||||
{
|
||||
foreach (var duplicatedTexture in texture.Value)
|
||||
{
|
||||
logger.LogInformation("Migrating duplicate {dup}", duplicatedTexture);
|
||||
try
|
||||
{
|
||||
File.Copy(texture.Key, duplicatedTexture, overwrite: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to copy duplicate {dup}", duplicatedTexture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_lightlessMediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFiles)));
|
||||
|
||||
await _dalamudUtil.RunOnFrameworkThread(async () =>
|
||||
{
|
||||
var gameObject = await _dalamudUtil.CreateGameObjectAsync(await _dalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false)).ConfigureAwait(false);
|
||||
_penumbraRedraw.Invoke(gameObject!.ObjectIndex, setting: RedrawType.Redraw);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateTemporaryCollectionAsync(ILogger logger, string uid)
|
||||
{
|
||||
if (!APIAvailable) return Guid.Empty;
|
||||
|
||||
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var collName = "Lightless_" + uid;
|
||||
_penumbraCreateNamedTemporaryCollection.Invoke(collName, collName, out var collId);
|
||||
logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId);
|
||||
return collId;
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, HashSet<string>>?> GetCharacterData(ILogger logger, GameObjectHandler handler)
|
||||
{
|
||||
if (!APIAvailable) return null;
|
||||
|
||||
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths");
|
||||
var idx = handler.GetGameObject()?.ObjectIndex;
|
||||
if (idx == null) return null;
|
||||
return _penumbraResourcePaths.Invoke(idx.Value)[0];
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string GetMetaManipulations()
|
||||
{
|
||||
if (!APIAvailable) return string.Empty;
|
||||
return _penumbraGetMetaManipulations.Invoke();
|
||||
}
|
||||
|
||||
public async Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
||||
{
|
||||
if (!APIAvailable || _dalamudUtil.IsZoning) return;
|
||||
try
|
||||
{
|
||||
await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) =>
|
||||
{
|
||||
logger.LogDebug("[{appid}] Calling on IPC: PenumbraRedraw", applicationId);
|
||||
_penumbraRedraw!.Invoke(chara.ObjectIndex, setting: RedrawType.Redraw);
|
||||
|
||||
}, token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_redrawManager.RedrawSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collId)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("[{applicationId}] Removing temp collection for {collId}", applicationId, collId);
|
||||
var ret2 = _penumbraRemoveTemporaryCollection.Invoke(collId);
|
||||
logger.LogTrace("[{applicationId}] RemoveTemporaryCollection: {ret2}", applicationId, ret2);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse)
|
||||
{
|
||||
return await _penumbraResolvePaths.Invoke(forward, reverse).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collId, string manipulationData)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("[{applicationId}] Manip: {data}", applicationId, manipulationData);
|
||||
var retAdd = _penumbraAddTemporaryMod.Invoke("LightlessChara_Meta", collId, [], manipulationData, 0);
|
||||
logger.LogTrace("[{applicationId}] Setting temp meta mod for {collId}, Success: {ret}", applicationId, collId, retAdd);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collId, Dictionary<string, string> modPaths)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
foreach (var mod in modPaths)
|
||||
{
|
||||
logger.LogTrace("[{applicationId}] Change: {from} => {to}", applicationId, mod.Key, mod.Value);
|
||||
}
|
||||
var retRemove = _penumbraRemoveTemporaryMod.Invoke("LightlessChara_Files", collId, 0);
|
||||
logger.LogTrace("[{applicationId}] Removing temp files mod for {collId}, Success: {ret}", applicationId, collId, retRemove);
|
||||
var retAdd = _penumbraAddTemporaryMod.Invoke("LightlessChara_Files", collId, modPaths, string.Empty, 0);
|
||||
logger.LogTrace("[{applicationId}] Setting temp files mod for {collId}, Success: {ret}", applicationId, collId, retAdd);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void RedrawEvent(IntPtr objectAddress, int objectTableIndex)
|
||||
{
|
||||
bool wasRequested = false;
|
||||
if (_penumbraRedrawRequests.TryGetValue(objectAddress, out var redrawRequest) && redrawRequest)
|
||||
{
|
||||
_penumbraRedrawRequests[objectAddress] = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_lightlessMediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, wasRequested));
|
||||
}
|
||||
}
|
||||
|
||||
private void ResourceLoaded(IntPtr ptr, string arg1, string arg2)
|
||||
{
|
||||
if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, ignoreCase: true, System.Globalization.CultureInfo.InvariantCulture) != 0)
|
||||
{
|
||||
_lightlessMediator.Publish(new PenumbraResourceLoadMessage(ptr, arg1, arg2));
|
||||
}
|
||||
}
|
||||
|
||||
private void PenumbraDispose()
|
||||
{
|
||||
_redrawManager.Cancel();
|
||||
_lightlessMediator.Publish(new PenumbraDisposedMessage());
|
||||
}
|
||||
|
||||
private void PenumbraInit()
|
||||
{
|
||||
APIAvailable = true;
|
||||
ModDirectory = _penumbraResolveModDir.Invoke();
|
||||
_lightlessMediator.Publish(new PenumbraInitializedMessage());
|
||||
_penumbraRedraw!.Invoke(0, setting: RedrawType.Redraw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerPetNames : IIpcCaller
|
||||
public sealed class IpcCallerPetNames : IpcServiceBase
|
||||
{
|
||||
private static readonly IpcServiceDescriptor PetRenamerDescriptor = new("PetRenamer", "Pet Renamer", new Version(0, 0, 0, 0));
|
||||
|
||||
private readonly ILogger<IpcCallerPetNames> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
@@ -24,18 +27,18 @@ public sealed class IpcCallerPetNames : IIpcCaller
|
||||
private readonly ICallGateSubscriber<ushort, object> _clearPlayerData;
|
||||
|
||||
public IpcCallerPetNames(ILogger<IpcCallerPetNames> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||
LightlessMediator lightlessMediator)
|
||||
LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, PetRenamerDescriptor)
|
||||
{
|
||||
_logger = logger;
|
||||
_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");
|
||||
@@ -46,25 +49,6 @@ public sealed class IpcCallerPetNames : IIpcCaller
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
try
|
||||
{
|
||||
APIAvailable = _enabled?.InvokeFunc() ?? false;
|
||||
if (APIAvailable)
|
||||
{
|
||||
APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 3, Item2: >= 1 };
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPetNicknamesReady()
|
||||
{
|
||||
CheckAPI();
|
||||
@@ -76,6 +60,34 @@ public sealed class IpcCallerPetNames : IIpcCaller
|
||||
_lightlessMediator.Publish(new PetNamesMessage(string.Empty));
|
||||
}
|
||||
|
||||
protected override IpcConnectionState EvaluateState()
|
||||
{
|
||||
var state = base.EvaluateState();
|
||||
if (state != IpcConnectionState.Available)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var enabled = _enabled?.InvokeFunc() ?? false;
|
||||
if (!enabled)
|
||||
{
|
||||
return IpcConnectionState.PluginDisabled;
|
||||
}
|
||||
|
||||
var version = _apiVersion?.InvokeFunc() ?? (0u, 0u);
|
||||
return version.Item1 == 4 && version.Item2 >= 0
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to query Pet Renamer API version");
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetLocalNames()
|
||||
{
|
||||
if (!APIAvailable) return string.Empty;
|
||||
@@ -84,7 +96,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");
|
||||
@@ -149,8 +161,14 @@ public sealed class IpcCallerPetNames : IIpcCaller
|
||||
_lightlessMediator.Publish(new PetNamesMessage(data));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_petnamesReady.Unsubscribe(OnPetNicknamesReady);
|
||||
_petnamesDisposing.Unsubscribe(OnPetNicknamesDispose);
|
||||
_playerDataChanged.Unsubscribe(OnLocalPetNicknamesDataChange);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using System;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
@@ -14,9 +15,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
||||
private readonly ILogger<IpcProvider> _logger;
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
private ICallGateProvider<string, IGameObject, bool>? _loadFileProvider;
|
||||
private ICallGateProvider<string, IGameObject, Task<bool>>? _loadFileAsyncProvider;
|
||||
private ICallGateProvider<List<nint>>? _handledGameAddresses;
|
||||
private readonly List<IpcRegister> _ipcRegisters = [];
|
||||
private readonly List<GameObjectHandler> _activeGameObjectHandlers = [];
|
||||
|
||||
public LightlessMediator Mediator { get; init; }
|
||||
@@ -44,12 +43,9 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Starting IpcProviderService");
|
||||
_loadFileProvider = _pi.GetIpcProvider<string, IGameObject, bool>("LightlessSync.LoadMcdf");
|
||||
_loadFileProvider.RegisterFunc(LoadMcdf);
|
||||
_loadFileAsyncProvider = _pi.GetIpcProvider<string, IGameObject, Task<bool>>("LightlessSync.LoadMcdfAsync");
|
||||
_loadFileAsyncProvider.RegisterFunc(LoadMcdfAsync);
|
||||
_handledGameAddresses = _pi.GetIpcProvider<List<nint>>("LightlessSync.GetHandledAddresses");
|
||||
_handledGameAddresses.RegisterFunc(GetHandledAddresses);
|
||||
_ipcRegisters.Add(RegisterFunc<string, IGameObject, bool>("LightlessSync.LoadMcdf", LoadMcdf));
|
||||
_ipcRegisters.Add(RegisterFunc<string, IGameObject, Task<bool>>("LightlessSync.LoadMcdfAsync", LoadMcdfAsync));
|
||||
_ipcRegisters.Add(RegisterFunc("LightlessSync.GetHandledAddresses", GetHandledAddresses));
|
||||
_logger.LogInformation("Started IpcProviderService");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -57,9 +53,11 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug("Stopping IpcProvider Service");
|
||||
_loadFileProvider?.UnregisterFunc();
|
||||
_loadFileAsyncProvider?.UnregisterFunc();
|
||||
_handledGameAddresses?.UnregisterFunc();
|
||||
foreach (var register in _ipcRegisters)
|
||||
{
|
||||
register.Dispose();
|
||||
}
|
||||
_ipcRegisters.Clear();
|
||||
Mediator.UnsubscribeAll(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -89,4 +87,40 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
return _activeGameObjectHandlers.Where(g => g.Address != nint.Zero).Select(g => g.Address).Distinct().ToList();
|
||||
}
|
||||
|
||||
private IpcRegister RegisterFunc(string label, Func<List<nint>> handler)
|
||||
{
|
||||
var provider = _pi.GetIpcProvider<List<nint>>(label);
|
||||
provider.RegisterFunc(handler);
|
||||
return new IpcRegister(provider.UnregisterFunc);
|
||||
}
|
||||
|
||||
private IpcRegister RegisterFunc<T1, T2, TRet>(string label, Func<T1, T2, TRet> handler)
|
||||
{
|
||||
var provider = _pi.GetIpcProvider<T1, T2, TRet>(label);
|
||||
provider.RegisterFunc(handler);
|
||||
return new IpcRegister(provider.UnregisterFunc);
|
||||
}
|
||||
|
||||
private sealed class IpcRegister : IDisposable
|
||||
{
|
||||
private readonly Action _unregister;
|
||||
private bool _disposed;
|
||||
|
||||
public IpcRegister(Action unregister)
|
||||
{
|
||||
_unregister = unregister;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_unregister();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
LightlessSync/Interop/Ipc/Penumbra/PenumbraBase.cs
Normal file
27
LightlessSync/Interop/Ipc/Penumbra/PenumbraBase.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc.Penumbra;
|
||||
|
||||
public abstract class PenumbraBase : IpcInteropBase
|
||||
{
|
||||
protected PenumbraBase(
|
||||
ILogger logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator) : base(logger)
|
||||
{
|
||||
PluginInterface = pluginInterface;
|
||||
DalamudUtil = dalamudUtil;
|
||||
Mediator = mediator;
|
||||
}
|
||||
|
||||
protected IDalamudPluginInterface PluginInterface { get; }
|
||||
|
||||
protected DalamudUtilService DalamudUtil { get; }
|
||||
|
||||
protected LightlessMediator Mediator { get; }
|
||||
}
|
||||
197
LightlessSync/Interop/Ipc/Penumbra/PenumbraCollections.cs
Normal file
197
LightlessSync/Interop/Ipc/Penumbra/PenumbraCollections.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc.Penumbra;
|
||||
|
||||
public sealed class PenumbraCollections : PenumbraBase
|
||||
{
|
||||
private readonly CreateTemporaryCollection _createNamedTemporaryCollection;
|
||||
private readonly AssignTemporaryCollection _assignTemporaryCollection;
|
||||
private readonly DeleteTemporaryCollection _removeTemporaryCollection;
|
||||
private readonly AddTemporaryMod _addTemporaryMod;
|
||||
private readonly RemoveTemporaryMod _removeTemporaryMod;
|
||||
private readonly GetCollections _getCollections;
|
||||
private readonly ConcurrentDictionary<Guid, string> _activeTemporaryCollections = new();
|
||||
|
||||
private int _cleanupScheduled;
|
||||
|
||||
public PenumbraCollections(
|
||||
ILogger logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator) : base(logger, pluginInterface, dalamudUtil, mediator)
|
||||
{
|
||||
_createNamedTemporaryCollection = new CreateTemporaryCollection(pluginInterface);
|
||||
_assignTemporaryCollection = new AssignTemporaryCollection(pluginInterface);
|
||||
_removeTemporaryCollection = new DeleteTemporaryCollection(pluginInterface);
|
||||
_addTemporaryMod = new AddTemporaryMod(pluginInterface);
|
||||
_removeTemporaryMod = new RemoveTemporaryMod(pluginInterface);
|
||||
_getCollections = new GetCollections(pluginInterface);
|
||||
}
|
||||
|
||||
public override string Name => "Penumbra.Collections";
|
||||
|
||||
public async Task AssignTemporaryCollectionAsync(ILogger logger, Guid collectionId, int objectIndex)
|
||||
{
|
||||
if (!IsAvailable || collectionId == Guid.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var result = _assignTemporaryCollection.Invoke(collectionId, objectIndex, forceAssignment: true);
|
||||
logger.LogTrace("Assigning Temp Collection {CollectionId} to index {ObjectIndex}, Success: {Result}", collectionId, objectIndex, result);
|
||||
return result;
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateTemporaryCollectionAsync(ILogger logger, string uid)
|
||||
{
|
||||
if (!IsAvailable)
|
||||
{
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
var (collectionId, collectionName) = await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var name = $"Lightless_{uid}";
|
||||
_createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId);
|
||||
logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}", name, tempCollectionId);
|
||||
return (tempCollectionId, name);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (collectionId != Guid.Empty)
|
||||
{
|
||||
_activeTemporaryCollections[collectionId] = collectionName;
|
||||
}
|
||||
|
||||
return collectionId;
|
||||
}
|
||||
|
||||
public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collectionId)
|
||||
{
|
||||
if (!IsAvailable || collectionId == Guid.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("[{ApplicationId}] Removing temp collection for {CollectionId}", applicationId, collectionId);
|
||||
var result = _removeTemporaryCollection.Invoke(collectionId);
|
||||
logger.LogTrace("[{ApplicationId}] RemoveTemporaryCollection: {Result}", applicationId, result);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
||||
}
|
||||
|
||||
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, IReadOnlyDictionary<string, string> modPaths)
|
||||
{
|
||||
if (!IsAvailable || collectionId == Guid.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
foreach (var mod in modPaths)
|
||||
{
|
||||
logger.LogTrace("[{ApplicationId}] Change: {From} => {To}", applicationId, mod.Key, mod.Value);
|
||||
}
|
||||
|
||||
var removeResult = _removeTemporaryMod.Invoke("LightlessChara_Files", collectionId, 0);
|
||||
logger.LogTrace("[{ApplicationId}] Removing temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, removeResult);
|
||||
|
||||
var addResult = _addTemporaryMod.Invoke("LightlessChara_Files", collectionId, new Dictionary<string, string>(modPaths), string.Empty, 0);
|
||||
logger.LogTrace("[{ApplicationId}] Setting temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, addResult);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collectionId, string manipulationData)
|
||||
{
|
||||
if (!IsAvailable || collectionId == Guid.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("[{ApplicationId}] Manip: {Data}", applicationId, manipulationData);
|
||||
var result = _addTemporaryMod.Invoke("LightlessChara_Meta", collectionId, [], manipulationData, 0);
|
||||
logger.LogTrace("[{ApplicationId}] Setting temp meta mod for {CollectionId}, Success: {Result}", applicationId, collectionId, result);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
if (current == IpcConnectionState.Available)
|
||||
{
|
||||
ScheduleCleanup();
|
||||
}
|
||||
else if (previous == IpcConnectionState.Available && current != IpcConnectionState.Available)
|
||||
{
|
||||
Interlocked.Exchange(ref _cleanupScheduled, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleCleanup()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _cleanupScheduled, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.Run(CleanupTemporaryCollectionsAsync);
|
||||
}
|
||||
|
||||
private async Task CleanupTemporaryCollectionsAsync()
|
||||
{
|
||||
if (!IsAvailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var collections = await DalamudUtil.RunOnFrameworkThread(() => _getCollections.Invoke()).ConfigureAwait(false);
|
||||
foreach (var (collectionId, name) in collections)
|
||||
{
|
||||
if (!IsLightlessCollectionName(name) || _activeTemporaryCollections.ContainsKey(collectionId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogDebug("Cleaning up stale temporary collection {CollectionName} ({CollectionId})", name, collectionId);
|
||||
var deleteResult = await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var result = (PenumbraApiEc)_removeTemporaryCollection.Invoke(collectionId);
|
||||
Logger.LogTrace("Cleanup RemoveTemporaryCollection result for {CollectionName} ({CollectionId}): {Result}", name, collectionId, result);
|
||||
return result;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (deleteResult == PenumbraApiEc.Success)
|
||||
{
|
||||
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug("Skipped removing temporary collection {CollectionName} ({CollectionId}). Result: {Result}", name, collectionId, deleteResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to clean up Penumbra temporary collections");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsLightlessCollectionName(string? name)
|
||||
=> !string.IsNullOrEmpty(name) && name.StartsWith("Lightless_", StringComparison.Ordinal);
|
||||
}
|
||||
89
LightlessSync/Interop/Ipc/Penumbra/PenumbraRedraw.cs
Normal file
89
LightlessSync/Interop/Ipc/Penumbra/PenumbraRedraw.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc.Penumbra;
|
||||
|
||||
public sealed class PenumbraRedraw : PenumbraBase
|
||||
{
|
||||
private readonly RedrawManager _redrawManager;
|
||||
private readonly RedrawObject _penumbraRedraw;
|
||||
private readonly EventSubscriber<nint, int> _penumbraObjectIsRedrawn;
|
||||
|
||||
public PenumbraRedraw(
|
||||
ILogger logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator,
|
||||
RedrawManager redrawManager) : base(logger, pluginInterface, dalamudUtil, mediator)
|
||||
{
|
||||
_redrawManager = redrawManager;
|
||||
|
||||
_penumbraRedraw = new RedrawObject(pluginInterface);
|
||||
_penumbraObjectIsRedrawn = GameObjectRedrawn.Subscriber(pluginInterface, HandlePenumbraRedrawEvent);
|
||||
}
|
||||
|
||||
public override string Name => "Penumbra.Redraw";
|
||||
|
||||
public void CancelPendingRedraws()
|
||||
=> _redrawManager.Cancel();
|
||||
|
||||
public void RequestImmediateRedraw(int objectIndex, RedrawType redrawType)
|
||||
{
|
||||
if (!IsAvailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_penumbraRedraw.Invoke(objectIndex, redrawType);
|
||||
}
|
||||
|
||||
public async Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
||||
{
|
||||
if (!IsAvailable || DalamudUtil.IsZoning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var redrawSemaphore = _redrawManager.RedrawSemaphore;
|
||||
var semaphoreAcquired = false;
|
||||
|
||||
try
|
||||
{
|
||||
await redrawSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
semaphoreAcquired = true;
|
||||
|
||||
await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, chara =>
|
||||
{
|
||||
logger.LogDebug("[{ApplicationId}] Calling on IPC: PenumbraRedraw", applicationId);
|
||||
_penumbraRedraw.Invoke(chara.ObjectIndex, RedrawType.Redraw);
|
||||
}, token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (semaphoreAcquired)
|
||||
{
|
||||
redrawSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePenumbraRedrawEvent(IntPtr objectAddress, int objectTableIndex)
|
||||
=> Mediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, false));
|
||||
|
||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
_penumbraObjectIsRedrawn.Dispose();
|
||||
}
|
||||
}
|
||||
141
LightlessSync/Interop/Ipc/Penumbra/PenumbraResource.cs
Normal file
141
LightlessSync/Interop/Ipc/Penumbra/PenumbraResource.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc.Penumbra;
|
||||
|
||||
public sealed class PenumbraResource : PenumbraBase
|
||||
{
|
||||
private readonly ActorObjectService _actorObjectService;
|
||||
private readonly GetGameObjectResourcePaths _gameObjectResourcePaths;
|
||||
private readonly ResolvePlayerPathsAsync _resolvePlayerPaths;
|
||||
private readonly GetPlayerMetaManipulations _getPlayerMetaManipulations;
|
||||
private readonly EventSubscriber<nint, string, string> _gameObjectResourcePathResolved;
|
||||
private readonly ConcurrentDictionary<IntPtr, byte> _trackedActors = new();
|
||||
|
||||
public PenumbraResource(
|
||||
ILogger logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator,
|
||||
ActorObjectService actorObjectService) : base(logger, pluginInterface, dalamudUtil, mediator)
|
||||
{
|
||||
_actorObjectService = actorObjectService;
|
||||
_gameObjectResourcePaths = new GetGameObjectResourcePaths(pluginInterface);
|
||||
_resolvePlayerPaths = new ResolvePlayerPathsAsync(pluginInterface);
|
||||
_getPlayerMetaManipulations = new GetPlayerMetaManipulations(pluginInterface);
|
||||
_gameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pluginInterface, HandleResourceLoaded);
|
||||
|
||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||
{
|
||||
TrackActor(descriptor.Address);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Name => "Penumbra.Resources";
|
||||
|
||||
public async Task<Dictionary<string, HashSet<string>>?> GetCharacterDataAsync(ILogger logger, GameObjectHandler handler)
|
||||
{
|
||||
if (!IsAvailable)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths");
|
||||
var idx = handler.GetGameObject()?.ObjectIndex;
|
||||
if (idx == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _gameObjectResourcePaths.Invoke(idx.Value)[0];
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string GetMetaManipulations()
|
||||
=> IsAvailable ? _getPlayerMetaManipulations.Invoke() : string.Empty;
|
||||
|
||||
public async Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forwardPaths, string[] reversePaths)
|
||||
{
|
||||
if (!IsAvailable)
|
||||
{
|
||||
return (Array.Empty<string>(), Array.Empty<string[]>());
|
||||
}
|
||||
|
||||
return await _resolvePlayerPaths.Invoke(forwardPaths, reversePaths).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void TrackActor(nint address)
|
||||
{
|
||||
if (address != nint.Zero)
|
||||
{
|
||||
_trackedActors[(IntPtr)address] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void UntrackActor(nint address)
|
||||
{
|
||||
if (address != nint.Zero)
|
||||
{
|
||||
_trackedActors.TryRemove((IntPtr)address, out _);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleResourceLoaded(nint ptr, string resolvedPath, string gamePath)
|
||||
{
|
||||
if (ptr == nint.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_trackedActors.ContainsKey(ptr))
|
||||
{
|
||||
var descriptor = _actorObjectService.PlayerDescriptors.FirstOrDefault(d => d.Address == ptr);
|
||||
if (descriptor.Address != nint.Zero)
|
||||
{
|
||||
_trackedActors[ptr] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.Compare(resolvedPath, gamePath, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, resolvedPath, gamePath));
|
||||
}
|
||||
|
||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
if (current != IpcConnectionState.Available)
|
||||
{
|
||||
_trackedActors.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||
{
|
||||
TrackActor(descriptor.Address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
_gameObjectResourcePathResolved.Dispose();
|
||||
}
|
||||
}
|
||||
121
LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs
Normal file
121
LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc.Penumbra;
|
||||
|
||||
public sealed class PenumbraTexture : PenumbraBase
|
||||
{
|
||||
private readonly PenumbraRedraw _redrawFeature;
|
||||
private readonly ConvertTextureFile _convertTextureFile;
|
||||
|
||||
public PenumbraTexture(
|
||||
ILogger logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator,
|
||||
PenumbraRedraw redrawFeature) : base(logger, pluginInterface, dalamudUtil, mediator)
|
||||
{
|
||||
_redrawFeature = redrawFeature;
|
||||
_convertTextureFile = new ConvertTextureFile(pluginInterface);
|
||||
}
|
||||
|
||||
public override string Name => "Penumbra.Textures";
|
||||
|
||||
public async Task ConvertTextureFilesAsync(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token)
|
||||
{
|
||||
if (!IsAvailable || jobs.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mediator.Publish(new HaltScanMessage(nameof(ConvertTextureFilesAsync)));
|
||||
|
||||
var totalJobs = jobs.Count;
|
||||
var completedJobs = 0;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
progress?.Report(new TextureConversionProgress(completedJobs, totalJobs, job));
|
||||
await ConvertSingleJobAsync(logger, job, token).ConfigureAwait(false);
|
||||
completedJobs++;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFilesAsync)));
|
||||
}
|
||||
|
||||
if (completedJobs > 0 && !token.IsCancellationRequested)
|
||||
{
|
||||
await DalamudUtil.RunOnFrameworkThread(async () =>
|
||||
{
|
||||
var player = await DalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false);
|
||||
if (player == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var gameObject = await DalamudUtil.CreateGameObjectAsync(player).ConfigureAwait(false);
|
||||
if (gameObject == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_redrawFeature.RequestImmediateRedraw(gameObject.ObjectIndex, RedrawType.Redraw);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token)
|
||||
{
|
||||
if (!IsAvailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ConvertSingleJobAsync(Logger, job, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ConvertSingleJobAsync(ILogger logger, TextureConversionJob job, CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
logger.LogInformation("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType);
|
||||
var convertTask = _convertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps);
|
||||
await convertTask.ConfigureAwait(false);
|
||||
|
||||
if (!convertTask.IsCompletedSuccessfully || job.DuplicateTargets is not { Count: > 0 })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var duplicate in job.DuplicateTargets)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogInformation("Synchronizing duplicate {Duplicate}", duplicate);
|
||||
File.Copy(job.OutputFile, duplicate, overwrite: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to copy duplicate {Duplicate}", duplicate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
}
|
||||
}
|
||||
21
LightlessSync/Interop/Ipc/TextureConversionJob.cs
Normal file
21
LightlessSync/Interop/Ipc/TextureConversionJob.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Penumbra.Api.Enums;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single texture conversion request, including optional duplicate targets.
|
||||
/// </summary>
|
||||
public sealed record TextureConversionJob(
|
||||
string InputFile,
|
||||
string OutputFile,
|
||||
TextureType TargetType,
|
||||
bool IncludeMipMaps = true,
|
||||
IReadOnlyList<string>? DuplicateTargets = null);
|
||||
|
||||
/// <summary>
|
||||
/// Progress payload for a texture conversion batch.
|
||||
/// </summary>
|
||||
/// <param name="Completed">Number of completed conversions.</param>
|
||||
/// <param name="Total">Total number of conversions scheduled.</param>
|
||||
/// <param name="CurrentJob">The job currently being processed.</param>
|
||||
public sealed record TextureConversionProgress(int Completed, int Total, TextureConversionJob CurrentJob);
|
||||
14
LightlessSync/LightlessConfiguration/ChatConfigService.cs
Normal file
14
LightlessSync/LightlessConfiguration/ChatConfigService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration;
|
||||
|
||||
public sealed class ChatConfigService : ConfigurationServiceBase<ChatConfig>
|
||||
{
|
||||
public const string ConfigName = "chatconfig.json";
|
||||
|
||||
public ChatConfigService(string configDir) : base(configDir)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ConfigurationName => ConfigName;
|
||||
}
|
||||
@@ -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,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
[Serializable]
|
||||
public sealed class ChatConfig : ILightlessConfiguration
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public bool AutoEnableChatOnLogin { get; set; } = false;
|
||||
public bool ShowRulesOverlayOnOpen { get; set; } = true;
|
||||
public bool ShowMessageTimestamps { get; set; } = true;
|
||||
public float ChatWindowOpacity { get; set; } = .97f;
|
||||
public bool FadeWhenUnfocused { get; set; } = false;
|
||||
public float UnfocusedWindowOpacity { get; set; } = 0.6f;
|
||||
public bool IsWindowPinned { get; set; } = false;
|
||||
public bool AutoOpenChatOnPluginLoad { get; set; } = false;
|
||||
public float ChatFontScale { get; set; } = 1.0f;
|
||||
public bool HideInCombat { get; set; } = false;
|
||||
public bool HideInDuty { get; set; } = false;
|
||||
public bool ShowWhenUiHidden { get; set; } = true;
|
||||
public bool ShowInCutscenes { get; set; } = true;
|
||||
public bool ShowInGpose { get; set; } = true;
|
||||
public List<string> ChannelOrder { get; set; } = new();
|
||||
public Dictionary<string, bool> PreferNotesForChannels { get; set; } = new(StringComparer.Ordinal);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
public enum LightfinderDtrDisplayMode
|
||||
{
|
||||
NearbyBroadcasts = 0,
|
||||
PendingPairRequests = 1,
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using Dalamud.Game.Text;
|
||||
using LightlessSync.UtilsEnum.Enum;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Models;
|
||||
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 VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical;
|
||||
public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical;
|
||||
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 ShowVisiblePairsGreenEye { get; set; } = false;
|
||||
public bool ShowTransferBars { get; set; } = true;
|
||||
public bool ShowTransferWindow { get; set; } = false;
|
||||
public bool ShowPlayerLinesTransferWindow { get; set; } = true;
|
||||
public bool ShowPlayerSpeedBarsTransferWindow { get; set; } = true;
|
||||
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 bool EnableDirectDownloads { 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;
|
||||
public NotificationLocation LightlessPerformanceNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||
|
||||
// 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; } = 30;
|
||||
public int PerformanceNotificationDurationSeconds { get; set; } = 20;
|
||||
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 PerformanceSoundId { get; set; } = 16; // Se15
|
||||
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 DisablePerformanceSound { get; set; } = true;
|
||||
public bool ShowPerformanceNotificationActions { get; set; } = true;
|
||||
public bool ShowPairRequestNotificationActions { 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;
|
||||
public string LastSeenVersion { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ public class PlayerPerformanceConfig : ILightlessConfiguration
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public bool ShowPerformanceIndicator { get; set; } = true;
|
||||
public bool ShowPerformanceUsageNextToName { get; set; } = false;
|
||||
public bool WarnOnExceedingThresholds { get; set; } = true;
|
||||
public bool WarnOnPreferredPermissionsExceedingThresholds { get; set; } = false;
|
||||
public int VRAMSizeWarningThresholdMiB { get; set; } = 375;
|
||||
@@ -13,4 +14,12 @@ 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;
|
||||
public bool EnableNonIndexTextureMipTrim { get; set; } = false;
|
||||
public bool EnableIndexTextureDownscale { get; set; } = false;
|
||||
public int TextureDownscaleMaxDimension { get; set; } = 2048;
|
||||
public bool OnlyDownscaleUncompressedTextures { get; set; } = true;
|
||||
public bool KeepOriginalTextureFiles { get; set; } = false;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -14,6 +13,8 @@ public class TransientConfig : ILightlessConfiguration
|
||||
public Dictionary<uint, List<string>> JobSpecificCache { get; set; } = [];
|
||||
public Dictionary<uint, List<string>> JobSpecificPetCache { get; set; } = [];
|
||||
|
||||
private readonly object _cacheLock = new();
|
||||
|
||||
public TransientPlayerConfig()
|
||||
{
|
||||
|
||||
@@ -40,45 +41,51 @@ public class TransientConfig : ILightlessConfiguration
|
||||
|
||||
public int RemovePath(string gamePath, ObjectKind objectKind)
|
||||
{
|
||||
int removedEntries = 0;
|
||||
if (objectKind == ObjectKind.Player)
|
||||
lock (_cacheLock)
|
||||
{
|
||||
if (GlobalPersistentCache.Remove(gamePath)) removedEntries++;
|
||||
foreach (var kvp in JobSpecificCache)
|
||||
int removedEntries = 0;
|
||||
if (objectKind == ObjectKind.Player)
|
||||
{
|
||||
if (kvp.Value.Remove(gamePath)) removedEntries++;
|
||||
if (GlobalPersistentCache.Remove(gamePath)) removedEntries++;
|
||||
foreach (var kvp in JobSpecificCache)
|
||||
{
|
||||
if (kvp.Value.Remove(gamePath)) removedEntries++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (objectKind == ObjectKind.Pet)
|
||||
{
|
||||
foreach (var kvp in JobSpecificPetCache)
|
||||
if (objectKind == ObjectKind.Pet)
|
||||
{
|
||||
if (kvp.Value.Remove(gamePath)) removedEntries++;
|
||||
foreach (var kvp in JobSpecificPetCache)
|
||||
{
|
||||
if (kvp.Value.Remove(gamePath)) removedEntries++;
|
||||
}
|
||||
}
|
||||
return removedEntries;
|
||||
}
|
||||
return removedEntries;
|
||||
}
|
||||
|
||||
public void AddOrElevate(uint jobId, string gamePath)
|
||||
{
|
||||
// check if it's in the global cache, if yes, do nothing
|
||||
if (GlobalPersistentCache.Contains(gamePath, StringComparer.Ordinal))
|
||||
lock (_cacheLock)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// check if it's in the global cache, if yes, do nothing
|
||||
if (GlobalPersistentCache.Contains(gamePath, StringComparer.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ElevateIfNeeded(jobId, gamePath)) return;
|
||||
if (ElevateIfNeeded(jobId, gamePath)) return;
|
||||
|
||||
// check if the jobid is already in the cache to start
|
||||
if (!JobSpecificCache.TryGetValue(jobId, out var jobCache))
|
||||
{
|
||||
JobSpecificCache[jobId] = jobCache = new();
|
||||
}
|
||||
// check if the jobid is already in the cache to start
|
||||
if (!JobSpecificCache.TryGetValue(jobId, out var jobCache))
|
||||
{
|
||||
JobSpecificCache[jobId] = jobCache = new();
|
||||
}
|
||||
|
||||
// check if the path is already in the job specific cache
|
||||
if (!jobCache.Contains(gamePath, StringComparer.Ordinal))
|
||||
{
|
||||
jobCache.Add(gamePath);
|
||||
// check if the path is already in the job specific cache
|
||||
if (!jobCache.Contains(gamePath, StringComparer.Ordinal))
|
||||
{
|
||||
jobCache.Add(gamePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,28 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Models;
|
||||
|
||||
public enum NotificationLocation
|
||||
{
|
||||
Nowhere,
|
||||
Chat,
|
||||
Toast,
|
||||
Both,
|
||||
LightlessUi,
|
||||
ChatAndLightlessUi,
|
||||
TextOverlay,
|
||||
}
|
||||
|
||||
public enum NotificationType
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
PairRequest,
|
||||
Download,
|
||||
Performance
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -13,5 +13,4 @@ public class ServerStorage
|
||||
public bool UseOAuth2 { get; set; } = false;
|
||||
public string? OAuthToken { get; set; } = null;
|
||||
public HttpTransportType HttpTransportType { get; set; } = HttpTransportType.WebSockets;
|
||||
public bool ForceWebSockets { get; set; } = false;
|
||||
}
|
||||
@@ -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,13 +1,15 @@
|
||||
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;
|
||||
using Serilog;
|
||||
using System.Reflection;
|
||||
|
||||
namespace LightlessSync;
|
||||
@@ -98,8 +100,9 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
|
||||
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut());
|
||||
|
||||
UIColors.Initialize(_lightlessConfigService);
|
||||
Mediator.StartQueueProcessing();
|
||||
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -113,6 +116,24 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void CheckVersion()
|
||||
{
|
||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
var currentVersion = ver == null ? string.Empty : $"{ver.Major}.{ver.Minor}.{ver.Build}";
|
||||
var lastSeen = _lightlessConfigService.Current.LastSeenVersion ?? string.Empty;
|
||||
Logger.LogInformation("Last seen version: {lastSeen}, current version: {currentVersion}", lastSeen, currentVersion);
|
||||
Logger.LogInformation("User has valid setup: {hasValidSetup}", _lightlessConfigService.Current.HasValidSetup());
|
||||
Logger.LogInformation("Server has valid config: {hasValidConfig}", _serverConfigurationManager.HasValidConfig());
|
||||
// Show update notes if version has changed and user has valid setup
|
||||
|
||||
if (!string.Equals(lastSeen, currentVersion, StringComparison.Ordinal) &&
|
||||
_lightlessConfigService.Current.HasValidSetup() &&
|
||||
_serverConfigurationManager.HasValidConfig())
|
||||
{
|
||||
Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi)));
|
||||
}
|
||||
}
|
||||
|
||||
private void DalamudUtilOnLogIn()
|
||||
{
|
||||
@@ -151,6 +172,8 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>();
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<VisibleUserDataDistributor>();
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<NameplateService>();
|
||||
CheckVersion();
|
||||
|
||||
#if !DEBUG
|
||||
if (_lightlessConfigService.Current.LogLevel != LogLevel.Information)
|
||||
@@ -1,16 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Dalamud.NET.Sdk/13.0.0">
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Dalamud.NET.Sdk/14.0.1">
|
||||
<PropertyGroup>
|
||||
<Authors></Authors>
|
||||
<Company></Company>
|
||||
<Version>1.11.2</Version>
|
||||
<Version>2.0.1</Version>
|
||||
<Description></Description>
|
||||
<Copyright></Copyright>
|
||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0-windows7.0</TargetFramework>
|
||||
<TargetFramework>net10.0-windows7.0</TargetFramework>
|
||||
<Platforms>x64</Platforms>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
@@ -27,25 +27,26 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DalamudPackager" Version="13.0.0" />
|
||||
<PackageReference Include="Blake3" Version="2.0.0" />
|
||||
<PackageReference Include="Brio.API" Version="3.0.1" />
|
||||
<PackageReference Include="Downloader" Version="4.0.3" />
|
||||
<PackageReference Include="K4os.Compression.LZ4.Legacy" Version="1.3.8" />
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="2.0.212">
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="2.0.264">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
|
||||
<PackageReference Include="Glamourer.Api" Version="2.6.0" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.2.2" />
|
||||
<PackageReference Include="Penumbra.String" Version="1.0.5" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.7.0.110445">
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
||||
<PackageReference Include="Glamourer.Api" Version="2.8.0" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.3.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.17.0.131074">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.7.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -64,6 +65,8 @@
|
||||
<None Update="images\icon.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<EmbeddedResource Include="Changelog\changelog.yaml" />
|
||||
<EmbeddedResource Include="Changelog\credits.yaml" />
|
||||
<EmbeddedResource Include="Localization\de.json" />
|
||||
<EmbeddedResource Include="Localization\fr.json" />
|
||||
</ItemGroup>
|
||||
@@ -74,7 +77,28 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj" />
|
||||
<ProjectReference Include="..\PenumbraAPI\Penumbra.Api.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
|
||||
<ProjectReference Include="..\ffxiv_pictomancy\Pictomancy\Pictomancy.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="OtterTex">
|
||||
<HintPath>lib\OtterTex.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="lib\DirectXTexC.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>DirectXTexC.dll</TargetPath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="DalamudPackager" Version="14.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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,29 +0,0 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Models.Obsolete;
|
||||
|
||||
[Serializable]
|
||||
[Obsolete("Deprecated, use ServerStorage")]
|
||||
public class ServerStorageV0
|
||||
{
|
||||
public List<Authentication> Authentications { get; set; } = [];
|
||||
public bool FullPause { get; set; } = false;
|
||||
public Dictionary<string, string> GidServerComments { get; set; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> OpenPairTags { get; set; } = new(StringComparer.Ordinal);
|
||||
public Dictionary<int, SecretKey> SecretKeys { get; set; } = [];
|
||||
public HashSet<string> ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal);
|
||||
public string ServerName { get; set; } = string.Empty;
|
||||
public string ServerUri { get; set; } = string.Empty;
|
||||
public Dictionary<string, string> UidServerComments { get; set; } = new(StringComparer.Ordinal);
|
||||
public Dictionary<string, List<string>> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal);
|
||||
|
||||
public ServerStorage ToV1()
|
||||
{
|
||||
return new ServerStorage()
|
||||
{
|
||||
ServerUri = ServerUri,
|
||||
ServerName = ServerName,
|
||||
Authentications = [.. Authentications],
|
||||
FullPause = FullPause,
|
||||
SecretKeys = SecretKeys.ToDictionary(p => p.Key, p => p.Value)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace LightlessSync.PlayerData.Data;
|
||||
|
||||
@@ -13,37 +16,42 @@ public class FileReplacementDataComparer : IEqualityComparer<FileReplacementData
|
||||
|
||||
public bool Equals(FileReplacementData? x, FileReplacementData? y)
|
||||
{
|
||||
if (x == null || y == null) return false;
|
||||
return x.Hash.Equals(y.Hash) && CompareHashSets(x.GamePaths.ToHashSet(StringComparer.Ordinal), y.GamePaths.ToHashSet(StringComparer.Ordinal)) && string.Equals(x.FileSwapPath, y.FileSwapPath, StringComparison.Ordinal);
|
||||
if (ReferenceEquals(x, y))
|
||||
return true;
|
||||
if (x is null || y is null)
|
||||
return false;
|
||||
|
||||
return string.Equals(x.Hash, y.Hash, StringComparison.OrdinalIgnoreCase)
|
||||
&& ComparePathSets(x.GamePaths, y.GamePaths)
|
||||
&& string.Equals(x.FileSwapPath ?? string.Empty, y.FileSwapPath ?? string.Empty, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public int GetHashCode(FileReplacementData obj)
|
||||
{
|
||||
return HashCode.Combine(obj.Hash.GetHashCode(StringComparison.OrdinalIgnoreCase), GetOrderIndependentHashCode(obj.GamePaths), StringComparer.Ordinal.GetHashCode(obj.FileSwapPath));
|
||||
if (obj is null)
|
||||
return 0;
|
||||
|
||||
var hash = StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Hash ?? string.Empty);
|
||||
hash = HashCode.Combine(hash, GetSetHashCode(obj.GamePaths));
|
||||
hash = HashCode.Combine(hash, StringComparer.OrdinalIgnoreCase.GetHashCode(obj.FileSwapPath ?? string.Empty));
|
||||
return hash;
|
||||
}
|
||||
|
||||
private static bool CompareHashSets(HashSet<string> list1, HashSet<string> list2)
|
||||
private static bool ComparePathSets(IEnumerable<string> first, IEnumerable<string> second)
|
||||
{
|
||||
if (list1.Count != list2.Count)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < list1.Count; i++)
|
||||
{
|
||||
if (!string.Equals(list1.ElementAt(i), list2.ElementAt(i), StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
var left = new HashSet<string>(first ?? Enumerable.Empty<string>(), StringComparer.OrdinalIgnoreCase);
|
||||
var right = new HashSet<string>(second ?? Enumerable.Empty<string>(), StringComparer.OrdinalIgnoreCase);
|
||||
return left.SetEquals(right);
|
||||
}
|
||||
|
||||
private static int GetOrderIndependentHashCode<T>(IEnumerable<T> source) where T : notnull
|
||||
private static int GetSetHashCode(IEnumerable<string> paths)
|
||||
{
|
||||
int hash = 0;
|
||||
foreach (T element in source)
|
||||
foreach (var element in paths ?? Enumerable.Empty<string>())
|
||||
{
|
||||
hash = unchecked(hash +
|
||||
EqualityComparer<T>.Default.GetHashCode(element));
|
||||
hash = unchecked(hash + StringComparer.OrdinalIgnoreCase.GetHashCode(element));
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.TextureCompression;
|
||||
using LightlessSync.WebAPI.Files;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -7,24 +9,45 @@ namespace LightlessSync.PlayerData.Factories;
|
||||
|
||||
public class FileDownloadManagerFactory
|
||||
{
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly FileCompactor _fileCompactor;
|
||||
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly FileCompactor _fileCompactor;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly TextureDownscaleService _textureDownscaleService;
|
||||
private readonly TextureMetadataHelper _textureMetadataHelper;
|
||||
|
||||
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, LightlessMediator lightlessMediator, FileTransferOrchestrator fileTransferOrchestrator,
|
||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor)
|
||||
public FileDownloadManagerFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
LightlessMediator lightlessMediator,
|
||||
FileTransferOrchestrator fileTransferOrchestrator,
|
||||
FileCacheManager fileCacheManager,
|
||||
FileCompactor fileCompactor,
|
||||
LightlessConfigService configService,
|
||||
TextureDownscaleService textureDownscaleService,
|
||||
TextureMetadataHelper textureMetadataHelper)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_fileTransferOrchestrator = fileTransferOrchestrator;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_fileCompactor = fileCompactor;
|
||||
_configService = configService;
|
||||
_textureDownscaleService = textureDownscaleService;
|
||||
_textureMetadataHelper = textureMetadataHelper;
|
||||
}
|
||||
|
||||
public FileDownloadManager Create()
|
||||
{
|
||||
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _lightlessMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor);
|
||||
return new FileDownloadManager(
|
||||
_loggerFactory.CreateLogger<FileDownloadManager>(),
|
||||
_lightlessMediator,
|
||||
_fileTransferOrchestrator,
|
||||
_fileCacheManager,
|
||||
_fileCompactor,
|
||||
_configService,
|
||||
_textureDownscaleService,
|
||||
_textureMetadataHelper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,29 +2,40 @@
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Factories;
|
||||
|
||||
public class GameObjectHandlerFactory
|
||||
{
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||
|
||||
public GameObjectHandlerFactory(ILoggerFactory loggerFactory, PerformanceCollectorService performanceCollectorService, LightlessMediator lightlessMediator,
|
||||
DalamudUtilService dalamudUtilService)
|
||||
public GameObjectHandlerFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
PerformanceCollectorService performanceCollectorService,
|
||||
LightlessMediator lightlessMediator,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_performanceCollectorService = performanceCollectorService;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task<GameObjectHandler> Create(ObjectKind objectKind, Func<nint> getAddressFunc, bool isWatched = false)
|
||||
{
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => new GameObjectHandler(_loggerFactory.CreateLogger<GameObjectHandler>(),
|
||||
_performanceCollectorService, _lightlessMediator, _dalamudUtilService, objectKind, getAddressFunc, isWatched)).ConfigureAwait(false);
|
||||
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
|
||||
return await dalamudUtilService.RunOnFrameworkThread(() => new GameObjectHandler(
|
||||
_loggerFactory.CreateLogger<GameObjectHandler>(),
|
||||
_performanceCollectorService,
|
||||
_lightlessMediator,
|
||||
dalamudUtilService,
|
||||
objectKind,
|
||||
getAddressFunc,
|
||||
isWatched)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,83 @@
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LightlessSync.WebAPI;
|
||||
|
||||
namespace LightlessSync.PlayerData.Factories;
|
||||
|
||||
public class PairFactory
|
||||
{
|
||||
private readonly PairHandlerFactory _cachedPlayerFactory;
|
||||
private readonly PairLedger _pairLedger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly Lazy<ServerConfigurationManager> _serverConfigurationManager;
|
||||
private readonly Lazy<ApiController> _apiController;
|
||||
|
||||
public PairFactory(ILoggerFactory loggerFactory, PairHandlerFactory cachedPlayerFactory,
|
||||
LightlessMediator lightlessMediator, ServerConfigurationManager serverConfigurationManager)
|
||||
public PairFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
PairLedger pairLedger,
|
||||
LightlessMediator lightlessMediator,
|
||||
Lazy<ServerConfigurationManager> serverConfigurationManager,
|
||||
Lazy<ApiController> apiController)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_cachedPlayerFactory = cachedPlayerFactory;
|
||||
_pairLedger = pairLedger;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_apiController = apiController;
|
||||
}
|
||||
|
||||
public Pair Create(UserFullPairDto userPairDto)
|
||||
{
|
||||
return new Pair(_loggerFactory.CreateLogger<Pair>(), userPairDto, _cachedPlayerFactory, _lightlessMediator, _serverConfigurationManager);
|
||||
return CreateInternal(userPairDto);
|
||||
}
|
||||
|
||||
public Pair Create(UserPairDto userPairDto)
|
||||
{
|
||||
return new Pair(_loggerFactory.CreateLogger<Pair>(), new(userPairDto.User, userPairDto.IndividualPairStatus, [], userPairDto.OwnPermissions, userPairDto.OtherPermissions),
|
||||
_cachedPlayerFactory, _lightlessMediator, _serverConfigurationManager);
|
||||
var full = new UserFullPairDto(
|
||||
userPairDto.User,
|
||||
userPairDto.IndividualPairStatus,
|
||||
new List<string>(),
|
||||
userPairDto.OwnPermissions,
|
||||
userPairDto.OtherPermissions);
|
||||
|
||||
return CreateInternal(full);
|
||||
}
|
||||
}
|
||||
|
||||
public Pair? Create(PairDisplayEntry entry)
|
||||
{
|
||||
var dto = new UserFullPairDto(
|
||||
entry.User,
|
||||
entry.PairStatus ?? IndividualPairStatus.None,
|
||||
entry.Groups.Select(g => g.Group.GID).Distinct(StringComparer.Ordinal).ToList(),
|
||||
entry.SelfPermissions,
|
||||
entry.OtherPermissions);
|
||||
|
||||
return CreateInternal(dto);
|
||||
}
|
||||
|
||||
public Pair? Create(PairUniqueIdentifier ident)
|
||||
{
|
||||
if (!_pairLedger.TryGetEntry(ident, out var entry) || entry is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Create(entry);
|
||||
}
|
||||
|
||||
private Pair CreateInternal(UserFullPairDto dto)
|
||||
{
|
||||
return new Pair(
|
||||
_loggerFactory.CreateLogger<Pair>(),
|
||||
dto,
|
||||
_pairLedger,
|
||||
_lightlessMediator,
|
||||
_serverConfigurationManager.Value,
|
||||
_apiController);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Factories;
|
||||
|
||||
public class PairHandlerFactory
|
||||
{
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly FileDownloadManagerFactory _fileDownloadManagerFactory;
|
||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||
private readonly ServerConfigurationManager _serverConfigManager;
|
||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||
|
||||
public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager,
|
||||
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
||||
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
||||
FileCacheManager fileCacheManager, LightlessMediator lightlessMediator, PlayerPerformanceService playerPerformanceService,
|
||||
ServerConfigurationManager serverConfigManager)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||
_ipcManager = ipcManager;
|
||||
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||
_hostApplicationLifetime = hostApplicationLifetime;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
_serverConfigManager = serverConfigManager;
|
||||
}
|
||||
|
||||
public PairHandler Create(Pair pair)
|
||||
{
|
||||
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _gameObjectHandlerFactory,
|
||||
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
||||
_fileCacheManager, _lightlessMediator, _playerPerformanceService, _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)
|
||||
@@ -108,6 +119,7 @@ public class PlayerDataFactory
|
||||
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
|
||||
|
||||
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
|
||||
var logDebug = _logger.IsEnabled(LogLevel.Debug);
|
||||
|
||||
// wait until chara is not drawing and present so nothing spontaneously explodes
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
|
||||
@@ -121,11 +133,6 @@ public class PlayerDataFactory
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
Dictionary<string, List<ushort>>? boneIndices =
|
||||
objectKind != ObjectKind.Player
|
||||
? null
|
||||
: await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
|
||||
|
||||
DateTime start = DateTime.UtcNow;
|
||||
|
||||
// penumbra call, it's currently broken
|
||||
@@ -143,11 +150,21 @@ public class PlayerDataFactory
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
_logger.LogDebug("== Static Replacements ==");
|
||||
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
||||
if (logDebug)
|
||||
{
|
||||
_logger.LogDebug("=> {repl}", replacement);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
_logger.LogDebug("== Static Replacements ==");
|
||||
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("=> {repl}", replacement);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement))
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
|
||||
await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
|
||||
@@ -179,11 +196,21 @@ public class PlayerDataFactory
|
||||
var transientPaths = ManageSemiTransientData(objectKind);
|
||||
var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("== Transient Replacements ==");
|
||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
|
||||
if (logDebug)
|
||||
{
|
||||
_logger.LogDebug("=> {repl}", replacement);
|
||||
fragment.FileReplacements.Add(replacement);
|
||||
_logger.LogDebug("== Transient Replacements ==");
|
||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
|
||||
{
|
||||
_logger.LogDebug("=> {repl}", replacement);
|
||||
fragment.FileReplacements.Add(replacement);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)))
|
||||
{
|
||||
fragment.FileReplacements.Add(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
|
||||
@@ -241,11 +268,26 @@ public class PlayerDataFactory
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
Dictionary<string, List<ushort>>? boneIndices = null;
|
||||
var hasPapFiles = false;
|
||||
if (objectKind == ObjectKind.Player)
|
||||
{
|
||||
hasPapFiles = fragment.FileReplacements.Any(f =>
|
||||
!f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase));
|
||||
if (hasPapFiles)
|
||||
{
|
||||
boneIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (objectKind == ObjectKind.Player)
|
||||
{
|
||||
try
|
||||
{
|
||||
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
|
||||
if (hasPapFiles)
|
||||
{
|
||||
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
@@ -267,12 +309,16 @@ public class PlayerDataFactory
|
||||
{
|
||||
if (boneIndices == null) return;
|
||||
|
||||
foreach (var kvp in boneIndices)
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
|
||||
foreach (var kvp in boneIndices)
|
||||
{
|
||||
_logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
|
||||
}
|
||||
}
|
||||
|
||||
if (boneIndices.All(u => u.Value.Count == 0)) return;
|
||||
var maxPlayerBoneIndex = boneIndices.SelectMany(kvp => kvp.Value).DefaultIfEmpty().Max();
|
||||
if (maxPlayerBoneIndex <= 0) return;
|
||||
|
||||
int noValidationFailed = 0;
|
||||
foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
|
||||
@@ -292,12 +338,13 @@ public class PlayerDataFactory
|
||||
|
||||
_logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
|
||||
|
||||
foreach (var boneCount in skeletonIndices.Select(k => k).ToList())
|
||||
foreach (var boneCount in skeletonIndices)
|
||||
{
|
||||
if (boneCount.Value.Max() > boneIndices.SelectMany(b => b.Value).Max())
|
||||
var maxAnimationIndex = boneCount.Value.DefaultIfEmpty().Max();
|
||||
if (maxAnimationIndex > maxPlayerBoneIndex)
|
||||
{
|
||||
_logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})",
|
||||
file.ResolvedPath, boneCount.Key, boneCount.Value.Max(), boneIndices.SelectMany(b => b.Value).Max());
|
||||
file.ResolvedPath, boneCount.Key, maxAnimationIndex, maxPlayerBoneIndex);
|
||||
validationFailed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
|
||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
|
||||
namespace LightlessSync.PlayerData.Handlers;
|
||||
@@ -15,6 +16,8 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
private readonly Func<IntPtr> _getAddress;
|
||||
private readonly bool _isOwnedObject;
|
||||
private readonly PerformanceCollectorService _performanceCollector;
|
||||
private readonly object _frameworkUpdateGate = new();
|
||||
private bool _frameworkUpdateSubscribed;
|
||||
private byte _classJob = 0;
|
||||
private Task? _delayedZoningTask;
|
||||
private bool _haltProcessing = false;
|
||||
@@ -46,7 +49,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
});
|
||||
}
|
||||
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
|
||||
if (_isOwnedObject)
|
||||
{
|
||||
EnableFrameworkUpdates();
|
||||
}
|
||||
|
||||
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
|
||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
|
||||
@@ -94,6 +100,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
public DrawCondition CurrentDrawCondition { get; set; } = DrawCondition.None;
|
||||
public byte Gender { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public uint EntityId { get; private set; } = uint.MaxValue;
|
||||
public ObjectKind ObjectKind { get; }
|
||||
public byte RaceId { get; private set; }
|
||||
public byte TribeId { get; private set; }
|
||||
@@ -107,7 +114,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
{
|
||||
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
if (_haltProcessing) CheckAndUpdateObject();
|
||||
EnsureLatestObjectState();
|
||||
if (CurrentDrawCondition != DrawCondition.None) return true;
|
||||
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
||||
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
||||
@@ -142,9 +149,15 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
{
|
||||
Address = IntPtr.Zero;
|
||||
DrawObjectAddress = IntPtr.Zero;
|
||||
EntityId = uint.MaxValue;
|
||||
_haltProcessing = false;
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
||||
{
|
||||
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
||||
@@ -171,13 +184,16 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
Address = _getAddress();
|
||||
if (Address != IntPtr.Zero)
|
||||
{
|
||||
var drawObjAddr = (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->DrawObject;
|
||||
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
|
||||
var drawObjAddr = (IntPtr)gameObject->DrawObject;
|
||||
DrawObjectAddress = drawObjAddr;
|
||||
EntityId = gameObject->EntityId;
|
||||
CurrentDrawCondition = DrawCondition.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawObjectAddress = IntPtr.Zero;
|
||||
EntityId = uint.MaxValue;
|
||||
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
||||
}
|
||||
|
||||
@@ -355,7 +371,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
|
||||
private bool IsBeingDrawn()
|
||||
{
|
||||
if (_haltProcessing) CheckAndUpdateObject();
|
||||
EnsureLatestObjectState();
|
||||
|
||||
if (_dalamudUtil.IsAnythingDrawing)
|
||||
{
|
||||
@@ -367,12 +383,34 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
return CurrentDrawCondition != DrawCondition.None;
|
||||
}
|
||||
|
||||
private void EnsureLatestObjectState()
|
||||
{
|
||||
if (_haltProcessing || !_frameworkUpdateSubscribed)
|
||||
{
|
||||
CheckAndUpdateObject();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnableFrameworkUpdates()
|
||||
{
|
||||
lock (_frameworkUpdateGate)
|
||||
{
|
||||
if (_frameworkUpdateSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
||||
_frameworkUpdateSubscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe DrawCondition IsBeingDrawnUnsafe()
|
||||
{
|
||||
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
||||
if (DrawObjectAddress == IntPtr.Zero) return DrawCondition.DrawObjectZero;
|
||||
var renderFlags = (((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags) != 0x0;
|
||||
if (renderFlags) return DrawCondition.RenderFlags;
|
||||
var visibilityFlags = ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags;
|
||||
if (visibilityFlags != VisibilityFlags.None) return DrawCondition.RenderFlags;
|
||||
|
||||
if (ObjectKind == ObjectKind.Player)
|
||||
{
|
||||
|
||||
@@ -1,718 +0,0 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Events;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI.Files;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
|
||||
namespace LightlessSync.PlayerData.Handlers;
|
||||
|
||||
public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced);
|
||||
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly FileDownloadManager _downloadManager;
|
||||
private readonly FileCacheManager _fileDbManager;
|
||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly IHostApplicationLifetime _lifetime;
|
||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||
private readonly ServerConfigurationManager _serverConfigManager;
|
||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
||||
private Guid _applicationId;
|
||||
private Task? _applicationTask;
|
||||
private CharacterData? _cachedData = null;
|
||||
private GameObjectHandler? _charaHandler;
|
||||
private readonly Dictionary<ObjectKind, Guid?> _customizeIds = [];
|
||||
private CombatData? _dataReceivedInDowntime;
|
||||
private CancellationTokenSource? _downloadCancellationTokenSource = new();
|
||||
private bool _forceApplyMods = false;
|
||||
private bool _isVisible;
|
||||
private Guid _penumbraCollection;
|
||||
private bool _redrawOnNextApplication = false;
|
||||
|
||||
public PairHandler(ILogger<PairHandler> logger, Pair pair,
|
||||
GameObjectHandlerFactory gameObjectHandlerFactory,
|
||||
IpcManager ipcManager, FileDownloadManager transferManager,
|
||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
||||
FileCacheManager fileDbManager, LightlessMediator mediator,
|
||||
PlayerPerformanceService playerPerformanceService,
|
||||
ServerConfigurationManager serverConfigManager) : base(logger, mediator)
|
||||
{
|
||||
Pair = pair;
|
||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||
_ipcManager = ipcManager;
|
||||
_downloadManager = transferManager;
|
||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_lifetime = lifetime;
|
||||
_fileDbManager = fileDbManager;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
_serverConfigManager = serverConfigManager;
|
||||
_penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, Pair.UserData.UID).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
|
||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) =>
|
||||
{
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_charaHandler?.Invalidate();
|
||||
IsVisible = false;
|
||||
});
|
||||
Mediator.Subscribe<PenumbraInitializedMessage>(this, (_) =>
|
||||
{
|
||||
_penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, Pair.UserData.UID).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (!IsVisible && _charaHandler != null)
|
||||
{
|
||||
PlayerName = string.Empty;
|
||||
_charaHandler.Dispose();
|
||||
_charaHandler = null;
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<ClassJobChangedMessage>(this, (msg) =>
|
||||
{
|
||||
if (msg.GameObjectHandler == _charaHandler)
|
||||
{
|
||||
_redrawOnNextApplication = true;
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<CombatOrPerformanceEndMessage>(this, (msg) =>
|
||||
{
|
||||
if (IsVisible && _dataReceivedInDowntime != null)
|
||||
{
|
||||
ApplyCharacterData(_dataReceivedInDowntime.ApplicationId,
|
||||
_dataReceivedInDowntime.CharacterData, _dataReceivedInDowntime.Forced);
|
||||
_dataReceivedInDowntime = null;
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<CombatOrPerformanceStartMessage>(this, _ =>
|
||||
{
|
||||
_dataReceivedInDowntime = null;
|
||||
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate();
|
||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate();
|
||||
});
|
||||
|
||||
LastAppliedDataBytes = -1;
|
||||
}
|
||||
|
||||
public bool IsVisible
|
||||
{
|
||||
get => _isVisible;
|
||||
private set
|
||||
{
|
||||
if (_isVisible != value)
|
||||
{
|
||||
_isVisible = value;
|
||||
string text = "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible");
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler),
|
||||
EventSeverity.Informational, text)));
|
||||
Mediator.Publish(new RefreshUiMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long LastAppliedDataBytes { get; private set; }
|
||||
public Pair Pair { get; private set; }
|
||||
public nint PlayerCharacter => _charaHandler?.Address ?? nint.Zero;
|
||||
public unsafe uint PlayerCharacterId => (_charaHandler?.Address ?? nint.Zero) == nint.Zero
|
||||
? uint.MaxValue
|
||||
: ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_charaHandler!.Address)->EntityId;
|
||||
public string? PlayerName { get; private set; }
|
||||
public string PlayerNameHash => Pair.Ident;
|
||||
|
||||
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
||||
{
|
||||
if (_dalamudUtil.IsInCombatOrPerforming)
|
||||
{
|
||||
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);
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(isUploading: false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_charaHandler == null || (PlayerCharacter == IntPtr.Zero))
|
||||
{
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||
"Cannot apply character data: Receiving Player is in an invalid state, deferring application")));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player was in invalid state, charaHandlerIsNull: {charaIsNull}, playerPointerIsNull: {ptrIsNull}",
|
||||
applicationBase, _charaHandler == null, PlayerCharacter == IntPtr.Zero);
|
||||
var hasDiffMods = characterData.CheckUpdatedData(applicationBase, _cachedData, Logger,
|
||||
this, forceApplyCustomization, forceApplyMods: false)
|
||||
.Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles));
|
||||
_forceApplyMods = hasDiffMods || _forceApplyMods || (PlayerCharacter == IntPtr.Zero && _cachedData == null);
|
||||
_cachedData = characterData;
|
||||
Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods);
|
||||
return;
|
||||
}
|
||||
|
||||
SetUploading(isUploading: false);
|
||||
|
||||
Logger.LogDebug("[BASE-{appbase}] Applying data for {player}, forceApplyCustomization: {forced}, forceApplyMods: {forceMods}", applicationBase, this, forceApplyCustomization, _forceApplyMods);
|
||||
Logger.LogDebug("[BASE-{appbase}] Hash for data is {newHash}, current cache hash is {oldHash}", applicationBase, characterData.DataHash.Value, _cachedData?.DataHash.Value ?? "NODATA");
|
||||
|
||||
if (string.Equals(characterData.DataHash.Value, _cachedData?.DataHash.Value ?? string.Empty, StringComparison.Ordinal) && !forceApplyCustomization) return;
|
||||
|
||||
if (_dalamudUtil.IsInCutscene || _dalamudUtil.IsInGpose || !_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
|
||||
{
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||
"Cannot apply character data: you are in GPose, a Cutscene or Penumbra/Glamourer is not available")));
|
||||
Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while in cutscene/gpose or Penumbra/Glamourer unavailable, returning", applicationBase, this);
|
||||
return;
|
||||
}
|
||||
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational,
|
||||
"Applying Character Data")));
|
||||
|
||||
_forceApplyMods |= forceApplyCustomization;
|
||||
|
||||
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this, forceApplyCustomization, _forceApplyMods);
|
||||
|
||||
if (_charaHandler != null && _forceApplyMods)
|
||||
{
|
||||
_forceApplyMods = false;
|
||||
}
|
||||
|
||||
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
|
||||
{
|
||||
player.Add(PlayerChanges.ForcedRedraw);
|
||||
_redrawOnNextApplication = false;
|
||||
}
|
||||
|
||||
if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges))
|
||||
{
|
||||
_pluginWarningNotificationManager.NotifyForMissingPlugins(Pair.UserData, PlayerName!, playerChanges);
|
||||
}
|
||||
|
||||
Logger.LogDebug("[BASE-{appbase}] Downloading and applying character for {name}", applicationBase, this);
|
||||
|
||||
DownloadAndApplyCharacter(applicationBase, characterData.DeepClone(), charaDataToUpdate);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Pair == null
|
||||
? base.ToString() ?? string.Empty
|
||||
: Pair.UserData.AliasOrUID + ":" + PlayerName + ":" + (PlayerCharacter != nint.Zero ? "HasChar" : "NoChar");
|
||||
}
|
||||
|
||||
internal void SetUploading(bool isUploading = true)
|
||||
{
|
||||
Logger.LogTrace("Setting {this} uploading {uploading}", this, isUploading);
|
||||
if (_charaHandler != null)
|
||||
{
|
||||
Mediator.Publish(new PlayerUploadingMessage(_charaHandler, isUploading));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
SetUploading(isUploading: false);
|
||||
var name = PlayerName;
|
||||
Logger.LogDebug("Disposing {name} ({user})", name, Pair);
|
||||
try
|
||||
{
|
||||
Guid applicationId = Guid.NewGuid();
|
||||
_applicationCancellationTokenSource?.CancelDispose();
|
||||
_applicationCancellationTokenSource = null;
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_downloadCancellationTokenSource = null;
|
||||
_downloadManager.Dispose();
|
||||
_charaHandler?.Dispose();
|
||||
_charaHandler = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
Mediator.Publish(new EventMessage(new Event(name, Pair.UserData, nameof(PairHandler), EventSeverity.Informational, "Disposing User")));
|
||||
}
|
||||
|
||||
if (_lifetime.ApplicationStopping.IsCancellationRequested) return;
|
||||
|
||||
if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name))
|
||||
{
|
||||
Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, Pair.UserPair);
|
||||
Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, Pair.UserPair);
|
||||
_ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, _penumbraCollection).GetAwaiter().GetResult();
|
||||
if (!IsVisible)
|
||||
{
|
||||
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, Pair.UserPair);
|
||||
_ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).GetAwaiter().GetResult();
|
||||
}
|
||||
else
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(60));
|
||||
|
||||
Logger.LogInformation("[{applicationId}] CachedData is null {isNull}, contains things: {contains}", applicationId, _cachedData == null, _cachedData?.FileReplacements.Any() ?? false);
|
||||
|
||||
foreach (KeyValuePair<ObjectKind, List<FileReplacementData>> item in _cachedData?.FileReplacements ?? [])
|
||||
{
|
||||
try
|
||||
{
|
||||
RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed disposing player (not present anymore?)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Error on disposal of {name}", name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PlayerName = null;
|
||||
_cachedData = null;
|
||||
Logger.LogDebug("Disposing {name} complete", name);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyCustomizationDataAsync(Guid applicationId, KeyValuePair<ObjectKind, HashSet<PlayerChanges>> changes, CharacterData charaData, CancellationToken token)
|
||||
{
|
||||
if (PlayerCharacter == nint.Zero) return;
|
||||
var ptr = PlayerCharacter;
|
||||
|
||||
var handler = changes.Key switch
|
||||
{
|
||||
ObjectKind.Player => _charaHandler!,
|
||||
ObjectKind.Companion => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetCompanionPtr(ptr), isWatched: false).ConfigureAwait(false),
|
||||
ObjectKind.MinionOrMount => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetMinionOrMountPtr(ptr), isWatched: false).ConfigureAwait(false),
|
||||
ObjectKind.Pet => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetPetPtr(ptr), isWatched: false).ConfigureAwait(false),
|
||||
_ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
if (handler.Address == nint.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
foreach (var change in changes.Value.OrderBy(p => (int)p))
|
||||
{
|
||||
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
|
||||
switch (change)
|
||||
{
|
||||
case PlayerChanges.Customize:
|
||||
if (charaData.CustomizePlusData.TryGetValue(changes.Key, out var customizePlusData))
|
||||
{
|
||||
_customizeIds[changes.Key] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(handler.Address, customizePlusData).ConfigureAwait(false);
|
||||
}
|
||||
else if (_customizeIds.TryGetValue(changes.Key, out var customizeId))
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
_customizeIds.Remove(changes.Key);
|
||||
}
|
||||
break;
|
||||
|
||||
case PlayerChanges.Heels:
|
||||
await _ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.Honorific:
|
||||
await _ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.Glamourer:
|
||||
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
|
||||
{
|
||||
await _ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token).ConfigureAwait(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case PlayerChanges.Moodles:
|
||||
await _ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.PetNames:
|
||||
await _ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.ForcedRedraw:
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (handler != _charaHandler) handler.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void DownloadAndApplyCharacter(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData)
|
||||
{
|
||||
if (!updatedData.Any())
|
||||
{
|
||||
Logger.LogDebug("[BASE-{appBase}] Nothing to update for {obj}", applicationBase, this);
|
||||
return;
|
||||
}
|
||||
|
||||
var updateModdedPaths = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModFiles));
|
||||
var updateManip = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModManip));
|
||||
|
||||
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
|
||||
var downloadToken = _downloadCancellationTokenSource.Token;
|
||||
|
||||
_ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, downloadToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Task? _pairDownloadTask;
|
||||
|
||||
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
||||
bool updateModdedPaths, bool updateManip, CancellationToken downloadToken)
|
||||
{
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
|
||||
|
||||
if (updateModdedPaths)
|
||||
{
|
||||
int attempts = 0;
|
||||
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||
|
||||
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
|
||||
{
|
||||
if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted)
|
||||
{
|
||||
Logger.LogDebug("[BASE-{appBase}] Finishing prior running download task for player {name}, {kind}", applicationBase, PlayerName, updatedData);
|
||||
await _pairDownloadTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData);
|
||||
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational,
|
||||
$"Starting download for {toDownloadReplacements.Count} files")));
|
||||
var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false);
|
||||
|
||||
if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles))
|
||||
{
|
||||
_downloadManager.ClearDownload();
|
||||
return;
|
||||
}
|
||||
|
||||
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false));
|
||||
|
||||
await _pairDownloadTask.ConfigureAwait(false);
|
||||
|
||||
if (downloadToken.IsCancellationRequested)
|
||||
{
|
||||
Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase);
|
||||
return;
|
||||
}
|
||||
|
||||
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||
|
||||
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false))
|
||||
return;
|
||||
}
|
||||
|
||||
downloadToken.ThrowIfCancellationRequested();
|
||||
|
||||
var appToken = _applicationCancellationTokenSource?.Token;
|
||||
while ((!_applicationTask?.IsCompleted ?? false)
|
||||
&& !downloadToken.IsCancellationRequested
|
||||
&& (!appToken?.IsCancellationRequested ?? false))
|
||||
{
|
||||
// block until current application is done
|
||||
Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName);
|
||||
await Task.Delay(250).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false)) return;
|
||||
|
||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
||||
var token = _applicationCancellationTokenSource.Token;
|
||||
|
||||
_applicationTask = ApplyCharacterDataAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token);
|
||||
}
|
||||
|
||||
private async Task ApplyCharacterDataAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip,
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
_applicationId = Guid.NewGuid();
|
||||
Logger.LogDebug("[BASE-{applicationId}] Starting application task for {this}: {appId}", applicationBase, this, _applicationId);
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, _charaHandler);
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, _charaHandler!, _applicationId, 30000, token).ConfigureAwait(false);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (updateModdedPaths)
|
||||
{
|
||||
// ensure collection is set
|
||||
var objIndex = await _dalamudUtil.RunOnFrameworkThread(() => _charaHandler!.GetGameObject()!.ObjectIndex).ConfigureAwait(false);
|
||||
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, objIndex).ConfigureAwait(false);
|
||||
|
||||
await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, _penumbraCollection,
|
||||
moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false);
|
||||
LastAppliedDataBytes = -1;
|
||||
foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists))
|
||||
{
|
||||
if (LastAppliedDataBytes == -1) LastAppliedDataBytes = 0;
|
||||
|
||||
LastAppliedDataBytes += path.Length;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateManip)
|
||||
{
|
||||
await _ipcManager.Penumbra.SetManipulationDataAsync(Logger, _applicationId, _penumbraCollection, charaData.ManipulationData).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
foreach (var kind in updatedData)
|
||||
{
|
||||
await ApplyCustomizationDataAsync(_applicationId, kind, charaData, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
_cachedData = charaData;
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException))
|
||||
{
|
||||
IsVisible = false;
|
||||
_forceApplyMods = true;
|
||||
_cachedData = charaData;
|
||||
Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FrameworkUpdate()
|
||||
{
|
||||
if (string.IsNullOrEmpty(PlayerName))
|
||||
{
|
||||
var pc = _dalamudUtil.FindPlayerByNameHash(Pair.Ident);
|
||||
if (pc == default((string, nint))) return;
|
||||
Logger.LogDebug("One-Time Initializing {this}", this);
|
||||
Initialize(pc.Name);
|
||||
Logger.LogDebug("One-Time Initialized {this}", this);
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational,
|
||||
$"Initializing User For Character {pc.Name}")));
|
||||
}
|
||||
|
||||
if (_charaHandler?.Address != nint.Zero && !IsVisible)
|
||||
{
|
||||
Guid appData = Guid.NewGuid();
|
||||
IsVisible = true;
|
||||
if (_cachedData != null)
|
||||
{
|
||||
Logger.LogTrace("[BASE-{appBase}] {this} visibility changed, now: {visi}, cached data exists", appData, this, IsVisible);
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
ApplyCharacterData(appData, _cachedData!, forceApplyCustomization: true);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogTrace("{this} visibility changed, now: {visi}, no cached data exists", this, IsVisible);
|
||||
}
|
||||
}
|
||||
else if (_charaHandler?.Address == nint.Zero && IsVisible)
|
||||
{
|
||||
IsVisible = false;
|
||||
_charaHandler.Invalidate();
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_downloadCancellationTokenSource = null;
|
||||
Logger.LogTrace("{this} visibility changed, now: {visi}", this, IsVisible);
|
||||
}
|
||||
}
|
||||
|
||||
private void Initialize(string name)
|
||||
{
|
||||
PlayerName = name;
|
||||
_charaHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Pair.Ident), isWatched: false).GetAwaiter().GetResult();
|
||||
|
||||
_serverConfigManager.AutoPopulateNoteForUid(Pair.UserData.UID, name);
|
||||
|
||||
Mediator.Subscribe<HonorificReadyMessage>(this, async (_) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(_cachedData?.HonorificData)) return;
|
||||
Logger.LogTrace("Reapplying Honorific data for {this}", this);
|
||||
await _ipcManager.Honorific.SetTitleAsync(PlayerCharacter, _cachedData.HonorificData).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
Mediator.Subscribe<PetNamesReadyMessage>(this, async (_) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(_cachedData?.PetNamesData)) return;
|
||||
Logger.LogTrace("Reapplying Pet Names data for {this}", this);
|
||||
await _ipcManager.PetNames.SetPlayerData(PlayerCharacter, _cachedData.PetNamesData).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
_ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, _charaHandler.GetGameObject()!.ObjectIndex).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private async Task RevertCustomizationDataAsync(ObjectKind objectKind, string name, Guid applicationId, CancellationToken cancelToken)
|
||||
{
|
||||
nint address = _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Pair.Ident);
|
||||
if (address == nint.Zero) return;
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Reverting all Customization for {alias}/{name} {objectKind}", applicationId, Pair.UserData.AliasOrUID, name, objectKind);
|
||||
|
||||
if (_customizeIds.TryGetValue(objectKind, out var customizeId))
|
||||
{
|
||||
_customizeIds.Remove(objectKind);
|
||||
}
|
||||
|
||||
if (objectKind == ObjectKind.Player)
|
||||
{
|
||||
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, () => address, isWatched: false).ConfigureAwait(false);
|
||||
tempHandler.CompareNameAndThrow(name);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Customization and Equipment for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
tempHandler.CompareNameAndThrow(name);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Heels for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.Heels.RestoreOffsetForPlayerAsync(address).ConfigureAwait(false);
|
||||
tempHandler.CompareNameAndThrow(name);
|
||||
Logger.LogDebug("[{applicationId}] Restoring C+ for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
tempHandler.CompareNameAndThrow(name);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Honorific for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.Honorific.ClearTitleAsync(address).ConfigureAwait(false);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Moodles for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.Moodles.RevertStatusAsync(address).ConfigureAwait(false);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Pet Nicknames for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.PetNames.ClearPlayerData(address).ConfigureAwait(false);
|
||||
}
|
||||
else if (objectKind == ObjectKind.MinionOrMount)
|
||||
{
|
||||
var minionOrMount = await _dalamudUtil.GetMinionOrMountAsync(address).ConfigureAwait(false);
|
||||
if (minionOrMount != nint.Zero)
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => minionOrMount, isWatched: false).ConfigureAwait(false);
|
||||
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (objectKind == ObjectKind.Pet)
|
||||
{
|
||||
var pet = await _dalamudUtil.GetPetAsync(address).ConfigureAwait(false);
|
||||
if (pet != nint.Zero)
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => pet, isWatched: false).ConfigureAwait(false);
|
||||
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (objectKind == ObjectKind.Companion)
|
||||
{
|
||||
var companion = await _dalamudUtil.GetCompanionAsync(address).ConfigureAwait(false);
|
||||
if (companion != nint.Zero)
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => companion, isWatched: false).ConfigureAwait(false);
|
||||
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<FileReplacementData> TryCalculateModdedDictionary(Guid applicationBase, CharacterData charaData, out Dictionary<(string GamePath, string? Hash), string> moddedDictionary, CancellationToken token)
|
||||
{
|
||||
Stopwatch st = Stopwatch.StartNew();
|
||||
ConcurrentBag<FileReplacementData> missingFiles = [];
|
||||
moddedDictionary = [];
|
||||
ConcurrentDictionary<(string GamePath, string? Hash), string> outputDict = new();
|
||||
bool hasMigrationChanges = false;
|
||||
|
||||
try
|
||||
{
|
||||
var replacementList = charaData.FileReplacements.SelectMany(k => k.Value.Where(v => string.IsNullOrEmpty(v.FileSwapPath))).ToList();
|
||||
Parallel.ForEach(replacementList, new ParallelOptions()
|
||||
{
|
||||
CancellationToken = token,
|
||||
MaxDegreeOfParallelism = 4
|
||||
},
|
||||
(item) =>
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var fileCache = _fileDbManager.GetFileCacheByHash(item.Hash);
|
||||
if (fileCache != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(new FileInfo(fileCache.ResolvedFilepath).Extension))
|
||||
{
|
||||
hasMigrationChanges = true;
|
||||
fileCache = _fileDbManager.MigrateFileHashToExtension(fileCache, item.GamePaths[0].Split(".")[^1]);
|
||||
}
|
||||
|
||||
foreach (var gamePath in item.GamePaths)
|
||||
{
|
||||
outputDict[(gamePath, item.Hash)] = fileCache.ResolvedFilepath;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogTrace("Missing file: {hash}", item.Hash);
|
||||
missingFiles.Add(item);
|
||||
}
|
||||
});
|
||||
|
||||
moddedDictionary = outputDict.ToDictionary(k => k.Key, k => k.Value);
|
||||
|
||||
foreach (var item in charaData.FileReplacements.SelectMany(k => k.Value.Where(v => !string.IsNullOrEmpty(v.FileSwapPath))).ToList())
|
||||
{
|
||||
foreach (var gamePath in item.GamePaths)
|
||||
{
|
||||
Logger.LogTrace("[BASE-{appBase}] Adding file swap for {path}: {fileSwap}", applicationBase, gamePath, item.FileSwapPath);
|
||||
moddedDictionary[(gamePath, null)] = item.FileSwapPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "[BASE-{appBase}] Something went wrong during calculation replacements", applicationBase);
|
||||
}
|
||||
if (hasMigrationChanges) _fileDbManager.WriteOutFullCsv();
|
||||
st.Stop();
|
||||
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];
|
||||
}
|
||||
}
|
||||
43
LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs
Normal file
43
LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using LightlessSync.API.Data;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// orchestrates the lifecycle of a paired character
|
||||
/// </summary>
|
||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||
{
|
||||
new string Ident { get; }
|
||||
bool Initialized { get; }
|
||||
bool IsVisible { get; }
|
||||
bool ScheduledForDeletion { get; set; }
|
||||
CharacterData? LastReceivedCharacterData { get; }
|
||||
long LastAppliedDataBytes { get; }
|
||||
new string? PlayerName { get; }
|
||||
string PlayerNameHash { get; }
|
||||
uint PlayerCharacterId { get; }
|
||||
DateTime? LastDataReceivedAt { get; }
|
||||
DateTime? LastApplyAttemptAt { get; }
|
||||
DateTime? LastSuccessfulApplyAt { get; }
|
||||
string? LastFailureReason { get; }
|
||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||
bool IsApplying { get; }
|
||||
bool IsDownloading { get; }
|
||||
int PendingDownloadCount { get; }
|
||||
int ForbiddenDownloadCount { get; }
|
||||
bool PendingModReapply { get; }
|
||||
bool ModApplyDeferred { get; }
|
||||
int MissingCriticalMods { get; }
|
||||
int MissingNonCriticalMods { get; }
|
||||
int MissingForbiddenMods { get; }
|
||||
DateTime? InvisibleSinceUtc { get; }
|
||||
DateTime? VisibilityEvictionDueAtUtc { get; }
|
||||
|
||||
void Initialize();
|
||||
void ApplyData(CharacterData data);
|
||||
void ApplyLastReceivedData(bool forced = false);
|
||||
bool FetchPerformanceMetricsFromCache();
|
||||
void LoadCachedCharacterData(CharacterData data);
|
||||
void SetUploading(bool uploading);
|
||||
void SetPaused(bool paused);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
public interface IPairHandlerAdapterFactory
|
||||
{
|
||||
IPairHandlerAdapter Create(string ident);
|
||||
}
|
||||
19
LightlessSync/PlayerData/Pairs/IPairPerformanceSubject.cs
Normal file
19
LightlessSync/PlayerData/Pairs/IPairPerformanceSubject.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using LightlessSync.API.Data;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// performance metrics for each pair handler
|
||||
/// </summary>
|
||||
public interface IPairPerformanceSubject
|
||||
{
|
||||
string Ident { get; }
|
||||
string PlayerName { get; }
|
||||
UserData UserData { get; }
|
||||
bool IsPaused { get; }
|
||||
bool IsDirectlyPaired { get; }
|
||||
bool HasStickyPermissions { get; }
|
||||
long LastAppliedApproximateVRAMBytes { get; set; }
|
||||
long LastAppliedApproximateEffectiveVRAMBytes { get; set; }
|
||||
long LastAppliedDataTris { get; set; }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
public record OptionalPluginWarning
|
||||
{
|
||||
public bool ShownHeelsWarning { get; set; } = false;
|
||||
public bool ShownCustomizePlusWarning { get; set; } = false;
|
||||
public bool ShownHonorificWarning { get; set; } = false;
|
||||
public bool ShownMoodlesWarning { get; set; } = false;
|
||||
public bool ShowPetNicknamesWarning { get; set; } = false;
|
||||
}
|
||||
@@ -1,172 +1,176 @@
|
||||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// ui wrapper around a pair connection
|
||||
/// </summary>
|
||||
public class Pair
|
||||
{
|
||||
private readonly PairHandlerFactory _cachedPlayerFactory;
|
||||
private readonly SemaphoreSlim _creationSemaphore = new(1);
|
||||
private readonly PairLedger _pairLedger;
|
||||
private readonly ILogger<Pair> _logger;
|
||||
private readonly LightlessMediator _mediator;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private CancellationTokenSource _applicationCts = new();
|
||||
private OnlineUserIdentDto? _onlineUserIdentDto = null;
|
||||
private readonly Lazy<ApiController> _apiController;
|
||||
|
||||
public Pair(ILogger<Pair> logger, UserFullPairDto userPair, PairHandlerFactory cachedPlayerFactory,
|
||||
LightlessMediator mediator, ServerConfigurationManager serverConfigurationManager)
|
||||
private const int _lightlessPrefixColor = 708;
|
||||
|
||||
public Pair(
|
||||
ILogger<Pair> logger,
|
||||
UserFullPairDto userPair,
|
||||
PairLedger pairLedger,
|
||||
LightlessMediator mediator,
|
||||
ServerConfigurationManager serverConfigurationManager,
|
||||
Lazy<ApiController> apiController)
|
||||
{
|
||||
_logger = logger;
|
||||
UserPair = userPair;
|
||||
_cachedPlayerFactory = cachedPlayerFactory;
|
||||
_pairLedger = pairLedger;
|
||||
_mediator = mediator;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_apiController = apiController;
|
||||
}
|
||||
|
||||
public bool HasCachedPlayer => CachedPlayer != null && !string.IsNullOrEmpty(CachedPlayer.PlayerName) && _onlineUserIdentDto != null;
|
||||
private PairUniqueIdentifier PairIdent => UniqueIdent;
|
||||
|
||||
private IPairHandlerAdapter? TryGetHandler()
|
||||
{
|
||||
return _pairLedger.GetHandler(PairIdent);
|
||||
}
|
||||
|
||||
private PairConnection? TryGetConnection()
|
||||
{
|
||||
return _pairLedger.TryGetEntry(PairIdent, out var entry) && entry is not null
|
||||
? entry.Connection
|
||||
: null;
|
||||
}
|
||||
|
||||
public bool HasCachedPlayer => TryGetHandler() is not null;
|
||||
public IndividualPairStatus IndividualPairStatus => UserPair.IndividualPairStatus;
|
||||
public bool IsDirectlyPaired => IndividualPairStatus != IndividualPairStatus.None;
|
||||
public bool IsOneSidedPair => IndividualPairStatus == IndividualPairStatus.OneSided;
|
||||
public bool IsOnline => CachedPlayer != null;
|
||||
|
||||
public bool IsOnline => TryGetConnection()?.IsOnline ?? false;
|
||||
|
||||
public bool IsPaired => IndividualPairStatus == IndividualPairStatus.Bidirectional || UserPair.Groups.Any();
|
||||
public bool IsPaused => UserPair.OwnPermissions.IsPaused();
|
||||
public bool IsVisible => CachedPlayer?.IsVisible ?? false;
|
||||
public CharacterData? LastReceivedCharacterData { get; set; }
|
||||
public string? PlayerName => CachedPlayer?.PlayerName ?? string.Empty;
|
||||
public long LastAppliedDataBytes => CachedPlayer?.LastAppliedDataBytes ?? -1;
|
||||
public long LastAppliedDataTris { get; set; } = -1;
|
||||
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
||||
public string Ident => _onlineUserIdentDto?.Ident ?? string.Empty;
|
||||
public bool IsVisible => _pairLedger.IsPairVisible(PairIdent);
|
||||
public CharacterData? LastReceivedCharacterData => TryGetHandler()?.LastReceivedCharacterData;
|
||||
public string? PlayerName => TryGetHandler()?.PlayerName ?? UserPair.User.AliasOrUID;
|
||||
public long LastAppliedDataBytes => TryGetHandler()?.LastAppliedDataBytes ?? -1;
|
||||
public long LastAppliedDataTris => TryGetHandler()?.LastAppliedDataTris ?? -1;
|
||||
public long LastAppliedApproximateVRAMBytes => TryGetHandler()?.LastAppliedApproximateVRAMBytes ?? -1;
|
||||
public long LastAppliedApproximateEffectiveVRAMBytes => TryGetHandler()?.LastAppliedApproximateEffectiveVRAMBytes ?? -1;
|
||||
public string Ident => TryGetHandler()?.Ident ?? TryGetConnection()?.Ident ?? string.Empty;
|
||||
public uint PlayerCharacterId => TryGetHandler()?.PlayerCharacterId ?? uint.MaxValue;
|
||||
public PairUniqueIdentifier UniqueIdent => new(UserData.UID);
|
||||
|
||||
public UserData UserData => UserPair.User;
|
||||
|
||||
public UserFullPairDto UserPair { get; set; }
|
||||
private PairHandler? CachedPlayer { get; set; }
|
||||
|
||||
public void AddContextMenu(IMenuOpenedArgs args)
|
||||
{
|
||||
if (CachedPlayer == null || (args.Target is not MenuTargetDefault target) || target.TargetObjectId != CachedPlayer.PlayerCharacterId || IsPaused) return;
|
||||
|
||||
SeStringBuilder seStringBuilder = new();
|
||||
SeStringBuilder seStringBuilder2 = new();
|
||||
SeStringBuilder seStringBuilder3 = new();
|
||||
SeStringBuilder seStringBuilder4 = new();
|
||||
var openProfileSeString = seStringBuilder.AddText("Open Profile").Build();
|
||||
var reapplyDataSeString = seStringBuilder2.AddText("Reapply last data").Build();
|
||||
var cyclePauseState = seStringBuilder3.AddText("Cycle pause state").Build();
|
||||
var changePermissions = seStringBuilder4.AddText("Change Permissions").Build();
|
||||
args.AddMenuItem(new MenuItem()
|
||||
var handler = TryGetHandler();
|
||||
if (handler is null)
|
||||
{
|
||||
Name = openProfileSeString,
|
||||
OnClicked = (a) => _mediator.Publish(new ProfileOpenStandaloneMessage(this)),
|
||||
UseDefaultPrefix = false,
|
||||
PrefixChar = 'M',
|
||||
PrefixColor = 526
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsPaused)
|
||||
{
|
||||
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
ApplyLastReceivedData(forced: true);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
_mediator.Publish(new OpenPermissionWindow(this));
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
args.AddMenuItem(new MenuItem()
|
||||
if (IsPaused)
|
||||
{
|
||||
Name = reapplyDataSeString,
|
||||
OnClicked = (a) => ApplyLastReceivedData(forced: true),
|
||||
UseDefaultPrefix = false,
|
||||
PrefixChar = 'M',
|
||||
PrefixColor = 526
|
||||
});
|
||||
UiSharedService.AddContextMenuItem(args, name: "Toggle Unpause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
_ = _apiController.Value.UnpauseAsync(UserData);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.AddContextMenuItem(args, name: "Toggle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
_ = _apiController.Value.PauseAsync(UserData);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
args.AddMenuItem(new MenuItem()
|
||||
UiSharedService.AddContextMenuItem(args, name: "Cycle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
Name = changePermissions,
|
||||
OnClicked = (a) => _mediator.Publish(new OpenPermissionWindow(this)),
|
||||
UseDefaultPrefix = false,
|
||||
PrefixChar = 'M',
|
||||
PrefixColor = 526
|
||||
});
|
||||
|
||||
args.AddMenuItem(new MenuItem()
|
||||
{
|
||||
Name = cyclePauseState,
|
||||
OnClicked = (a) => _mediator.Publish(new CyclePauseMessage(UserData)),
|
||||
UseDefaultPrefix = false,
|
||||
PrefixChar = 'M',
|
||||
PrefixColor = 526
|
||||
TriggerCyclePause();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
public void ApplyData(OnlineUserCharaDataDto data)
|
||||
{
|
||||
_applicationCts = _applicationCts.CancelRecreate();
|
||||
LastReceivedCharacterData = data.CharaData;
|
||||
_logger.LogTrace("Character data received for {Uid}; handler will process via registry.", UserData.UID);
|
||||
}
|
||||
|
||||
if (CachedPlayer == null)
|
||||
{
|
||||
_logger.LogDebug("Received Data for {uid} but CachedPlayer does not exist, waiting", data.User.UID);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
using var timeoutCts = new CancellationTokenSource();
|
||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(120));
|
||||
var appToken = _applicationCts.Token;
|
||||
using var combined = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, appToken);
|
||||
while (CachedPlayer == null && !combined.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(250, combined.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!combined.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogDebug("Applying delayed data for {uid}", data.User.UID);
|
||||
ApplyLastReceivedData();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyLastReceivedData();
|
||||
private void TriggerCyclePause()
|
||||
{
|
||||
_ = _apiController.Value.CyclePauseAsync(this);
|
||||
}
|
||||
|
||||
public void ApplyLastReceivedData(bool forced = false)
|
||||
{
|
||||
if (CachedPlayer == null) return;
|
||||
if (LastReceivedCharacterData == null) return;
|
||||
var handler = TryGetHandler();
|
||||
if (handler is null)
|
||||
{
|
||||
_logger.LogTrace("ApplyLastReceivedData skipped for {Uid}: handler missing.", UserData.UID);
|
||||
return;
|
||||
}
|
||||
|
||||
CachedPlayer.ApplyCharacterData(Guid.NewGuid(), RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone())!, forced);
|
||||
handler.ApplyLastReceivedData(forced);
|
||||
}
|
||||
|
||||
public void CreateCachedPlayer(OnlineUserIdentDto? dto = null)
|
||||
{
|
||||
try
|
||||
var handler = TryGetHandler();
|
||||
if (handler is null)
|
||||
{
|
||||
_creationSemaphore.Wait();
|
||||
|
||||
if (CachedPlayer != null) return;
|
||||
|
||||
if (dto == null && _onlineUserIdentDto == null)
|
||||
{
|
||||
CachedPlayer?.Dispose();
|
||||
CachedPlayer = null;
|
||||
return;
|
||||
}
|
||||
if (dto != null)
|
||||
{
|
||||
_onlineUserIdentDto = dto;
|
||||
}
|
||||
|
||||
CachedPlayer?.Dispose();
|
||||
CachedPlayer = _cachedPlayerFactory.Create(this);
|
||||
_logger.LogTrace("CreateCachedPlayer skipped for {Uid}: handler unavailable.", UserData.UID);
|
||||
return;
|
||||
}
|
||||
finally
|
||||
|
||||
if (!handler.Initialized)
|
||||
{
|
||||
_creationSemaphore.Release();
|
||||
handler.Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +181,7 @@ public class Pair
|
||||
|
||||
public string GetPlayerNameHash()
|
||||
{
|
||||
return CachedPlayer?.PlayerNameHash ?? string.Empty;
|
||||
return TryGetHandler()?.PlayerNameHash ?? string.Empty;
|
||||
}
|
||||
|
||||
public bool HasAnyConnection()
|
||||
@@ -187,21 +191,7 @@ public class Pair
|
||||
|
||||
public void MarkOffline(bool wait = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (wait)
|
||||
_creationSemaphore.Wait();
|
||||
LastReceivedCharacterData = null;
|
||||
var player = CachedPlayer;
|
||||
CachedPlayer = null;
|
||||
player?.Dispose();
|
||||
_onlineUserIdentDto = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (wait)
|
||||
_creationSemaphore.Release();
|
||||
}
|
||||
_logger.LogTrace("MarkOffline invoked for {Uid} (wait: {Wait}). New registry handles handler disposal.", UserData.UID, wait);
|
||||
}
|
||||
|
||||
public void SetNote(string note)
|
||||
@@ -211,47 +201,48 @@ public class Pair
|
||||
|
||||
internal void SetIsUploading()
|
||||
{
|
||||
CachedPlayer?.SetUploading();
|
||||
var handler = TryGetHandler();
|
||||
if (handler is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
handler.SetUploading(true);
|
||||
}
|
||||
|
||||
private CharacterData? RemoveNotSyncedFiles(CharacterData? data)
|
||||
public PairDebugInfo GetDebugInfo()
|
||||
{
|
||||
_logger.LogTrace("Removing not synced files");
|
||||
if (data == null)
|
||||
{
|
||||
_logger.LogTrace("Nothing to remove");
|
||||
return data;
|
||||
}
|
||||
var handler = TryGetHandler();
|
||||
if (handler is null)
|
||||
return PairDebugInfo.Empty;
|
||||
|
||||
bool disableIndividualAnimations = (UserPair.OtherPermissions.IsDisableAnimations() || UserPair.OwnPermissions.IsDisableAnimations());
|
||||
bool disableIndividualVFX = (UserPair.OtherPermissions.IsDisableVFX() || UserPair.OwnPermissions.IsDisableVFX());
|
||||
bool disableIndividualSounds = (UserPair.OtherPermissions.IsDisableSounds() || UserPair.OwnPermissions.IsDisableSounds());
|
||||
var now = DateTime.UtcNow;
|
||||
var dueAt = handler.VisibilityEvictionDueAtUtc;
|
||||
var remainingSeconds = dueAt.HasValue
|
||||
? Math.Max(0, (dueAt.Value - now).TotalSeconds)
|
||||
: (double?)null;
|
||||
|
||||
_logger.LogTrace("Disable: Sounds: {disableIndividualSounds}, Anims: {disableIndividualAnims}; " +
|
||||
"VFX: {disableGroupSounds}",
|
||||
disableIndividualSounds, disableIndividualAnimations, disableIndividualVFX);
|
||||
|
||||
if (disableIndividualAnimations || disableIndividualSounds || disableIndividualVFX)
|
||||
{
|
||||
_logger.LogTrace("Data cleaned up: Animations disabled: {disableAnimations}, Sounds disabled: {disableSounds}, VFX disabled: {disableVFX}",
|
||||
disableIndividualAnimations, disableIndividualSounds, disableIndividualVFX);
|
||||
foreach (var objectKind in data.FileReplacements.Select(k => k.Key))
|
||||
{
|
||||
if (disableIndividualSounds)
|
||||
data.FileReplacements[objectKind] = data.FileReplacements[objectKind]
|
||||
.Where(f => !f.GamePaths.Any(p => p.EndsWith("scd", StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
if (disableIndividualAnimations)
|
||||
data.FileReplacements[objectKind] = data.FileReplacements[objectKind]
|
||||
.Where(f => !f.GamePaths.Any(p => p.EndsWith("tmb", StringComparison.OrdinalIgnoreCase) || p.EndsWith("pap", StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
if (disableIndividualVFX)
|
||||
data.FileReplacements[objectKind] = data.FileReplacements[objectKind]
|
||||
.Where(f => !f.GamePaths.Any(p => p.EndsWith("atex", StringComparison.OrdinalIgnoreCase) || p.EndsWith("avfx", StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
return new PairDebugInfo(
|
||||
true,
|
||||
handler.Initialized,
|
||||
handler.IsVisible,
|
||||
handler.ScheduledForDeletion,
|
||||
handler.LastDataReceivedAt,
|
||||
handler.LastApplyAttemptAt,
|
||||
handler.LastSuccessfulApplyAt,
|
||||
handler.InvisibleSinceUtc,
|
||||
handler.VisibilityEvictionDueAtUtc,
|
||||
remainingSeconds,
|
||||
handler.LastFailureReason,
|
||||
handler.LastBlockingConditions,
|
||||
handler.IsApplying,
|
||||
handler.IsDownloading,
|
||||
handler.PendingDownloadCount,
|
||||
handler.ForbiddenDownloadCount,
|
||||
handler.PendingModReapply,
|
||||
handler.ModApplyDeferred,
|
||||
handler.MissingCriticalMods,
|
||||
handler.MissingNonCriticalMods,
|
||||
handler.MissingForbiddenMods);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
136
LightlessSync/PlayerData/Pairs/PairCoordinator.Groups.cs
Normal file
136
LightlessSync/PlayerData/Pairs/PairCoordinator.Groups.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// handles group related pair events
|
||||
/// </summary>
|
||||
public sealed partial class PairCoordinator
|
||||
{
|
||||
public void HandleGroupChangePermissions(GroupPermissionDto dto)
|
||||
{
|
||||
var result = _pairManager.UpdateGroupPermissions(dto);
|
||||
if (!result.Success)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update permissions for group {GroupId}: {Error}", dto.Group.GID, result.Error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
|
||||
public void HandleGroupFullInfo(GroupFullInfoDto dto)
|
||||
{
|
||||
var result = _pairManager.AddGroup(dto);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to add group {GroupId}: {Error}", dto.Group.GID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
|
||||
public void HandleGroupPairJoined(GroupPairFullInfoDto dto)
|
||||
{
|
||||
var result = _pairManager.AddOrUpdateGroupPair(dto);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to add group pair {Uid}/{Group}: {Error}", dto.User.UID, dto.Group.GID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
|
||||
public void HandleGroupPairLeft(GroupPairDto dto)
|
||||
{
|
||||
var deregistration = _pairManager.RemoveGroupPair(dto);
|
||||
if (deregistration.Success && deregistration.Value is { } registration && registration.CharacterIdent is not null)
|
||||
{
|
||||
_ = _handlerRegistry.DeregisterOfflinePair(registration, forceDisposal: true);
|
||||
}
|
||||
else if (!deregistration.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("RemoveGroupPair failed for {Uid}: {Error}", dto.User.UID, deregistration.Error);
|
||||
}
|
||||
|
||||
if (deregistration.Success)
|
||||
{
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleGroupRemoved(GroupDto dto)
|
||||
{
|
||||
var removalResult = _pairManager.RemoveGroup(dto.Group.GID);
|
||||
if (removalResult.Success)
|
||||
{
|
||||
foreach (var registration in removalResult.Value)
|
||||
{
|
||||
if (registration.CharacterIdent is not null)
|
||||
{
|
||||
_ = _handlerRegistry.DeregisterOfflinePair(registration, forceDisposal: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to remove group {Group}: {Error}", dto.Group.GID, removalResult.Error);
|
||||
}
|
||||
|
||||
if (removalResult.Success)
|
||||
{
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleGroupInfoUpdate(GroupInfoDto dto)
|
||||
{
|
||||
var result = _pairManager.UpdateGroupInfo(dto);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update group info for {Group}: {Error}", dto.Group.GID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
|
||||
public void HandleGroupPairPermissions(GroupPairUserPermissionDto dto)
|
||||
{
|
||||
var result = _pairManager.UpdateGroupPairPermissions(dto);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update group pair permissions for {Group}: {Error}", dto.Group.GID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
|
||||
public void HandleGroupPairStatus(GroupPairUserInfoDto dto, bool isSelf)
|
||||
{
|
||||
PairOperationResult result;
|
||||
if (isSelf)
|
||||
{
|
||||
result = _pairManager.UpdateGroupStatus(dto);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = _pairManager.UpdateGroupPairStatus(dto);
|
||||
}
|
||||
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update group status for {Group}:{Uid}: {Error}", dto.GID, dto.UID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
}
|
||||
302
LightlessSync/PlayerData/Pairs/PairCoordinator.Users.cs
Normal file
302
LightlessSync/PlayerData/Pairs/PairCoordinator.Users.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.Services.Events;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// handles user pair events
|
||||
/// </summary>
|
||||
public sealed partial class PairCoordinator
|
||||
{
|
||||
public void HandleUserAddPair(UserPairDto dto, bool addToLastAddedUser = true)
|
||||
{
|
||||
var result = _pairManager.AddOrUpdateIndividual(dto, addToLastAddedUser);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to add/update pair {Uid}: {Error}", dto.User.UID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged();
|
||||
}
|
||||
|
||||
public void HandleUserAddPair(UserFullPairDto dto)
|
||||
{
|
||||
var result = _pairManager.AddOrUpdateIndividual(dto);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to add/update full pair {Uid}: {Error}", dto.User.UID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged();
|
||||
}
|
||||
|
||||
public void HandleUserRemovePair(UserDto dto)
|
||||
{
|
||||
var removal = _pairManager.RemoveIndividual(dto);
|
||||
if (removal.Success && removal.Value is { } registration && registration.CharacterIdent is not null)
|
||||
{
|
||||
_ = _handlerRegistry.DeregisterOfflinePair(registration, forceDisposal: true);
|
||||
}
|
||||
else if (!removal.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("RemoveIndividual failed for {Uid}: {Error}", dto.User.UID, removal.Error);
|
||||
}
|
||||
|
||||
if (removal.Success)
|
||||
{
|
||||
_pendingCharacterData.TryRemove(dto.User.UID, out _);
|
||||
PublishPairDataChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleUserStatus(UserIndividualPairStatusDto dto)
|
||||
{
|
||||
var result = _pairManager.SetIndividualStatus(dto);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update individual pair status for {Uid}: {Error}", dto.User.UID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged();
|
||||
}
|
||||
|
||||
public void HandleUserOnline(OnlineUserIdentDto dto, bool sendNotification)
|
||||
{
|
||||
var wasOnline = false;
|
||||
PairConnection? previousConnection = null;
|
||||
if (_pairManager.TryGetPair(dto.User.UID, out var existingConnection))
|
||||
{
|
||||
previousConnection = existingConnection;
|
||||
wasOnline = existingConnection.IsOnline;
|
||||
}
|
||||
|
||||
var registrationResult = _pairManager.MarkOnline(dto);
|
||||
if (!registrationResult.Success)
|
||||
{
|
||||
_logger.LogDebug("MarkOnline failed for {Uid}: {Error}", dto.User.UID, registrationResult.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var registration = registrationResult.Value;
|
||||
if (registration.CharacterIdent is null)
|
||||
{
|
||||
_logger.LogDebug("Online registration for {Uid} missing ident.", dto.User.UID);
|
||||
}
|
||||
else
|
||||
{
|
||||
var handlerResult = _handlerRegistry.RegisterOnlinePair(registration);
|
||||
if (!handlerResult.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("RegisterOnlinePair failed for {Uid}: {Error}", dto.User.UID, handlerResult.Error);
|
||||
}
|
||||
}
|
||||
|
||||
var connectionResult = _pairManager.GetPair(dto.User.UID);
|
||||
var connection = connectionResult.Success ? connectionResult.Value : previousConnection;
|
||||
if (connection is not null)
|
||||
{
|
||||
_mediator.Publish(new ClearProfileUserDataMessage(connection.User));
|
||||
}
|
||||
else
|
||||
{
|
||||
_mediator.Publish(new ClearProfileUserDataMessage(dto.User));
|
||||
}
|
||||
|
||||
if (!wasOnline)
|
||||
{
|
||||
NotifyUserOnline(connection, sendNotification);
|
||||
}
|
||||
|
||||
if (registration.CharacterIdent is not null &&
|
||||
_pendingCharacterData.TryRemove(dto.User.UID, out var pendingData))
|
||||
{
|
||||
var pendingRegistration = new PairRegistration(new PairUniqueIdentifier(dto.User.UID), registration.CharacterIdent);
|
||||
var pendingApply = _handlerRegistry.ApplyCharacterData(pendingRegistration, pendingData);
|
||||
if (!pendingApply.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Applying pending character data for {Uid} failed: {Error}", dto.User.UID, pendingApply.Error);
|
||||
}
|
||||
}
|
||||
|
||||
PublishPairDataChanged();
|
||||
}
|
||||
|
||||
public void HandleUserOffline(UserData user)
|
||||
{
|
||||
var registrationResult = _pairManager.MarkOffline(user);
|
||||
if (registrationResult.Success)
|
||||
{
|
||||
_pendingCharacterData.TryRemove(user.UID, out _);
|
||||
if (registrationResult.Value.CharacterIdent is not null)
|
||||
{
|
||||
_ = _handlerRegistry.DeregisterOfflinePair(registrationResult.Value);
|
||||
}
|
||||
|
||||
_mediator.Publish(new ClearProfileUserDataMessage(user));
|
||||
PublishPairDataChanged();
|
||||
}
|
||||
else if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("MarkOffline failed for {Uid}: {Error}", user.UID, registrationResult.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleUserPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
var pairResult = _pairManager.GetPair(dto.User.UID);
|
||||
if (!pairResult.Success)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Permission update received for unknown pair {Uid}", dto.User.UID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var connection = pairResult.Value;
|
||||
var previous = connection.OtherToSelfPermissions;
|
||||
|
||||
var updateResult = _pairManager.UpdateOtherPermissions(dto);
|
||||
if (!updateResult.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update permissions for {Uid}: {Error}", dto.User.UID, updateResult.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged();
|
||||
|
||||
if (previous.IsPaused() != dto.Permissions.IsPaused())
|
||||
{
|
||||
_mediator.Publish(new ClearProfileUserDataMessage(dto.User));
|
||||
|
||||
if (connection.Ident is not null)
|
||||
{
|
||||
var pauseResult = _handlerRegistry.SetPausedState(new PairUniqueIdentifier(dto.User.UID), connection.Ident, dto.Permissions.IsPaused());
|
||||
if (!pauseResult.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update pause state for {Uid}: {Error}", dto.User.UID, pauseResult.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!connection.IsPaused && connection.Ident is not null)
|
||||
{
|
||||
ReapplyLastKnownData(dto.User.UID, connection.Ident);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleSelfPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
var pairResult = _pairManager.GetPair(dto.User.UID);
|
||||
if (!pairResult.Success)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Self permission update received for unknown pair {Uid}", dto.User.UID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var connection = pairResult.Value;
|
||||
var previous = connection.SelfToOtherPermissions;
|
||||
|
||||
var updateResult = _pairManager.UpdateSelfPermissions(dto);
|
||||
if (!updateResult.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update self permissions for {Uid}: {Error}", dto.User.UID, updateResult.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged();
|
||||
|
||||
if (previous.IsPaused() != dto.Permissions.IsPaused())
|
||||
{
|
||||
_mediator.Publish(new ClearProfileUserDataMessage(dto.User));
|
||||
|
||||
if (connection.Ident is not null)
|
||||
{
|
||||
var pauseResult = _handlerRegistry.SetPausedState(new PairUniqueIdentifier(dto.User.UID), connection.Ident, dto.Permissions.IsPaused());
|
||||
if (!pauseResult.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update pause state for {Uid}: {Error}", dto.User.UID, pauseResult.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!connection.IsPaused && connection.Ident is not null)
|
||||
{
|
||||
ReapplyLastKnownData(dto.User.UID, connection.Ident);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleUploadStatus(UserDto dto)
|
||||
{
|
||||
var pairResult = _pairManager.GetPair(dto.User.UID);
|
||||
if (!pairResult.Success)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Upload status received for unknown pair {Uid}", dto.User.UID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var connection = pairResult.Value;
|
||||
if (connection.Ident is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var setResult = _handlerRegistry.SetUploading(new PairUniqueIdentifier(dto.User.UID), connection.Ident, true);
|
||||
if (!setResult.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to set uploading for {Uid}: {Error}", dto.User.UID, setResult.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleCharacterData(OnlineUserCharaDataDto dto)
|
||||
{
|
||||
var pairResult = _pairManager.GetPair(dto.User.UID);
|
||||
if (!pairResult.Success)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Character data received for unknown pair {Uid}, queued for later.", dto.User.UID);
|
||||
}
|
||||
_pendingCharacterData[dto.User.UID] = dto;
|
||||
return;
|
||||
}
|
||||
|
||||
var connection = pairResult.Value;
|
||||
_mediator.Publish(new EventMessage(new Event(connection.User, nameof(PairCoordinator), EventSeverity.Informational, "Received Character Data")));
|
||||
if (connection.Ident is null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Character data received for {Uid} without ident, queued for later.", dto.User.UID);
|
||||
}
|
||||
_pendingCharacterData[dto.User.UID] = dto;
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingCharacterData.TryRemove(dto.User.UID, out _);
|
||||
var registration = new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident);
|
||||
var applyResult = _handlerRegistry.ApplyCharacterData(registration, dto);
|
||||
if (!applyResult.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("ApplyCharacterData queued for {Uid}: {Error}", dto.User.UID, applyResult.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleProfile(UserDto dto)
|
||||
{
|
||||
_mediator.Publish(new ClearProfileUserDataMessage(dto.User));
|
||||
}
|
||||
}
|
||||
139
LightlessSync/PlayerData/Pairs/PairCoordinator.cs
Normal file
139
LightlessSync/PlayerData/Pairs/PairCoordinator.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System.Collections.Concurrent;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// wires mediator events into the pair system
|
||||
/// </summary>
|
||||
public sealed partial class PairCoordinator : MediatorSubscriberBase
|
||||
{
|
||||
private readonly ILogger<PairCoordinator> _logger;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly LightlessMediator _mediator;
|
||||
private readonly PairHandlerRegistry _handlerRegistry;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairLedger _pairLedger;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly PairPerformanceMetricsCache _metricsCache;
|
||||
private readonly ConcurrentDictionary<string, OnlineUserCharaDataDto> _pendingCharacterData = new(StringComparer.Ordinal);
|
||||
|
||||
public PairCoordinator(
|
||||
ILogger<PairCoordinator> logger,
|
||||
LightlessConfigService configService,
|
||||
LightlessMediator mediator,
|
||||
PairHandlerRegistry handlerRegistry,
|
||||
PairManager pairManager,
|
||||
PairLedger pairLedger,
|
||||
ServerConfigurationManager serverConfigurationManager,
|
||||
PairPerformanceMetricsCache metricsCache)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_mediator = mediator;
|
||||
_handlerRegistry = handlerRegistry;
|
||||
_pairManager = pairManager;
|
||||
_pairLedger = pairLedger;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_metricsCache = metricsCache;
|
||||
|
||||
mediator.Subscribe<ActiveServerChangedMessage>(this, msg => HandleActiveServerChange(msg.ServerUrl));
|
||||
mediator.Subscribe<DisconnectedMessage>(this, _ => HandleDisconnected());
|
||||
}
|
||||
|
||||
internal PairLedger Ledger => _pairLedger;
|
||||
|
||||
private void PublishPairDataChanged(bool groupChanged = false)
|
||||
{
|
||||
_mediator.Publish(new RefreshUiMessage());
|
||||
_mediator.Publish(new PairDataChangedMessage());
|
||||
if (groupChanged)
|
||||
{
|
||||
_mediator.Publish(new GroupCollectionChangedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyUserOnline(PairConnection? connection, bool sendNotification)
|
||||
{
|
||||
if (connection is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var config = _configService.Current;
|
||||
if (config.ShowOnlineNotifications && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Pair {Uid} marked online", connection.User.UID);
|
||||
}
|
||||
|
||||
if (!sendNotification || !config.ShowOnlineNotifications)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.ShowOnlineNotificationsOnlyForIndividualPairs &&
|
||||
(!connection.IsDirectlyPaired || connection.IsOneSided))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var note = _serverConfigurationManager.GetNoteForUid(connection.User.UID);
|
||||
if (config.ShowOnlineNotificationsOnlyForNamedPairs &&
|
||||
string.IsNullOrEmpty(note))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var message = !string.IsNullOrEmpty(note)
|
||||
? $"{note} ({connection.User.AliasOrUID}) is now online"
|
||||
: $"{connection.User.AliasOrUID} is now online";
|
||||
|
||||
_mediator.Publish(new NotificationMessage("User online", message, NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
private void ReapplyLastKnownData(string userId, string ident, bool forced = false)
|
||||
{
|
||||
var result = _handlerRegistry.ApplyLastReceivedData(new PairUniqueIdentifier(userId), ident, forced);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to reapply cached data for {Uid}: {Error}", userId, result.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleActiveServerChange(string serverUrl)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Active server changed to {Server}", serverUrl);
|
||||
}
|
||||
|
||||
ResetPairState();
|
||||
}
|
||||
|
||||
private void HandleDisconnected()
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Lightless disconnected, clearing pair state");
|
||||
}
|
||||
|
||||
ResetPairState();
|
||||
}
|
||||
|
||||
private void ResetPairState()
|
||||
{
|
||||
_handlerRegistry.ResetAllHandlers();
|
||||
_pairManager.ClearAll();
|
||||
_pendingCharacterData.Clear();
|
||||
_metricsCache.ClearAll();
|
||||
_mediator.Publish(new ClearProfileUserDataMessage());
|
||||
_mediator.Publish(new ClearProfileGroupDataMessage());
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
}
|
||||
48
LightlessSync/PlayerData/Pairs/PairDebugInfo.cs
Normal file
48
LightlessSync/PlayerData/Pairs/PairDebugInfo.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
public sealed record PairDebugInfo(
|
||||
bool HasHandler,
|
||||
bool HandlerInitialized,
|
||||
bool HandlerVisible,
|
||||
bool HandlerScheduledForDeletion,
|
||||
DateTime? LastDataReceivedAt,
|
||||
DateTime? LastApplyAttemptAt,
|
||||
DateTime? LastSuccessfulApplyAt,
|
||||
DateTime? InvisibleSinceUtc,
|
||||
DateTime? VisibilityEvictionDueAtUtc,
|
||||
double? VisibilityEvictionRemainingSeconds,
|
||||
string? LastFailureReason,
|
||||
IReadOnlyList<string> BlockingConditions,
|
||||
bool IsApplying,
|
||||
bool IsDownloading,
|
||||
int PendingDownloadCount,
|
||||
int ForbiddenDownloadCount,
|
||||
bool PendingModReapply,
|
||||
bool ModApplyDeferred,
|
||||
int MissingCriticalMods,
|
||||
int MissingNonCriticalMods,
|
||||
int MissingForbiddenMods)
|
||||
{
|
||||
public static PairDebugInfo Empty { get; } = new(
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Array.Empty<string>(),
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0);
|
||||
}
|
||||
2303
LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs
Normal file
2303
LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs
Normal file
File diff suppressed because it is too large
Load Diff
96
LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs
Normal file
96
LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.PairProcessing;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.Services.TextureCompression;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LightlessMediator _mediator;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly FileDownloadManagerFactory _fileDownloadManagerFactory;
|
||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IHostApplicationLifetime _lifetime;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||
private readonly ServerConfigurationManager _serverConfigManager;
|
||||
private readonly TextureDownscaleService _textureDownscaleService;
|
||||
private readonly PairStateCache _pairStateCache;
|
||||
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
||||
|
||||
public PairHandlerAdapterFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
LightlessMediator mediator,
|
||||
PairManager pairManager,
|
||||
GameObjectHandlerFactory gameObjectHandlerFactory,
|
||||
IpcManager ipcManager,
|
||||
FileDownloadManagerFactory fileDownloadManagerFactory,
|
||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||
IServiceProvider serviceProvider,
|
||||
IHostApplicationLifetime lifetime,
|
||||
FileCacheManager fileCacheManager,
|
||||
PlayerPerformanceService playerPerformanceService,
|
||||
PairProcessingLimiter pairProcessingLimiter,
|
||||
ServerConfigurationManager serverConfigManager,
|
||||
TextureDownscaleService textureDownscaleService,
|
||||
PairStateCache pairStateCache,
|
||||
PairPerformanceMetricsCache pairPerformanceMetricsCache)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_mediator = mediator;
|
||||
_pairManager = pairManager;
|
||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||
_ipcManager = ipcManager;
|
||||
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||
_serviceProvider = serviceProvider;
|
||||
_lifetime = lifetime;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
_pairProcessingLimiter = pairProcessingLimiter;
|
||||
_serverConfigManager = serverConfigManager;
|
||||
_textureDownscaleService = textureDownscaleService;
|
||||
_pairStateCache = pairStateCache;
|
||||
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
||||
}
|
||||
|
||||
public IPairHandlerAdapter Create(string ident)
|
||||
{
|
||||
var downloadManager = _fileDownloadManagerFactory.Create();
|
||||
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
|
||||
var actorObjectService = _serviceProvider.GetRequiredService<ActorObjectService>();
|
||||
return new PairHandlerAdapter(
|
||||
_loggerFactory.CreateLogger<PairHandlerAdapter>(),
|
||||
_mediator,
|
||||
_pairManager,
|
||||
ident,
|
||||
_gameObjectHandlerFactory,
|
||||
_ipcManager,
|
||||
downloadManager,
|
||||
_pluginWarningNotificationManager,
|
||||
dalamudUtilService,
|
||||
actorObjectService,
|
||||
_lifetime,
|
||||
_fileCacheManager,
|
||||
_playerPerformanceService,
|
||||
_pairProcessingLimiter,
|
||||
_serverConfigManager,
|
||||
_textureDownscaleService,
|
||||
_pairStateCache,
|
||||
_pairPerformanceMetricsCache);
|
||||
}
|
||||
}
|
||||
521
LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs
Normal file
521
LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs
Normal file
@@ -0,0 +1,521 @@
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// creates, tracks, and removes pair handlers
|
||||
/// </summary>
|
||||
public sealed class PairHandlerRegistry : IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly object _pendingGate = new();
|
||||
private readonly object _visibilityGate = new();
|
||||
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, CancellationTokenSource> _pendingInvisibleEvictions = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new(ReferenceEqualityComparer.Instance);
|
||||
|
||||
private readonly IPairHandlerAdapterFactory _handlerFactory;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairStateCache _pairStateCache;
|
||||
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
||||
private readonly ILogger<PairHandlerRegistry> _logger;
|
||||
|
||||
private readonly TimeSpan _deletionGracePeriod = TimeSpan.FromMinutes(5);
|
||||
private static readonly TimeSpan _handlerReadyTimeout = TimeSpan.FromMinutes(3);
|
||||
private const int _handlerReadyPollDelayMs = 500;
|
||||
private readonly Dictionary<string, CancellationTokenSource> _pendingCharacterData = new(StringComparer.Ordinal);
|
||||
|
||||
public PairHandlerRegistry(
|
||||
IPairHandlerAdapterFactory handlerFactory,
|
||||
PairManager pairManager,
|
||||
PairStateCache pairStateCache,
|
||||
PairPerformanceMetricsCache pairPerformanceMetricsCache,
|
||||
ILogger<PairHandlerRegistry> logger)
|
||||
{
|
||||
_handlerFactory = handlerFactory;
|
||||
_pairManager = pairManager;
|
||||
_pairStateCache = pairStateCache;
|
||||
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public int GetVisibleUsersCount()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _entriesByHandler.Keys.Count(handler => handler.IsVisible);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsIdentVisible(string ident)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _entriesByIdent.TryGetValue(ident, out var entry) && entry.Handler.IsVisible;
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult<PairUniqueIdentifier> RegisterOnlinePair(PairRegistration registration)
|
||||
{
|
||||
if (registration.CharacterIdent is null)
|
||||
{
|
||||
return PairOperationResult<PairUniqueIdentifier>.Fail($"Registration for {registration.PairIdent.UserId} missing ident.");
|
||||
}
|
||||
|
||||
IPairHandlerAdapter handler;
|
||||
lock (_gate)
|
||||
{
|
||||
var entry = GetOrCreateEntry(registration.CharacterIdent);
|
||||
handler = entry.Handler;
|
||||
handler.ScheduledForDeletion = false;
|
||||
entry.AddPair(registration.PairIdent);
|
||||
if (!handler.Initialized)
|
||||
{
|
||||
handler.Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
ApplyPauseStateForHandler(handler);
|
||||
|
||||
if (handler.LastReceivedCharacterData is null)
|
||||
{
|
||||
var cachedData = _pairStateCache.TryLoad(registration.CharacterIdent);
|
||||
if (cachedData is not null)
|
||||
{
|
||||
handler.LoadCachedCharacterData(cachedData);
|
||||
}
|
||||
}
|
||||
|
||||
if (handler.LastReceivedCharacterData is not null &&
|
||||
(handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0))
|
||||
{
|
||||
handler.ApplyLastReceivedData(forced: true);
|
||||
}
|
||||
|
||||
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
|
||||
}
|
||||
|
||||
public PairOperationResult<PairUniqueIdentifier> DeregisterOfflinePair(PairRegistration registration, bool forceDisposal = false)
|
||||
{
|
||||
if (registration.CharacterIdent is null)
|
||||
{
|
||||
return PairOperationResult<PairUniqueIdentifier>.Fail($"Deregister for {registration.PairIdent.UserId} missing ident.");
|
||||
}
|
||||
|
||||
IPairHandlerAdapter? handler = null;
|
||||
bool shouldScheduleRemoval = false;
|
||||
bool shouldDisposeImmediately = false;
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_entriesByIdent.TryGetValue(registration.CharacterIdent, out var entry))
|
||||
{
|
||||
return PairOperationResult<PairUniqueIdentifier>.Fail($"Ident {registration.CharacterIdent} not registered.");
|
||||
}
|
||||
|
||||
handler = entry.Handler;
|
||||
entry.RemovePair(registration.PairIdent);
|
||||
if (entry.PairCount == 0)
|
||||
{
|
||||
if (forceDisposal)
|
||||
{
|
||||
shouldDisposeImmediately = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldScheduleRemoval = true;
|
||||
handler.ScheduledForDeletion = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDisposeImmediately && handler is not null)
|
||||
{
|
||||
if (TryFinalizeHandlerRemoval(handler))
|
||||
{
|
||||
handler.Dispose();
|
||||
}
|
||||
}
|
||||
else if (shouldScheduleRemoval && handler is not null)
|
||||
{
|
||||
_ = RemoveAfterGracePeriodAsync(handler);
|
||||
}
|
||||
|
||||
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
|
||||
}
|
||||
|
||||
private PairOperationResult CancelAllInvisibleEvictions()
|
||||
{
|
||||
List<CancellationTokenSource> snapshot;
|
||||
lock (_visibilityGate)
|
||||
{
|
||||
snapshot = [.. _pendingInvisibleEvictions.Values];
|
||||
_pendingInvisibleEvictions.Clear();
|
||||
}
|
||||
|
||||
List<string>? errors = null;
|
||||
|
||||
foreach (var cts in snapshot)
|
||||
{
|
||||
try { cts.Cancel(); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
(errors ??= new List<string>()).Add($"Cancel: {ex.Message}");
|
||||
}
|
||||
|
||||
try { cts.Dispose(); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
(errors ??= new List<string>()).Add($"Dispose: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return errors is null
|
||||
? PairOperationResult.Ok()
|
||||
: PairOperationResult.Fail($"CancelAllInvisibleEvictions had error(s): {string.Join(" | ", errors)}");
|
||||
}
|
||||
|
||||
public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
|
||||
{
|
||||
if (registration.CharacterIdent is null)
|
||||
{
|
||||
return PairOperationResult.Fail($"Character data received without ident for {registration.PairIdent.UserId}.");
|
||||
}
|
||||
|
||||
if (!TryGetHandler(registration.CharacterIdent, out var handler) || handler is null)
|
||||
{
|
||||
var registerResult = RegisterOnlinePair(registration);
|
||||
if (!registerResult.Success)
|
||||
{
|
||||
return PairOperationResult.Fail(registerResult.Error);
|
||||
}
|
||||
|
||||
if (!TryGetHandler(registration.CharacterIdent, out handler) || handler is null)
|
||||
{
|
||||
QueuePendingCharacterData(registration, dto);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
if (!handler.Initialized)
|
||||
{
|
||||
handler.Initialize();
|
||||
QueuePendingCharacterData(registration, dto);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
|
||||
handler.ApplyData(dto.CharaData);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
|
||||
public PairOperationResult ApplyLastReceivedData(PairUniqueIdentifier pairIdent, string ident, bool forced = false)
|
||||
{
|
||||
if (!TryGetHandler(ident, out var handler) || handler is null)
|
||||
{
|
||||
return PairOperationResult.Fail($"Cannot reapply data: handler for {pairIdent.UserId} not found.");
|
||||
}
|
||||
|
||||
handler.ApplyLastReceivedData(forced);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
|
||||
public PairOperationResult SetUploading(PairUniqueIdentifier pairIdent, string ident, bool uploading)
|
||||
{
|
||||
if (!TryGetHandler(ident, out var handler) || handler is null)
|
||||
{
|
||||
return PairOperationResult.Fail($"Cannot set uploading for {pairIdent.UserId}: handler not found.");
|
||||
}
|
||||
|
||||
handler.SetUploading(uploading);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
|
||||
public PairOperationResult SetPausedState(PairUniqueIdentifier pairIdent, string ident, bool paused)
|
||||
{
|
||||
if (!TryGetHandler(ident, out var handler) || handler is null)
|
||||
{
|
||||
return PairOperationResult.Fail($"Cannot update pause state for {pairIdent.UserId}: handler not found.");
|
||||
}
|
||||
|
||||
_ = paused; // value reflected in pair manager already
|
||||
ApplyPauseStateForHandler(handler);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
|
||||
public PairOperationResult<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>> GetPairConnections(string ident)
|
||||
{
|
||||
PairHandlerEntry? entry;
|
||||
lock (_gate)
|
||||
{
|
||||
_entriesByIdent.TryGetValue(ident, out entry);
|
||||
}
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
return PairOperationResult<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>>.Fail($"No handler registered for {ident}.");
|
||||
}
|
||||
|
||||
var list = new List<(PairUniqueIdentifier, PairConnection)>();
|
||||
foreach (var pairIdent in entry.SnapshotPairs())
|
||||
{
|
||||
var result = _pairManager.GetPair(pairIdent.UserId);
|
||||
if (result.Success)
|
||||
{
|
||||
list.Add((pairIdent, result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
return PairOperationResult<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>>.Ok(list);
|
||||
}
|
||||
|
||||
private void ApplyPauseStateForHandler(IPairHandlerAdapter handler)
|
||||
{
|
||||
var pairs = _pairManager.GetPairsByIdent(handler.Ident);
|
||||
bool paused = pairs.Any(p => p.SelfToOtherPermissions.IsPaused() || p.OtherToSelfPermissions.IsPaused());
|
||||
handler.SetPaused(paused);
|
||||
}
|
||||
|
||||
internal bool TryGetHandler(string ident, out IPairHandlerAdapter? handler)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var success = _entriesByIdent.TryGetValue(ident, out var entry);
|
||||
handler = entry?.Handler;
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
internal IReadOnlyList<IPairHandlerAdapter> GetHandlerSnapshot()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _entriesByHandler.Keys.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
internal IReadOnlyCollection<PairUniqueIdentifier> GetRegisteredPairs(IPairHandlerAdapter handler)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_entriesByHandler.TryGetValue(handler, out var entry))
|
||||
{
|
||||
return entry.SnapshotPairs();
|
||||
}
|
||||
}
|
||||
|
||||
return Array.Empty<PairUniqueIdentifier>();
|
||||
}
|
||||
|
||||
internal void ReapplyAll(bool forced = false)
|
||||
{
|
||||
var handlers = GetHandlerSnapshot();
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
try
|
||||
{
|
||||
handler.ApplyLastReceivedData(forced);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to reapply cached data for {Ident}", handler.Ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void ResetAllHandlers()
|
||||
{
|
||||
List<IPairHandlerAdapter> handlers;
|
||||
lock (_gate)
|
||||
{
|
||||
handlers = _entriesByHandler.Keys.ToList();
|
||||
CancelAllInvisibleEvictions();
|
||||
_entriesByIdent.Clear();
|
||||
_entriesByHandler.Clear();
|
||||
}
|
||||
|
||||
CancelAllPendingCharacterData();
|
||||
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
try
|
||||
{
|
||||
handler.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to dispose handler for {Ident}", handler.Ident);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
List<IPairHandlerAdapter> handlers;
|
||||
lock (_gate)
|
||||
{
|
||||
handlers = _entriesByHandler.Keys.ToList();
|
||||
CancelAllInvisibleEvictions();
|
||||
_entriesByIdent.Clear();
|
||||
_entriesByHandler.Clear();
|
||||
}
|
||||
|
||||
CancelAllPendingCharacterData();
|
||||
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
handler.Dispose();
|
||||
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
||||
}
|
||||
}
|
||||
|
||||
private PairHandlerEntry GetOrCreateEntry(string ident)
|
||||
{
|
||||
if (_entriesByIdent.TryGetValue(ident, out var entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
var handler = _handlerFactory.Create(ident);
|
||||
entry = new PairHandlerEntry(ident, handler);
|
||||
_entriesByIdent[ident] = entry;
|
||||
_entriesByHandler[handler] = entry;
|
||||
return entry;
|
||||
}
|
||||
|
||||
private async Task RemoveAfterGracePeriodAsync(IPairHandlerAdapter handler)
|
||||
{
|
||||
await Task.Delay(_deletionGracePeriod).ConfigureAwait(false);
|
||||
|
||||
if (TryFinalizeHandlerRemoval(handler))
|
||||
{
|
||||
handler.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFinalizeHandlerRemoval(IPairHandlerAdapter handler)
|
||||
{
|
||||
string? ident = null;
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_entriesByHandler.TryGetValue(handler, out var entry) || entry.HasPairs)
|
||||
{
|
||||
handler.ScheduledForDeletion = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
ident = entry.Ident;
|
||||
_entriesByHandler.Remove(handler);
|
||||
_entriesByIdent.Remove(entry.Ident);
|
||||
}
|
||||
|
||||
if (ident is not null)
|
||||
{
|
||||
_pairPerformanceMetricsCache.Clear(ident);
|
||||
CancelPendingCharacterData(ident);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void QueuePendingCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
|
||||
{
|
||||
if (registration.CharacterIdent is null) return;
|
||||
|
||||
CancellationTokenSource? previous;
|
||||
CancellationTokenSource cts;
|
||||
|
||||
lock (_pendingGate)
|
||||
{
|
||||
_pendingCharacterData.TryGetValue(registration.CharacterIdent, out previous);
|
||||
previous?.Cancel();
|
||||
|
||||
cts = new CancellationTokenSource();
|
||||
_pendingCharacterData[registration.CharacterIdent] = cts;
|
||||
}
|
||||
|
||||
cts.CancelAfter(_handlerReadyTimeout);
|
||||
_ = Task.Run(() => WaitThenApplyPendingCharacterDataAsync(registration, dto, cts.Token, cts));
|
||||
}
|
||||
|
||||
private void CancelPendingCharacterData(string ident)
|
||||
{
|
||||
CancellationTokenSource? cts = null;
|
||||
lock (_pendingGate)
|
||||
{
|
||||
if (_pendingCharacterData.TryGetValue(ident, out cts))
|
||||
_pendingCharacterData.Remove(ident);
|
||||
}
|
||||
|
||||
cts?.Cancel();
|
||||
}
|
||||
|
||||
private void CancelAllPendingCharacterData()
|
||||
{
|
||||
List<CancellationTokenSource>? snapshot = null;
|
||||
lock (_pendingGate)
|
||||
{
|
||||
if (_pendingCharacterData.Count > 0)
|
||||
{
|
||||
snapshot = [.. _pendingCharacterData.Values];
|
||||
_pendingCharacterData.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (snapshot is null) return;
|
||||
foreach (var cts in snapshot) cts.Cancel();
|
||||
}
|
||||
|
||||
private async Task WaitThenApplyPendingCharacterDataAsync(
|
||||
PairRegistration registration,
|
||||
OnlineUserCharaDataDto dto,
|
||||
CancellationToken token,
|
||||
CancellationTokenSource source)
|
||||
{
|
||||
if (registration.CharacterIdent is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
if (TryGetHandler(registration.CharacterIdent, out var handler) && handler is not null && handler.Initialized)
|
||||
{
|
||||
handler.ApplyData(dto.CharaData);
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.Delay(_handlerReadyPollDelayMs, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// expected
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_pendingGate)
|
||||
{
|
||||
if (_pendingCharacterData.TryGetValue(registration.CharacterIdent, out var current) && ReferenceEquals(current, source))
|
||||
{
|
||||
_pendingCharacterData.Remove(registration.CharacterIdent);
|
||||
}
|
||||
}
|
||||
|
||||
source.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
294
LightlessSync/PlayerData/Pairs/PairLedger.cs
Normal file
294
LightlessSync/PlayerData/Pairs/PairLedger.cs
Normal file
@@ -0,0 +1,294 @@
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// keeps pair info for ui and reapplication
|
||||
/// </summary>
|
||||
public sealed class PairLedger : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairHandlerRegistry _registry;
|
||||
private readonly ILogger<PairLedger> _logger;
|
||||
private readonly object _metricsGate = new();
|
||||
private CancellationTokenSource? _ensureMetricsCts;
|
||||
|
||||
public PairLedger(
|
||||
ILogger<PairLedger> logger,
|
||||
LightlessMediator mediator,
|
||||
PairManager pairManager,
|
||||
PairHandlerRegistry registry) : base(logger, mediator)
|
||||
{
|
||||
_pairManager = pairManager;
|
||||
_registry = registry;
|
||||
_logger = logger;
|
||||
|
||||
Mediator.Subscribe<CutsceneEndMessage>(this, _ => ReapplyAll(forced: true));
|
||||
Mediator.Subscribe<GposeEndMessage>(this, _ => ReapplyAll());
|
||||
Mediator.Subscribe<PenumbraInitializedMessage>(this, _ => ReapplyAll(forced: true));
|
||||
Mediator.Subscribe<FileCacheInitializedMessage>(this, _ => ReapplyAll(forced: true));
|
||||
Mediator.Subscribe<DisconnectedMessage>(this, _ => Reset());
|
||||
Mediator.Subscribe<ConnectedMessage>(this, _ => ScheduleEnsureMetrics(TimeSpan.FromSeconds(2)));
|
||||
Mediator.Subscribe<HubReconnectedMessage>(this, _ => ScheduleEnsureMetrics(TimeSpan.FromSeconds(2)));
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, _ => ScheduleEnsureMetrics(TimeSpan.FromSeconds(2)));
|
||||
Mediator.Subscribe<VisibilityChange>(this, _ => EnsureMetricsForVisiblePairs());
|
||||
}
|
||||
|
||||
public bool IsPairVisible(PairUniqueIdentifier pairIdent)
|
||||
{
|
||||
var connectionResult = _pairManager.GetPair(pairIdent.UserId);
|
||||
if (!connectionResult.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var connection = connectionResult.Value;
|
||||
if (connection.Ident is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _registry.IsIdentVisible(connection.Ident);
|
||||
}
|
||||
|
||||
public IPairHandlerAdapter? GetHandler(PairUniqueIdentifier pairIdent)
|
||||
{
|
||||
var connectionResult = _pairManager.GetPair(pairIdent.UserId);
|
||||
if (!connectionResult.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var connection = connectionResult.Value;
|
||||
if (connection.Ident is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _registry.TryGetHandler(connection.Ident, out var handler) ? handler : null;
|
||||
}
|
||||
|
||||
public IReadOnlyList<PairConnection> GetVisiblePairs()
|
||||
{
|
||||
return _pairManager.GetAllPairs()
|
||||
.Select(kv => kv.Value)
|
||||
.Where(connection => connection.Ident is not null && _registry.IsIdentVisible(connection.Ident))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public IReadOnlyList<GroupFullInfoDto> GetAllGroupInfos()
|
||||
{
|
||||
return _pairManager.GetAllGroups()
|
||||
.Select(kv => kv.Value.GroupFullInfo)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, Syncshell> GetAllSyncshells()
|
||||
{
|
||||
return _pairManager.GetAllGroups();
|
||||
}
|
||||
|
||||
public void ReapplyAll(bool forced = false)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
_logger.LogTrace("Reapplying cached data for all handlers (forced: {Forced})", forced);
|
||||
}
|
||||
|
||||
_registry.ReapplyAll(forced);
|
||||
}
|
||||
|
||||
public void ReapplyPair(PairUniqueIdentifier pairIdent, bool forced = false)
|
||||
{
|
||||
var connectionResult = _pairManager.GetPair(pairIdent.UserId);
|
||||
if (!connectionResult.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var connection = connectionResult.Value;
|
||||
if (connection.Ident is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = _registry.ApplyLastReceivedData(pairIdent, connection.Ident, forced);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to reapply data for {UserId}: {Error}", pairIdent.UserId, result.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
_logger.LogTrace("Resetting pair handlers after disconnect.");
|
||||
}
|
||||
|
||||
CancelScheduledMetrics();
|
||||
}
|
||||
|
||||
public IReadOnlyList<PairDisplayEntry> GetAllEntries()
|
||||
{
|
||||
var groups = _pairManager.GetAllGroups();
|
||||
var list = new List<PairDisplayEntry>();
|
||||
foreach (var (userId, connection) in _pairManager.GetAllPairs())
|
||||
{
|
||||
var ident = new PairUniqueIdentifier(userId);
|
||||
IPairHandlerAdapter? handler = null;
|
||||
if (connection.Ident is not null)
|
||||
{
|
||||
_registry.TryGetHandler(connection.Ident, out handler);
|
||||
}
|
||||
|
||||
var groupInfos = connection.Groups.Keys
|
||||
.Select(gid =>
|
||||
{
|
||||
if (groups.TryGetValue(gid, out var shell))
|
||||
{
|
||||
return shell.GroupFullInfo;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.Where(dto => dto is not null)
|
||||
.Cast<GroupFullInfoDto>()
|
||||
.ToList();
|
||||
|
||||
list.Add(new PairDisplayEntry(ident, connection, groupInfos, handler));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public bool TryGetEntry(PairUniqueIdentifier ident, out PairDisplayEntry? entry)
|
||||
{
|
||||
entry = null;
|
||||
var connectionResult = _pairManager.GetPair(ident.UserId);
|
||||
if (!connectionResult.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var connection = connectionResult.Value;
|
||||
var groups = connection.Groups.Keys
|
||||
.Select(gid =>
|
||||
{
|
||||
var groupResult = _pairManager.GetGroup(gid);
|
||||
return groupResult.Success ? groupResult.Value.GroupFullInfo : null;
|
||||
})
|
||||
.Where(dto => dto is not null)
|
||||
.Cast<GroupFullInfoDto>()
|
||||
.ToList();
|
||||
|
||||
IPairHandlerAdapter? handler = null;
|
||||
if (connection.Ident is not null)
|
||||
{
|
||||
_registry.TryGetHandler(connection.Ident, out handler);
|
||||
}
|
||||
|
||||
entry = new PairDisplayEntry(ident, connection, groups, handler);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ScheduleEnsureMetrics(TimeSpan? delay = null)
|
||||
{
|
||||
lock (_metricsGate)
|
||||
{
|
||||
_ensureMetricsCts?.Cancel();
|
||||
var cts = new CancellationTokenSource();
|
||||
_ensureMetricsCts = cts;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (delay is { } d && d > TimeSpan.Zero)
|
||||
{
|
||||
await Task.Delay(d, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
EnsureMetricsForVisiblePairs();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_metricsGate)
|
||||
{
|
||||
if (_ensureMetricsCts == cts)
|
||||
{
|
||||
_ensureMetricsCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
cts.Dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelScheduledMetrics()
|
||||
{
|
||||
lock (_metricsGate)
|
||||
{
|
||||
_ensureMetricsCts?.Cancel();
|
||||
_ensureMetricsCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureMetricsForVisiblePairs()
|
||||
{
|
||||
var handlers = _registry.GetHandlerSnapshot();
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
if (!handler.IsVisible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handler.LastReceivedCharacterData is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handler.LastAppliedApproximateVRAMBytes >= 0
|
||||
&& handler.LastAppliedDataTris >= 0
|
||||
&& handler.LastAppliedApproximateEffectiveVRAMBytes >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handler.FetchPerformanceMetricsFromCache())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
handler.ApplyLastReceivedData(forced: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to ensure performance metrics for {Ident}", handler.Ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
CancelScheduledMetrics();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -1,408 +1,577 @@
|
||||
using Dalamud.Plugin.Services;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Comparer;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.Services.Events;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||
/// <summary>
|
||||
/// in memory state for pairs, groups, and syncshells
|
||||
/// </summary>
|
||||
public sealed class PairManager
|
||||
{
|
||||
private readonly ConcurrentDictionary<UserData, Pair> _allClientPairs = new(UserDataComparer.Instance);
|
||||
private readonly ConcurrentDictionary<GroupData, GroupFullInfoDto> _allGroups = new(GroupDataComparer.Instance);
|
||||
private readonly LightlessConfigService _configurationService;
|
||||
private readonly IContextMenu _dalamudContextMenu;
|
||||
private readonly PairFactory _pairFactory;
|
||||
private Lazy<List<Pair>> _directPairsInternal;
|
||||
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> _groupPairsInternal;
|
||||
private Lazy<Dictionary<Pair, List<GroupFullInfoDto>>> _pairsWithGroupsInternal;
|
||||
private readonly object _gate = new();
|
||||
private readonly Dictionary<string, PairConnection> _pairs = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, Syncshell> _groups = new(StringComparer.Ordinal);
|
||||
|
||||
public PairManager(ILogger<PairManager> logger, PairFactory pairFactory,
|
||||
LightlessConfigService configurationService, LightlessMediator mediator,
|
||||
IContextMenu dalamudContextMenu) : base(logger, mediator)
|
||||
public PairConnection? LastAddedUser { get; private set; }
|
||||
|
||||
public IReadOnlyDictionary<string, PairConnection> GetAllPairs()
|
||||
{
|
||||
_pairFactory = pairFactory;
|
||||
_configurationService = configurationService;
|
||||
_dalamudContextMenu = dalamudContextMenu;
|
||||
Mediator.Subscribe<DisconnectedMessage>(this, (_) => ClearPairs());
|
||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => ReapplyPairData());
|
||||
_directPairsInternal = DirectPairsLazy();
|
||||
_groupPairsInternal = GroupPairsLazy();
|
||||
_pairsWithGroupsInternal = PairsWithGroupsLazy();
|
||||
|
||||
_dalamudContextMenu.OnMenuOpened += DalamudContextMenuOnOnOpenGameObjectContextMenu;
|
||||
}
|
||||
|
||||
public List<Pair> DirectPairs => _directPairsInternal.Value;
|
||||
|
||||
public Dictionary<GroupFullInfoDto, List<Pair>> GroupPairs => _groupPairsInternal.Value;
|
||||
public Dictionary<GroupData, GroupFullInfoDto> Groups => _allGroups.ToDictionary(k => k.Key, k => k.Value);
|
||||
public Pair? LastAddedUser { get; internal set; }
|
||||
public Dictionary<Pair, List<GroupFullInfoDto>> PairsWithGroups => _pairsWithGroupsInternal.Value;
|
||||
|
||||
public void AddGroup(GroupFullInfoDto dto)
|
||||
{
|
||||
_allGroups[dto.Group] = dto;
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void AddGroupPair(GroupPairFullInfoDto dto)
|
||||
{
|
||||
if (!_allClientPairs.ContainsKey(dto.User))
|
||||
_allClientPairs[dto.User] = _pairFactory.Create(new UserFullPairDto(dto.User, API.Data.Enum.IndividualPairStatus.None,
|
||||
[dto.Group.GID], dto.SelfToOtherPermissions, dto.OtherToSelfPermissions));
|
||||
else _allClientPairs[dto.User].UserPair.Groups.Add(dto.GID);
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public Pair? GetPairByUID(string uid)
|
||||
{
|
||||
var existingPair = _allClientPairs.FirstOrDefault(f => f.Key.UID == uid);
|
||||
if (!Equals(existingPair, default(KeyValuePair<UserData, Pair>)))
|
||||
lock (_gate)
|
||||
{
|
||||
return existingPair.Value;
|
||||
return new Dictionary<string, PairConnection>(_pairs);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void AddUserPair(UserFullPairDto dto)
|
||||
public IReadOnlyDictionary<string, Syncshell> GetAllGroups()
|
||||
{
|
||||
if (!_allClientPairs.ContainsKey(dto.User))
|
||||
lock (_gate)
|
||||
{
|
||||
_allClientPairs[dto.User] = _pairFactory.Create(dto);
|
||||
return new Dictionary<string, Syncshell>(_groups);
|
||||
}
|
||||
else
|
||||
{
|
||||
_allClientPairs[dto.User].UserPair.IndividualPairStatus = dto.IndividualPairStatus;
|
||||
_allClientPairs[dto.User].ApplyLastReceivedData();
|
||||
}
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void AddUserPair(UserPairDto dto, bool addToLastAddedUser = true)
|
||||
public PairConnection? GetLastAddedUser()
|
||||
{
|
||||
if (!_allClientPairs.ContainsKey(dto.User))
|
||||
lock (_gate)
|
||||
{
|
||||
_allClientPairs[dto.User] = _pairFactory.Create(dto);
|
||||
return LastAddedUser;
|
||||
}
|
||||
else
|
||||
{
|
||||
addToLastAddedUser = false;
|
||||
}
|
||||
|
||||
_allClientPairs[dto.User].UserPair.IndividualPairStatus = dto.IndividualPairStatus;
|
||||
_allClientPairs[dto.User].UserPair.OwnPermissions = dto.OwnPermissions;
|
||||
_allClientPairs[dto.User].UserPair.OtherPermissions = dto.OtherPermissions;
|
||||
if (addToLastAddedUser)
|
||||
LastAddedUser = _allClientPairs[dto.User];
|
||||
_allClientPairs[dto.User].ApplyLastReceivedData();
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void ClearPairs()
|
||||
public void ClearLastAddedUser()
|
||||
{
|
||||
Logger.LogDebug("Clearing all Pairs");
|
||||
DisposePairs();
|
||||
_allClientPairs.Clear();
|
||||
_allGroups.Clear();
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public List<Pair> GetOnlineUserPairs() => _allClientPairs.Where(p => !string.IsNullOrEmpty(p.Value.GetPlayerNameHash())).Select(p => p.Value).ToList();
|
||||
|
||||
public int GetVisibleUserCount() => _allClientPairs.Count(p => p.Value.IsVisible);
|
||||
|
||||
public List<UserData> GetVisibleUsers() => [.. _allClientPairs.Where(p => p.Value.IsVisible).Select(p => p.Key)];
|
||||
|
||||
public void MarkPairOffline(UserData user)
|
||||
{
|
||||
if (_allClientPairs.TryGetValue(user, out var pair))
|
||||
lock (_gate)
|
||||
{
|
||||
Mediator.Publish(new ClearProfileDataMessage(pair.UserData));
|
||||
pair.MarkOffline();
|
||||
LastAddedUser = null;
|
||||
}
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void MarkPairOnline(OnlineUserIdentDto dto, bool sendNotif = true)
|
||||
public void ClearAll()
|
||||
{
|
||||
if (!_allClientPairs.ContainsKey(dto.User)) throw new InvalidOperationException("No user found for " + dto);
|
||||
|
||||
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
||||
|
||||
var pair = _allClientPairs[dto.User];
|
||||
if (pair.HasCachedPlayer)
|
||||
lock (_gate)
|
||||
{
|
||||
RecreateLazy();
|
||||
return;
|
||||
_pairs.Clear();
|
||||
_groups.Clear();
|
||||
LastAddedUser = null;
|
||||
}
|
||||
|
||||
if (sendNotif && _configurationService.Current.ShowOnlineNotifications
|
||||
&& (_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs && pair.IsDirectlyPaired && !pair.IsOneSidedPair
|
||||
|| !_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs)
|
||||
&& (_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs && !string.IsNullOrEmpty(pair.GetNote())
|
||||
|| !_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs))
|
||||
{
|
||||
string? note = pair.GetNote();
|
||||
var msg = !string.IsNullOrEmpty(note)
|
||||
? $"{note} ({pair.UserData.AliasOrUID}) is now online"
|
||||
: $"{pair.UserData.AliasOrUID} is now online";
|
||||
Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
pair.CreateCachedPlayer(dto);
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void ReceiveCharaData(OnlineUserCharaDataDto dto)
|
||||
public PairOperationResult<PairConnection> GetPair(string userId)
|
||||
{
|
||||
if (!_allClientPairs.TryGetValue(dto.User, out var pair)) throw new InvalidOperationException("No user found for " + dto.User);
|
||||
|
||||
Mediator.Publish(new EventMessage(new Event(pair.UserData, nameof(PairManager), EventSeverity.Informational, "Received Character Data")));
|
||||
_allClientPairs[dto.User].ApplyData(dto);
|
||||
}
|
||||
|
||||
public void RemoveGroup(GroupData data)
|
||||
{
|
||||
_allGroups.TryRemove(data, out _);
|
||||
|
||||
foreach (var item in _allClientPairs.ToList())
|
||||
lock (_gate)
|
||||
{
|
||||
item.Value.UserPair.Groups.Remove(data.GID);
|
||||
|
||||
if (!item.Value.HasAnyConnection())
|
||||
if (_pairs.TryGetValue(userId, out var connection))
|
||||
{
|
||||
item.Value.MarkOffline();
|
||||
_allClientPairs.TryRemove(item.Key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void RemoveGroupPair(GroupPairDto dto)
|
||||
{
|
||||
if (_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||
{
|
||||
pair.UserPair.Groups.Remove(dto.Group.GID);
|
||||
|
||||
if (!pair.HasAnyConnection())
|
||||
{
|
||||
pair.MarkOffline();
|
||||
_allClientPairs.TryRemove(dto.User, out _);
|
||||
}
|
||||
}
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void RemoveUserPair(UserDto dto)
|
||||
{
|
||||
if (_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||
{
|
||||
pair.UserPair.IndividualPairStatus = API.Data.Enum.IndividualPairStatus.None;
|
||||
|
||||
if (!pair.HasAnyConnection())
|
||||
{
|
||||
pair.MarkOffline();
|
||||
_allClientPairs.TryRemove(dto.User, out _);
|
||||
}
|
||||
}
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void SetGroupInfo(GroupInfoDto dto)
|
||||
{
|
||||
_allGroups[dto.Group].Group = dto.Group;
|
||||
_allGroups[dto.Group].Owner = dto.Owner;
|
||||
_allGroups[dto.Group].GroupPermissions = dto.GroupPermissions;
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void UpdatePairPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
if (!_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||
{
|
||||
throw new InvalidOperationException("No such pair for " + dto);
|
||||
}
|
||||
|
||||
if (pair.UserPair == null) throw new InvalidOperationException("No direct pair for " + dto);
|
||||
|
||||
if (pair.UserPair.OtherPermissions.IsPaused() != dto.Permissions.IsPaused())
|
||||
{
|
||||
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
||||
}
|
||||
|
||||
pair.UserPair.OtherPermissions = dto.Permissions;
|
||||
|
||||
Logger.LogTrace("Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}",
|
||||
pair.UserPair.OtherPermissions.IsPaused(),
|
||||
pair.UserPair.OtherPermissions.IsDisableAnimations(),
|
||||
pair.UserPair.OtherPermissions.IsDisableSounds(),
|
||||
pair.UserPair.OtherPermissions.IsDisableVFX());
|
||||
|
||||
if (!pair.IsPaused)
|
||||
pair.ApplyLastReceivedData();
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
public void UpdateSelfPairPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
if (!_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||
{
|
||||
throw new InvalidOperationException("No such pair for " + dto);
|
||||
}
|
||||
|
||||
if (pair.UserPair.OwnPermissions.IsPaused() != dto.Permissions.IsPaused())
|
||||
{
|
||||
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
||||
}
|
||||
|
||||
pair.UserPair.OwnPermissions = dto.Permissions;
|
||||
|
||||
Logger.LogTrace("Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}",
|
||||
pair.UserPair.OwnPermissions.IsPaused(),
|
||||
pair.UserPair.OwnPermissions.IsDisableAnimations(),
|
||||
pair.UserPair.OwnPermissions.IsDisableSounds(),
|
||||
pair.UserPair.OwnPermissions.IsDisableVFX());
|
||||
|
||||
if (!pair.IsPaused)
|
||||
pair.ApplyLastReceivedData();
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
internal void ReceiveUploadStatus(UserDto dto)
|
||||
{
|
||||
if (_allClientPairs.TryGetValue(dto.User, out var existingPair) && existingPair.IsVisible)
|
||||
{
|
||||
existingPair.SetIsUploading();
|
||||
}
|
||||
}
|
||||
|
||||
internal void SetGroupPairStatusInfo(GroupPairUserInfoDto dto)
|
||||
{
|
||||
_allGroups[dto.Group].GroupPairUserInfos[dto.UID] = dto.GroupUserInfo;
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
internal void SetGroupPermissions(GroupPermissionDto dto)
|
||||
{
|
||||
_allGroups[dto.Group].GroupPermissions = dto.Permissions;
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
internal void SetGroupStatusInfo(GroupPairUserInfoDto dto)
|
||||
{
|
||||
_allGroups[dto.Group].GroupUserInfo = dto.GroupUserInfo;
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
internal void UpdateGroupPairPermissions(GroupPairUserPermissionDto dto)
|
||||
{
|
||||
_allGroups[dto.Group].GroupUserPermissions = dto.GroupPairPermissions;
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
internal void UpdateIndividualPairStatus(UserIndividualPairStatusDto dto)
|
||||
{
|
||||
if (_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||
{
|
||||
pair.UserPair.IndividualPairStatus = dto.IndividualPairStatus;
|
||||
RecreateLazy();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
_dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu;
|
||||
|
||||
DisposePairs();
|
||||
}
|
||||
|
||||
private void DalamudContextMenuOnOnOpenGameObjectContextMenu(Dalamud.Game.Gui.ContextMenu.IMenuOpenedArgs args)
|
||||
{
|
||||
if (args.MenuType == Dalamud.Game.Gui.ContextMenu.ContextMenuType.Inventory) return;
|
||||
if (!_configurationService.Current.EnableRightClickMenus) return;
|
||||
|
||||
foreach (var pair in _allClientPairs.Where((p => p.Value.IsVisible)))
|
||||
{
|
||||
pair.Value.AddContextMenu(args);
|
||||
}
|
||||
}
|
||||
|
||||
private Lazy<List<Pair>> DirectPairsLazy() => new(() => _allClientPairs.Select(k => k.Value)
|
||||
.Where(k => k.IndividualPairStatus != API.Data.Enum.IndividualPairStatus.None).ToList());
|
||||
|
||||
private void DisposePairs()
|
||||
{
|
||||
Logger.LogDebug("Disposing all Pairs");
|
||||
Parallel.ForEach(_allClientPairs, item =>
|
||||
{
|
||||
item.Value.MarkOffline(wait: false);
|
||||
});
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
|
||||
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> GroupPairsLazy()
|
||||
{
|
||||
return new Lazy<Dictionary<GroupFullInfoDto, List<Pair>>>(() =>
|
||||
{
|
||||
Dictionary<GroupFullInfoDto, List<Pair>> outDict = [];
|
||||
foreach (var group in _allGroups)
|
||||
{
|
||||
outDict[group.Value] = _allClientPairs.Select(p => p.Value).Where(p => p.UserPair.Groups.Exists(g => GroupDataComparer.Instance.Equals(group.Key, new(g)))).ToList();
|
||||
}
|
||||
return outDict;
|
||||
});
|
||||
}
|
||||
|
||||
private Lazy<Dictionary<Pair, List<GroupFullInfoDto>>> PairsWithGroupsLazy()
|
||||
{
|
||||
return new Lazy<Dictionary<Pair, List<GroupFullInfoDto>>>(() =>
|
||||
{
|
||||
Dictionary<Pair, List<GroupFullInfoDto>> outDict = [];
|
||||
|
||||
foreach (var pair in _allClientPairs.Select(k => k.Value))
|
||||
{
|
||||
outDict[pair] = _allGroups.Where(k => pair.UserPair.Groups.Contains(k.Key.GID, StringComparer.Ordinal)).Select(k => k.Value).ToList();
|
||||
return PairOperationResult<PairConnection>.Ok(connection);
|
||||
}
|
||||
|
||||
return outDict;
|
||||
});
|
||||
}
|
||||
|
||||
private void ReapplyPairData()
|
||||
{
|
||||
foreach (var pair in _allClientPairs.Select(k => k.Value))
|
||||
{
|
||||
pair.ApplyLastReceivedData(forced: true);
|
||||
return PairOperationResult<PairConnection>.Fail($"Pair {userId} not found.");
|
||||
}
|
||||
}
|
||||
|
||||
private void RecreateLazy()
|
||||
public bool TryGetPair(string userId, [NotNullWhen(true)] out PairConnection? connection)
|
||||
{
|
||||
_directPairsInternal = DirectPairsLazy();
|
||||
_groupPairsInternal = GroupPairsLazy();
|
||||
_pairsWithGroupsInternal = PairsWithGroupsLazy();
|
||||
Mediator.Publish(new RefreshUiMessage());
|
||||
lock (_gate)
|
||||
{
|
||||
return _pairs.TryGetValue(userId, out connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult<Syncshell> GetGroup(string groupId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_groups.TryGetValue(groupId, out var shell))
|
||||
{
|
||||
return PairOperationResult<Syncshell>.Ok(shell);
|
||||
}
|
||||
|
||||
return PairOperationResult<Syncshell>.Fail($"Group {groupId} not found.");
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<PairConnection> GetDirectPairs()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _pairs.Values.Where(p => p.IsDirectlyPaired).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<PairConnection> GetPairsByIdent(string ident)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _pairs.Values
|
||||
.Where(p => p.Ident is not null && string.Equals(p.Ident, ident, StringComparison.Ordinal))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<Syncshell> GetOwnedOrModeratedShells(string currentUserUid)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _groups.Values
|
||||
.Where(s =>
|
||||
string.Equals(s.GroupFullInfo.Owner.UID, currentUserUid, StringComparison.OrdinalIgnoreCase)
|
||||
|| s.GroupFullInfo.GroupUserInfo.HasFlag(GroupPairUserInfo.IsModerator))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult<UserPermissions> GetPairCombinedPermissions(string userId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_pairs.TryGetValue(userId, out var connection))
|
||||
{
|
||||
return PairOperationResult<UserPermissions>.Fail($"Pair {userId} not found.");
|
||||
}
|
||||
|
||||
var combined = connection.SelfToOtherPermissions | connection.OtherToSelfPermissions;
|
||||
return PairOperationResult<UserPermissions>.Ok(combined);
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult<PairRegistration> MarkOnline(OnlineUserIdentDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_pairs.TryGetValue(dto.User.UID, out var connection))
|
||||
{
|
||||
connection = GetOrCreatePair(dto.User);
|
||||
}
|
||||
|
||||
connection.SetOnline(dto.Ident);
|
||||
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), dto.Ident));
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult<PairRegistration> MarkOffline(UserData user)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_pairs.TryGetValue(user.UID, out var connection))
|
||||
{
|
||||
return PairOperationResult<PairRegistration>.Fail($"Pair {user.UID} not found.");
|
||||
}
|
||||
|
||||
connection.SetOffline();
|
||||
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(user.UID), connection.Ident));
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult<PairRegistration> AddOrUpdateIndividual(UserPairDto dto, bool markAsLastAddedUser = true)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var connection = GetOrCreatePair(dto.User, out var created);
|
||||
connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions);
|
||||
connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);
|
||||
|
||||
if (connection.Ident is null)
|
||||
{
|
||||
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), null));
|
||||
}
|
||||
|
||||
if (created && markAsLastAddedUser)
|
||||
{
|
||||
LastAddedUser = connection;
|
||||
}
|
||||
|
||||
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident));
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult<PairRegistration> AddOrUpdateIndividual(UserFullPairDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var connection = GetOrCreatePair(dto.User, out _);
|
||||
connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions);
|
||||
connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);
|
||||
|
||||
var removedGroups = connection.Groups.Keys.Where(k => !dto.Groups.Contains(k, StringComparer.Ordinal)).ToList();
|
||||
foreach (var groupId in removedGroups)
|
||||
{
|
||||
connection.RemoveGroupRelationship(groupId);
|
||||
if (_groups.TryGetValue(groupId, out var shell))
|
||||
{
|
||||
shell.Users.Remove(dto.User.UID);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var groupId in dto.Groups)
|
||||
{
|
||||
connection.EnsureGroupRelationship(groupId, null);
|
||||
if (_groups.TryGetValue(groupId, out var shell))
|
||||
{
|
||||
shell.Users[dto.User.UID] = connection;
|
||||
}
|
||||
}
|
||||
|
||||
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident));
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult<PairRegistration?> RemoveIndividual(UserDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_pairs.TryGetValue(dto.User.UID, out var connection))
|
||||
{
|
||||
return PairOperationResult<PairRegistration?>.Fail($"Pair {dto.User.UID} not found.");
|
||||
}
|
||||
|
||||
connection.UpdateStatus(null);
|
||||
var registration = TryRemovePairIfNoConnection(connection);
|
||||
return PairOperationResult<PairRegistration?>.Ok(registration);
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult<PairRegistration> SetPairOtherToSelfPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_pairs.TryGetValue(dto.User.UID, out var connection))
|
||||
{
|
||||
return PairOperationResult<PairRegistration>.Fail($"Pair {dto.User.UID} not found.");
|
||||
}
|
||||
|
||||
connection.UpdatePermissions(connection.SelfToOtherPermissions, dto.Permissions);
|
||||
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident));
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult<PairRegistration> SetPairSelfToOtherPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_pairs.TryGetValue(dto.User.UID, out var connection))
|
||||
{
|
||||
return PairOperationResult<PairRegistration>.Fail($"Pair {dto.User.UID} not found.");
|
||||
}
|
||||
|
||||
connection.UpdatePermissions(dto.Permissions, connection.OtherToSelfPermissions);
|
||||
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident));
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult SetIndividualStatus(UserIndividualPairStatusDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_pairs.TryGetValue(dto.User.UID, out var connection))
|
||||
{
|
||||
return PairOperationResult.Fail($"Pair {dto.User.UID} not found.");
|
||||
}
|
||||
|
||||
connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);
|
||||
_ = TryRemovePairIfNoConnection(connection);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult AddOrUpdateGroupPair(GroupPairFullInfoDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var shell = GetOrCreateShell(dto.Group);
|
||||
var connection = GetOrCreatePair(dto.User);
|
||||
|
||||
var groupInfo = shell.GroupFullInfo.GroupPairUserInfos.GetValueOrDefault(dto.User.UID, GroupPairUserInfo.None);
|
||||
connection.EnsureGroupRelationship(dto.Group.GID, groupInfo == GroupPairUserInfo.None ? null : groupInfo);
|
||||
connection.UpdatePermissions(dto.SelfToOtherPermissions, dto.OtherToSelfPermissions);
|
||||
|
||||
shell.Users[dto.User.UID] = connection;
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult<PairRegistration?> RemoveGroupPair(GroupPairDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_groups.TryGetValue(dto.GID, out var shell))
|
||||
{
|
||||
shell.Users.Remove(dto.User.UID);
|
||||
}
|
||||
|
||||
PairRegistration? registration = null;
|
||||
if (_pairs.TryGetValue(dto.User.UID, out var connection))
|
||||
{
|
||||
connection.RemoveGroupRelationship(dto.GID);
|
||||
registration = TryRemovePairIfNoConnection(connection);
|
||||
}
|
||||
|
||||
return PairOperationResult<PairRegistration?>.Ok(registration);
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult<IReadOnlyList<PairRegistration>> RemoveGroup(string groupId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_groups.Remove(groupId, out var shell))
|
||||
{
|
||||
return PairOperationResult<IReadOnlyList<PairRegistration>>.Fail($"Group {groupId} not found.");
|
||||
}
|
||||
|
||||
var removed = new List<PairRegistration>();
|
||||
foreach (var connection in shell.Users.Values.ToList())
|
||||
{
|
||||
connection.RemoveGroupRelationship(groupId);
|
||||
var registration = TryRemovePairIfNoConnection(connection);
|
||||
if (registration is not null)
|
||||
{
|
||||
removed.Add(registration);
|
||||
}
|
||||
}
|
||||
|
||||
return PairOperationResult<IReadOnlyList<PairRegistration>>.Ok(removed);
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult AddGroup(GroupFullInfoDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_groups.TryGetValue(dto.Group.GID, out var shell))
|
||||
{
|
||||
shell = new Syncshell(dto);
|
||||
_groups[dto.Group.GID] = shell;
|
||||
}
|
||||
else
|
||||
{
|
||||
shell.Update(dto);
|
||||
shell.Users.Clear();
|
||||
}
|
||||
|
||||
foreach (var (userId, info) in dto.GroupPairUserInfos)
|
||||
{
|
||||
if (_pairs.TryGetValue(userId, out var connection))
|
||||
{
|
||||
connection.EnsureGroupRelationship(dto.Group.GID, info == GroupPairUserInfo.None ? null : info);
|
||||
shell.Users[userId] = connection;
|
||||
}
|
||||
}
|
||||
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult UpdateGroupInfo(GroupInfoDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_groups.TryGetValue(dto.Group.GID, out var shell))
|
||||
{
|
||||
return PairOperationResult.Fail($"Group {dto.Group.GID} not found.");
|
||||
}
|
||||
|
||||
var updated = new GroupFullInfoDto(
|
||||
dto.Group,
|
||||
dto.Owner,
|
||||
dto.GroupPermissions,
|
||||
shell.GroupFullInfo.GroupUserPermissions,
|
||||
shell.GroupFullInfo.GroupUserInfo,
|
||||
new Dictionary<string, GroupPairUserInfo>(shell.GroupFullInfo.GroupPairUserInfos, StringComparer.Ordinal),
|
||||
0);
|
||||
|
||||
shell.Update(updated);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult UpdateGroupPairPermissions(GroupPairUserPermissionDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_groups.TryGetValue(dto.Group.GID, out var shell))
|
||||
{
|
||||
return PairOperationResult.Fail($"Group {dto.Group.GID} not found.");
|
||||
}
|
||||
|
||||
var updated = shell.GroupFullInfo with { GroupUserPermissions = dto.GroupPairPermissions };
|
||||
shell.Update(updated);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult UpdateGroupPermissions(GroupPermissionDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_groups.TryGetValue(dto.Group.GID, out var shell))
|
||||
{
|
||||
return PairOperationResult.Fail($"Group {dto.Group.GID} not found.");
|
||||
}
|
||||
|
||||
var updated = shell.GroupFullInfo with { GroupPermissions = dto.Permissions };
|
||||
shell.Update(updated);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult UpdateGroupPairStatus(GroupPairUserInfoDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_pairs.TryGetValue(dto.UID, out var connection))
|
||||
{
|
||||
connection.EnsureGroupRelationship(dto.GID, dto.GroupUserInfo == GroupPairUserInfo.None ? null : dto.GroupUserInfo);
|
||||
}
|
||||
|
||||
if (_groups.TryGetValue(dto.GID, out var shell))
|
||||
{
|
||||
var infos = new Dictionary<string, GroupPairUserInfo>(shell.GroupFullInfo.GroupPairUserInfos, StringComparer.Ordinal)
|
||||
{
|
||||
[dto.UID] = dto.GroupUserInfo
|
||||
};
|
||||
var updated = shell.GroupFullInfo with { GroupPairUserInfos = infos };
|
||||
shell.Update(updated);
|
||||
}
|
||||
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult UpdateGroupStatus(GroupPairUserInfoDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_groups.TryGetValue(dto.GID, out var shell))
|
||||
{
|
||||
return PairOperationResult.Fail($"Group {dto.GID} not found.");
|
||||
}
|
||||
|
||||
var updated = shell.GroupFullInfo with { GroupUserInfo = dto.GroupUserInfo };
|
||||
shell.Update(updated);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult UpdateOtherPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_pairs.TryGetValue(dto.User.UID, out var connection))
|
||||
{
|
||||
return PairOperationResult.Fail($"Pair {dto.User.UID} not found.");
|
||||
}
|
||||
|
||||
connection.UpdatePermissions(connection.SelfToOtherPermissions, dto.Permissions);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult UpdateSelfPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_pairs.TryGetValue(dto.User.UID, out var connection))
|
||||
{
|
||||
return PairOperationResult.Fail($"Pair {dto.User.UID} not found.");
|
||||
}
|
||||
|
||||
connection.UpdatePermissions(dto.Permissions, connection.OtherToSelfPermissions);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
private PairConnection GetOrCreatePair(UserData user)
|
||||
{
|
||||
return GetOrCreatePair(user, out _);
|
||||
}
|
||||
|
||||
private PairConnection GetOrCreatePair(UserData user, out bool created)
|
||||
{
|
||||
if (_pairs.TryGetValue(user.UID, out var connection))
|
||||
{
|
||||
created = false;
|
||||
return connection;
|
||||
}
|
||||
|
||||
connection = new PairConnection(user);
|
||||
_pairs[user.UID] = connection;
|
||||
created = true;
|
||||
return connection;
|
||||
}
|
||||
|
||||
private Syncshell GetOrCreateShell(GroupData group)
|
||||
{
|
||||
if (_groups.TryGetValue(group.GID, out var shell))
|
||||
{
|
||||
return shell;
|
||||
}
|
||||
|
||||
var placeholder = new GroupFullInfoDto(
|
||||
group,
|
||||
new UserData(string.Empty),
|
||||
GroupPermissions.NoneSet,
|
||||
GroupUserPreferredPermissions.NoneSet,
|
||||
GroupPairUserInfo.None,
|
||||
new Dictionary<string, GroupPairUserInfo>(StringComparer.Ordinal),
|
||||
0);
|
||||
|
||||
shell = new Syncshell(placeholder);
|
||||
_groups[group.GID] = shell;
|
||||
return shell;
|
||||
}
|
||||
|
||||
private PairRegistration? TryRemovePairIfNoConnection(PairConnection connection)
|
||||
{
|
||||
if (connection.HasAnyConnection)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (connection.IsOnline)
|
||||
{
|
||||
connection.SetOffline();
|
||||
}
|
||||
|
||||
var userId = connection.User.UID;
|
||||
_pairs.Remove(userId);
|
||||
foreach (var shell in _groups.Values)
|
||||
{
|
||||
shell.Users.Remove(userId);
|
||||
}
|
||||
|
||||
return new PairRegistration(new PairUniqueIdentifier(userId), connection.Ident);
|
||||
}
|
||||
|
||||
public static PairConnection CreateFromFullData(UserFullPairDto dto)
|
||||
{
|
||||
var connection = new PairConnection(dto.User);
|
||||
connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions);
|
||||
connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);
|
||||
|
||||
foreach (var groupId in dto.Groups)
|
||||
{
|
||||
connection.EnsureGroupRelationship(groupId, null);
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
public static PairConnection CreateFromPartialData(UserPairDto dto)
|
||||
{
|
||||
var connection = new PairConnection(dto.User);
|
||||
connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions);
|
||||
connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);
|
||||
return connection;
|
||||
}
|
||||
|
||||
public static GroupPairRelationship CreateGroupPairRelationshipFromFullInfo(string userUid, GroupFullInfoDto fullInfo)
|
||||
{
|
||||
return new GroupPairRelationship(fullInfo.Group.GID,
|
||||
fullInfo.GroupPairUserInfos.TryGetValue(userUid, out var info) && info != GroupPairUserInfo.None
|
||||
? info
|
||||
: null);
|
||||
}
|
||||
}
|
||||
|
||||
220
LightlessSync/PlayerData/Pairs/PairModels.cs
Normal file
220
LightlessSync/PlayerData/Pairs/PairModels.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// core models for the pair system
|
||||
/// </summary>
|
||||
public sealed class PairState
|
||||
{
|
||||
public CharacterData? CharacterData { get; set; }
|
||||
public Guid? TemporaryCollectionId { get; set; }
|
||||
|
||||
public bool IsEmpty => CharacterData is null && (TemporaryCollectionId is null || TemporaryCollectionId == Guid.Empty);
|
||||
}
|
||||
|
||||
public readonly record struct PairUniqueIdentifier(string UserId);
|
||||
|
||||
/// <summary>
|
||||
/// link between a pair id and character ident
|
||||
/// </summary>
|
||||
public sealed record PairRegistration(PairUniqueIdentifier PairIdent, string? CharacterIdent);
|
||||
|
||||
/// <summary>
|
||||
/// per group membership info for a pair
|
||||
/// </summary>
|
||||
public sealed class GroupPairRelationship
|
||||
{
|
||||
public GroupPairRelationship(string groupId, GroupPairUserInfo? info)
|
||||
{
|
||||
GroupId = groupId;
|
||||
UserInfo = info;
|
||||
}
|
||||
|
||||
public string GroupId { get; }
|
||||
public GroupPairUserInfo? UserInfo { get; private set; }
|
||||
|
||||
public void SetUserInfo(GroupPairUserInfo? info)
|
||||
{
|
||||
UserInfo = info;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// runtime view of a single pair connection
|
||||
/// </summary>
|
||||
public sealed class PairConnection
|
||||
{
|
||||
public PairConnection(UserData user)
|
||||
{
|
||||
User = user;
|
||||
Groups = new Dictionary<string, GroupPairRelationship>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public UserData User { get; }
|
||||
public bool IsOnline { get; private set; }
|
||||
public string? Ident { get; private set; }
|
||||
public UserPermissions SelfToOtherPermissions { get; private set; } = UserPermissions.NoneSet;
|
||||
public UserPermissions OtherToSelfPermissions { get; private set; } = UserPermissions.NoneSet;
|
||||
public IndividualPairStatus? IndividualPairStatus { get; private set; }
|
||||
public Dictionary<string, GroupPairRelationship> Groups { get; }
|
||||
|
||||
public bool IsPaused => SelfToOtherPermissions.IsPaused();
|
||||
public bool IsDirectlyPaired => IndividualPairStatus is not null && IndividualPairStatus != API.Data.Enum.IndividualPairStatus.None;
|
||||
public bool IsOneSided => IndividualPairStatus == API.Data.Enum.IndividualPairStatus.OneSided;
|
||||
public bool HasAnyConnection => IsDirectlyPaired || Groups.Count > 0;
|
||||
|
||||
public void SetOnline(string? ident)
|
||||
{
|
||||
IsOnline = true;
|
||||
Ident = ident;
|
||||
}
|
||||
|
||||
public void SetOffline()
|
||||
{
|
||||
IsOnline = false;
|
||||
}
|
||||
|
||||
public void UpdatePermissions(UserPermissions own, UserPermissions other)
|
||||
{
|
||||
SelfToOtherPermissions = own;
|
||||
OtherToSelfPermissions = other;
|
||||
}
|
||||
|
||||
public void UpdateStatus(IndividualPairStatus? status)
|
||||
{
|
||||
IndividualPairStatus = status;
|
||||
}
|
||||
|
||||
public void EnsureGroupRelationship(string groupId, GroupPairUserInfo? info)
|
||||
{
|
||||
if (Groups.TryGetValue(groupId, out var relationship))
|
||||
{
|
||||
relationship.SetUserInfo(info);
|
||||
}
|
||||
else
|
||||
{
|
||||
Groups[groupId] = new GroupPairRelationship(groupId, info);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveGroupRelationship(string groupId)
|
||||
{
|
||||
Groups.Remove(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// syncshell metadata plus member connections
|
||||
/// </summary>
|
||||
public sealed class Syncshell
|
||||
{
|
||||
public Syncshell(GroupFullInfoDto dto)
|
||||
{
|
||||
GroupFullInfo = dto;
|
||||
Users = new Dictionary<string, PairConnection>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public GroupFullInfoDto GroupFullInfo { get; private set; }
|
||||
public Dictionary<string, PairConnection> Users { get; }
|
||||
|
||||
public void Update(GroupFullInfoDto dto)
|
||||
{
|
||||
GroupFullInfo = dto;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// simple success/failure result
|
||||
/// </summary>
|
||||
public readonly struct PairOperationResult
|
||||
{
|
||||
private PairOperationResult(bool success, string? error)
|
||||
{
|
||||
Success = success;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public bool Success { get; }
|
||||
public string? Error { get; }
|
||||
|
||||
public static PairOperationResult Ok() => new(true, null);
|
||||
|
||||
public static PairOperationResult Fail(string error) => new(false, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// typed success/failure result
|
||||
/// </summary>
|
||||
public readonly struct PairOperationResult<T>
|
||||
{
|
||||
private PairOperationResult(bool success, T value, string? error)
|
||||
{
|
||||
Success = success;
|
||||
Value = value;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public bool Success { get; }
|
||||
public T Value { get; }
|
||||
public string? Error { get; }
|
||||
|
||||
public static PairOperationResult<T> Ok(T value) => new(true, value, null);
|
||||
|
||||
public static PairOperationResult<T> Fail(string error) => new(false, default!, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// state of which optional plugin warnings were shown
|
||||
/// </summary>
|
||||
public record OptionalPluginWarning
|
||||
{
|
||||
public bool ShownHeelsWarning { get; set; } = false;
|
||||
public bool ShownCustomizePlusWarning { get; set; } = false;
|
||||
public bool ShownHonorificWarning { get; set; } = false;
|
||||
public bool ShownMoodlesWarning { get; set; } = false;
|
||||
public bool ShowPetNicknamesWarning { get; set; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// tracks the handler registered pairs for an ident
|
||||
/// </summary>
|
||||
internal sealed class PairHandlerEntry
|
||||
{
|
||||
private readonly HashSet<PairUniqueIdentifier> _pairs = new();
|
||||
|
||||
public PairHandlerEntry(string ident, IPairHandlerAdapter handler)
|
||||
{
|
||||
Ident = ident;
|
||||
Handler = handler;
|
||||
}
|
||||
|
||||
public string Ident { get; }
|
||||
public IPairHandlerAdapter Handler { get; }
|
||||
|
||||
public bool HasPairs => _pairs.Count > 0;
|
||||
public int PairCount => _pairs.Count;
|
||||
|
||||
public void AddPair(PairUniqueIdentifier pair)
|
||||
{
|
||||
_pairs.Add(pair);
|
||||
}
|
||||
|
||||
public bool RemovePair(PairUniqueIdentifier pair)
|
||||
{
|
||||
return _pairs.Remove(pair);
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<PairUniqueIdentifier> SnapshotPairs()
|
||||
{
|
||||
if (_pairs.Count == 0)
|
||||
{
|
||||
return Array.Empty<PairUniqueIdentifier>();
|
||||
}
|
||||
|
||||
return _pairs.ToArray();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user