Compare commits

...

45 Commits

Author SHA1 Message Date
e75dd86fdb Update .gitea/workflows/lightless-tag-and-release.yml
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 31s
2025-10-05 08:18:33 +02:00
716d7a54d9 Update .gitea/workflows/lightless-tag-and-release.yml
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 33s
2025-10-05 08:10:21 +02:00
701ccaffe4 Update .gitea/workflows/lightless-tag-and-release.yml
Some checks failed
Tag and Release Lightless / tag-and-release (push) Failing after 32s
2025-10-05 08:06:59 +02:00
ee8d05ca7a Update .gitea/workflows/lightless-tag-and-release.yml
Some checks failed
Tag and Release Lightless / tag-and-release (push) Failing after 31s
2025-10-05 08:04:34 +02:00
fd5522b90a Update .gitea/workflows/lightless-tag-and-release.yml
Some checks failed
Tag and Release Lightless / tag-and-release (push) Failing after 34s
2025-10-05 07:59:01 +02:00
7672f147f5 1.12.0 - LightFinder + Bugfixes + Other fixes from 1.11.X (#39)
Some checks failed
Tag and Release Lightless / tag-and-release (push) Failing after 32s
![image.png](/attachments/c984e826-254e-4ce3-af92-4e795ea717ab)

Lightless 1.12.0 is HERE! In this major update, we are introducing something we've been working on and testing for the last couple of weeks. In this update we are introducing a new (**OPTIONAL**) feature called **LightFinder**! We took inspiration from FFXIV's very own Party Finder and decided to implement something that allows users to not only look for fellow Lightless users, but also put up their Syncshell for others looking to join a sync community!

When you enable LightFinder, you will be visible to other LightFinder users for **3 hours** or when you want to disabled it. When the 3 hours are up, you can either leave it disabled or enable it again for another 3 hours. The tag shown above will show above your nameplate, and you will be able to receive pair requests in your UI from other users with LightFinder enabled without having to input their uid!

![image.png](/attachments/91a7ce60-7771-49d7-bae8-6d7a67e46fa3)

Are you at a Venue? In Limsa? Partying in the streets of Uldah? If you're looking for fellow Lightless users you can now enable LightFinder and you will be shown to others who also have LightFinder enabled!

Looking for a Syncshell to join? Enable LightFinder to see what SyncShells are available to join!

Want to advertise your Syncshell? Choose the syncshell you want to put up in LightFinder and enable LightFinder.

**IMPORTANT: We want to stress the fact that, if you just want to sync with just your friends and no one else, this does not take that away. No one will know you use Lightless unless you choose to tell them, or use this **OPTIONAL** feature. Your privacy is still maintained if you don't want to use the feature.**

# Major Changes

## LightFinder - **OPTIONAL FEATURE** - **DOES NOT AFFECT YOU IF YOU DON'T WANT TO USE IT**

![image.png](/attachments/9a1e7455-f9b3-4e3a-9ec5-5bc74ed43c26)

* New **OPTIONAL** syncing feature where one can enable something like a Party Finder so that other Lightless users in the area can see you are looking for people to sync with. Enable it by clicking the compass button and then the `Enable LightFinder` button.

![image.png](/attachments/7be5c1fc-439f-43fc-bb1e-8ae32ab5e52e)

* [L] Send Pair Request has been added to player context menus. You should still be able to send a request without Lightfinder on BUT you will need to know the other player is using Lightless and have them send a pair request back.

![image.png](/attachments/75e0d600-baf9-43d5-af37-e86b6a1e209b)

* When in LightFinder mode, for X mins you will be visible to all Lightless Users WHO ALSO HAVE LIGHTFINDER ON and will receive notifications of people wanting to pair with you. If you are the person using LightFinder, you understand the risks of pairing with someone you don't know. If you are the person sending a request to someone with LightFinder on, you also understand the risks of pairing with someone you don't know. **AGAIN, THIS IS OPTIONAL.**

* When in LightFinder mode, you can also put up a Syncshell you own on the Syncshell Finder so that others can easily find it and join. This has to be done prior to enabling LightFinder.

![image.png](/attachments/1929d8e0-778d-4ced-9bf0-f350887980db)

* Syncshell Finder allows you to join Syncshells that are indexed by LightFinder

![image.png](/attachments/fef7b786-b1a0-4967-a4a9-886cdddc1a61)

# Minor Changes

* Vanity addition to our supporters: On top of vanity ids you can find a fun addition under Your User Settings -> Edit Lightless Profile -> Vanity Settings to change the colour of your name in the lightless ui. This will be shown to all users.
![image.png](/attachments/9b950c07-6b27-4bd1-a0d4-5346396b23e2)

* Pairing nameplate colour override can also now override FC tag (new option_

* Bunch of UI fixes, updates, and changes

* kdb is now a whitelisted filetype that can upload

Co-authored-by: CakeAndBanana <admin@cakeandbanana.nl>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: choco <thijmenhogenkamp@gmail.com>
Co-authored-by: choco <choco@noreply.git.lightless-sync.org>
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #39
2025-10-05 07:48:32 +02:00
defnotken
5dce1977c7 1.11.12 - Fix cache
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 33s
2025-09-15 23:56:36 -05:00
defnotken
e396d2cf46 1.11.11 - Disable Show groups for now
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 32s
2025-09-15 21:41:22 -05:00
c5e6c06005 1.11.10 (#31)
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 34s
Co-authored-by: CakeAndBanana <admin@cakeandbanana.nl>
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #31
2025-09-16 04:14:32 +02:00
14ec282f21 Version Bump
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 33s
2025-09-13 17:16:46 +02:00
c7316e4f55 1.11.9 - Caching logging and dupe checks
Some checks failed
Tag and Release Lightless / tag-and-release (push) Has been cancelled
Co-authored-by: CakeAndBanana <admin@cakeandbanana.nl>
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #28
2025-09-13 17:16:04 +02:00
8c308ab488 1.11.8 Hottofixo
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 38s
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: cake <cake@noreply.git.lightless-sync.org>
Reviewed-on: #26
2025-09-12 07:10:37 +02:00
a8512e2a86 1.11.7 hotfix
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 36s
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #25
2025-09-12 01:14:23 +02:00
abe28e931c 1.11.6 (#4)
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 36s
1.11.6 Changelog (In Progress)
---
* Update submodule reference
* Update dalamud sdk
* Reworked the Syncshell Admin Page
   - Fixed that owners are visible in the list, Removed Pin/Remove/Ban buttons on Owners.
   - Styling is done similiar as settings page.
   - Added 1 or 3 day(s) option for inactive check.
+ Added new functions on the Server Top Bar button
   - Right click on the button will disconnect you from Lightless
   - Shift+Left click will open the settings page
+ Added colors section in the settings to change accent colors.
   - The nameplate coloring has been moved to this section
+ Added pin option from Dalamud in the UI.
+ Added ability to pause syncing while going in Instance/Duty
+ Added functionality to make syncshell folders
+ Fixed nameplate bug in PVP
+ added self-threshold warning

Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: CakeAndBanana <admin@cakeandbanana.nl>
Co-authored-by: thijmenh <thijmenhogenkamp@gmail.com>
Co-authored-by: choco <choco@noreply.git.lightless-sync.org>
Co-authored-by: cake <cake@noreply.git.lightless-sync.org>
Co-authored-by: choco <thijmenhogenkamp@gmail.com>
Reviewed-on: #4
2025-09-11 23:43:11 +02:00
defnotken
b177dbd595 Move workflow into workflow folder.
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 40s
2025-09-01 09:21:44 -05:00
defnotken
10d5519cc1 1.11.5 Release (#11)
* fancy settings

* incorrect label..

* random ui changes

* Added functionality to rename groups (#12)

* Updated logos in Profile screen.

* Updated IPC calls for Petrenamer and bumped API version.

* IPC calls updated, APIVersion pushed for Moodles

* Updated version 1.11.4 in csproj

* Added Rename Tag UI for renaming of groups

* Adding gitea workflow

* Version bump

---------

Co-authored-by: azyges <229218900+azyges@users.noreply.github.com>
Co-authored-by: Pim <48980200+CakeAndBanana@users.noreply.github.com>
2025-09-01 09:06:32 -05:00
defnotken
dc0265f614 1.11.4 Release Branch (#9)
* Updated logos in Profile screen. (#8)

* UI updates + bugfixes

* Updated version 1.11.4 in csproj

---------

Co-authored-by: Pim <48980200+CakeAndBanana@users.noreply.github.com>
2025-08-30 12:39:03 -05:00
defnotken
ba0e1cea08 Update lightless-tag-and-release.yml 2025-08-29 19:40:00 -05:00
defnotken
1dea9b713e Update LightlessSync.csproj 2025-08-29 18:51:29 -05:00
defnotken
23c57aedc4 Add Nameplates + Clean up.
* Yeet Token!

* Cleaning up workflow

* Testing auto version bump

* ExistingNames

* Remove a key

* Github Token no work

* Changing Assembly Version

* Version Fix

* Fixing version v2

* Cleanup naming

* Update LightlessSync.csproj

* Add nameplate settings + run code clean up

* purple
2025-08-29 18:48:01 -05:00
azyges
15bf87feb4 bump version 2025-08-29 09:29:53 +09:00
azyges
9c0379631f adjust colors 2025-08-29 09:23:51 +09:00
defnotken
44475fda50 FINAL FINAL (#6)
* Yeet Token!

* Cleaning up workflow
2025-08-27 19:21:42 +02:00
defnotken
071885b04c Modify Variable (#4) 2025-08-27 18:21:01 +02:00
Zura
27c3231084 Merge pull request #3 from defnotken/master
Build and Release Workflow on Push to Master
2025-08-27 17:18:20 +02:00
defnotken
2b98bf6891 changing the name of the release 2025-08-27 09:13:01 -05:00
defnotken
b6b8e7bddf Clean up 2025-08-27 09:09:03 -05:00
defnotken
9f8cc48e34 One more time! 2025-08-27 09:03:55 -05:00
defnotken
ae6013adf6 Forgot to make a directory 2025-08-27 08:59:36 -05:00
defnotken
2b67f44788 Switching path of the build 2025-08-27 08:56:29 -05:00
defnotken
ad74729c4a Testing zip and publish 2025-08-26 13:43:38 -05:00
defnotken
4f3a6a8205 running on windows 2025-08-26 13:31:13 -05:00
defnotken
71e80f5c48 add dalamud 2025-08-26 13:29:23 -05:00
defnotken
77f667ccfd now with submodules 2025-08-26 13:07:45 -05:00
defnotken
97e964473b remove cache mode 2025-08-26 13:01:50 -05:00
defnotken
d5d89a759e testing builds 2025-08-26 12:59:23 -05:00
defnotken
e5c87e5d68 debugging version 2025-08-26 11:30:34 -05:00
defnotken
5559c06d2b version references 2025-08-26 11:27:46 -05:00
defnotken
53bea001ba slash? 2025-08-26 11:24:49 -05:00
defnotken
a401a18c04 renaming variable oops 2025-08-26 11:22:55 -05:00
defnotken
213f6e9f6d Testing another action 2025-08-26 11:21:41 -05:00
defnotken
4570c4b5f4 Testing out tag and release 2025-08-26 11:14:35 -05:00
Zura
e6c1120323 Update README.md 2025-08-25 22:33:26 +02:00
Zura
0d35d04a20 Update README.md 2025-08-25 22:33:07 +02:00
Zura
5fb9ec7637 Add files via upload 2025-08-25 16:15:11 +02:00
125 changed files with 7444 additions and 1809 deletions

View File

@@ -0,0 +1,282 @@
name: Tag and Release Lightless
on:
push:
branches: [ master, dev ]
env:
PLUGIN_NAME: LightlessSync
DOTNET_VERSION: 9.x
jobs:
tag-and-release:
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- name: Checkout Lightless
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
- name: Setup .NET 9 SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.x
- name: Download Dalamud
run: |
cd /
mkdir -p root/.xlcore/dalamud/Hooks/dev
curl -O https://goatcorp.github.io/dalamud-distrib/stg/latest.zip
unzip latest.zip -d /root/.xlcore/dalamud/Hooks/dev
- name: Lets Build Lightless!
run: |
dotnet restore
dotnet build --configuration Release --no-restore
dotnet publish --configuration Release --no-build
- name: Get version
id: package_version
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: |
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"
)
release_id=$(echo "$response" | jq -r .id)
echo "release_id=$release_id" >> "$GITHUB_OUTPUT"
- 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

2
.gitmodules vendored
View File

@@ -1,6 +1,6 @@
[submodule "LightlessAPI"]
path = LightlessAPI
url = https://github.com/Light-Public-Syncshells/LightlessAPI
url = https://git.lightless-sync.org/Lightless-Sync/LightlessAPI.git
[submodule "PenumbraAPI"]
path = PenumbraAPI
url = https://github.com/Ottermandias/Penumbra.Api.git

View File

@@ -20,7 +20,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
private long _currentFileProgress = 0;
private CancellationTokenSource _scanCancellationTokenSource = new();
private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk"];
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
@@ -383,7 +383,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;
@@ -583,7 +583,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 +619,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
return;
}
if (entitiesToUpdate.Any() || entitiesToRemove.Any())
if (entitiesToUpdate.Count != 0 || entitiesToRemove.Count != 0)
{
foreach (var entity in entitiesToUpdate)
{
@@ -647,6 +654,12 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
CancellationToken = ct
}, (cachePath) =>
{
if (_fileDbManager == null || _ipcManager?.Penumbra == null || cachePath == null)
{
Logger.LogTrace("Potential null in db: {isDbNull} penumbra: {isPenumbraNull} cachepath: {isPathNull}", _fileDbManager == null, _ipcManager?.Penumbra == null, cachePath == null);
return;
}
if (ct.IsCancellationRequested) return;
if (!_ipcManager.Penumbra.APIAvailable)

View File

@@ -21,7 +21,7 @@ public sealed class FileCacheManager : IHostedService
private readonly string _csvPath;
private readonly ConcurrentDictionary<string, List<FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1);
private readonly object _fileWriteLock = new();
private readonly Lock _fileWriteLock = new();
private readonly IpcManager _ipcManager;
private readonly ILogger<FileCacheManager> _logger;
public string CacheFolder => _configService.Current.CacheFolder;
@@ -42,10 +42,7 @@ public sealed class FileCacheManager : IHostedService
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);
return CreateFileEntity(_configService.Current.CacheFolder.ToLowerInvariant(), CachePrefix, fi);
}
public FileCacheEntity? CreateFileEntry(string path)
@@ -53,9 +50,14 @@ public sealed class FileCacheManager : IHostedService
FileInfo fi = new(path);
if (!fi.Exists) return null;
_logger.LogTrace("Creating file entry for {path}", path);
return CreateFileEntity(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix, fi);
}
private FileCacheEntity? CreateFileEntity(string directory, string prefix, FileInfo fi)
{
var fullName = fi.FullName.ToLowerInvariant();
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);
if (!fullName.Contains(directory, StringComparison.Ordinal)) return null;
string prefixedPath = fullName.Replace(directory, prefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
return CreateFileCacheEntity(fi, prefixedPath);
}
@@ -66,7 +68,7 @@ public sealed class FileCacheManager : IHostedService
List<FileCacheEntity> output = [];
if (_fileCaches.TryGetValue(hash, out var fileCacheEntities))
{
foreach (var fileCache in fileCacheEntities.Where(c => ignoreCacheEntries ? !c.IsCacheEntry : true).ToList())
foreach (var fileCache in fileCacheEntities.Where(c => !ignoreCacheEntries || !c.IsCacheEntry).ToList())
{
if (!validate) output.Add(fileCache);
else
@@ -106,7 +108,7 @@ public sealed class FileCacheManager : IHostedService
var computedHash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
if (!string.Equals(computedHash, fileCache.Hash, StringComparison.Ordinal))
{
_logger.LogInformation("Failed to validate {file}, got hash {hash}, expected hash {hash}", fileCache.ResolvedFilepath, computedHash, fileCache.Hash);
_logger.LogInformation("Failed to validate {file}, got hash {computedHash}, expected hash {hash}", fileCache.ResolvedFilepath, computedHash, fileCache.Hash);
brokenEntities.Add(fileCache);
}
}
@@ -151,7 +153,7 @@ public sealed class FileCacheManager : IHostedService
{
if (_fileCaches.TryGetValue(hash, out var hashes))
{
var item = hashes.OrderBy(p => p.PrefixedFilePath.Contains(PenumbraPrefix) ? 0 : 1).FirstOrDefault();
var item = hashes.OrderBy(p => p.PrefixedFilePath.Contains(PenumbraPrefix, StringComparison.Ordinal) ? 0 : 1).FirstOrDefault();
if (item != null) return GetValidatedFileCache(item);
}
return null;
@@ -180,37 +182,73 @@ 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),
var allEntities = _fileCaches.SelectMany(f => f.Value).ToArray();
var cacheDict = new ConcurrentDictionary<string, FileCacheEntity>(
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)
Parallel.ForEach(allEntities, entity =>
{
//_logger.LogDebug("Checking {path}", entry.Value);
if (entity != null && entity.PrefixedFilePath != null)
{
cacheDict[entity.PrefixedFilePath] = entity;
}
else
{
_logger.LogWarning("Null FileCacheEntity or PrefixedFilePath encountered in cache population: {entity}", entity);
}
});
if (dict.TryGetValue(entry.Value, out var entity))
var cleanedPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var seenCleaned = new ConcurrentDictionary<string, byte>(StringComparer.OrdinalIgnoreCase);
Parallel.ForEach(paths, p =>
{
var cleaned = 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);
if (seenCleaned.TryAdd(cleaned, 0))
{
_logger.LogDebug("Adding to cleanedPaths: {cleaned}", cleaned);
cleanedPaths[p] = cleaned;
}
else
{
_logger.LogWarning("Duplicate found: {cleaned}", cleaned);
}
});
var result = new ConcurrentDictionary<string, FileCacheEntity?>(StringComparer.OrdinalIgnoreCase);
Parallel.ForEach(cleanedPaths, entry =>
{
_logger.LogDebug("Checking if in cache: {path}", entry.Value);
if (cacheDict.TryGetValue(entry.Value, out var entity))
{
var validatedCache = GetValidatedFileCache(entity);
result.Add(entry.Key, validatedCache);
result[entry.Key] = validatedCache;
}
else
{
if (!entry.Value.Contains(CachePrefix, StringComparison.Ordinal))
result.Add(entry.Key, CreateFileEntry(entry.Key));
result[entry.Key] = CreateFileEntry(entry.Key);
else
result.Add(entry.Key, CreateCacheEntry(entry.Key));
result[entry.Key] = CreateCacheEntry(entry.Key);
}
}
});
return result;
return new Dictionary<string, FileCacheEntity?>(result, StringComparer.OrdinalIgnoreCase);
}
finally
{
@@ -222,6 +260,7 @@ public sealed class FileCacheManager : IHostedService
{
if (_fileCaches.TryGetValue(hash, out var caches))
{
_logger.LogTrace("Removing from DB: {hash} => {path}", hash, prefixedFilePath);
var removedCount = caches?.RemoveAll(c => string.Equals(c.PrefixedFilePath, prefixedFilePath, StringComparison.Ordinal));
_logger.LogTrace("Removed from DB: {count} file(s) with hash {hash} and file cache {path}", removedCount, hash, prefixedFilePath);
@@ -366,6 +405,12 @@ public sealed class FileCacheManager : IHostedService
private FileCacheEntity? Validate(FileCacheEntity fileCache)
{
if (string.IsNullOrWhiteSpace(fileCache.ResolvedFilepath))
{
_logger.LogWarning("FileCacheEntity has empty ResolvedFilepath for hash {hash}, prefixed path {prefixed}", fileCache.Hash, fileCache.PrefixedFilePath);
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
return null;
}
var file = new FileInfo(fileCache.ResolvedFilepath);
if (!file.Exists)
{
@@ -439,7 +484,7 @@ public sealed class FileCacheManager : IHostedService
{
attempts++;
_logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath);
Thread.Sleep(100);
Task.Delay(100, cancellationToken);
}
}

View File

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

View File

@@ -27,9 +27,9 @@ public sealed class IpcCallerMoodles : IIpcCaller
_moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version");
_moodlesOnChange = pi.GetIpcSubscriber<IPlayerCharacter, object>("Moodles.StatusManagerModified");
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtr");
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtr");
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtr");
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtrV2");
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtrV2");
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtrV2");
_moodlesOnChange.Subscribe(OnMoodlesChange);
@@ -47,7 +47,7 @@ public sealed class IpcCallerMoodles : IIpcCaller
{
try
{
APIAvailable = _moodlesApiVersion.InvokeFunc() == 1;
APIAvailable = _moodlesApiVersion.InvokeFunc() == 3;
}
catch
{

View File

@@ -30,12 +30,12 @@ public sealed class IpcCallerPetNames : IIpcCaller
_dalamudUtil = dalamudUtil;
_lightlessMediator = lightlessMediator;
_petnamesReady = pi.GetIpcSubscriber<object>("PetRenamer.Ready");
_petnamesDisposing = pi.GetIpcSubscriber<object>("PetRenamer.Disposing");
_petnamesReady = pi.GetIpcSubscriber<object>("PetRenamer.OnReady");
_petnamesDisposing = pi.GetIpcSubscriber<object>("PetRenamer.OnDisposing");
_apiVersion = pi.GetIpcSubscriber<(uint, uint)>("PetRenamer.ApiVersion");
_enabled = pi.GetIpcSubscriber<bool>("PetRenamer.Enabled");
_enabled = pi.GetIpcSubscriber<bool>("PetRenamer.IsEnabled");
_playerDataChanged = pi.GetIpcSubscriber<string, object>("PetRenamer.PlayerDataChanged");
_playerDataChanged = pi.GetIpcSubscriber<string, object>("PetRenamer.OnPlayerDataChanged");
_getPlayerData = pi.GetIpcSubscriber<string>("PetRenamer.GetPlayerData");
_setPlayerData = pi.GetIpcSubscriber<string, object>("PetRenamer.SetPlayerData");
_clearPlayerData = pi.GetIpcSubscriber<ushort, object>("PetRenamer.ClearPlayerData");
@@ -56,7 +56,7 @@ public sealed class IpcCallerPetNames : IIpcCaller
APIAvailable = _enabled?.InvokeFunc() ?? false;
if (APIAvailable)
{
APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 3, Item2: >= 1 };
APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 4, Item2: >= 0 };
}
}
catch
@@ -84,7 +84,7 @@ public sealed class IpcCallerPetNames : IIpcCaller
{
string localNameData = _getPlayerData.InvokeFunc();
return string.IsNullOrEmpty(localNameData) ? string.Empty : localNameData;
}
}
catch (Exception e)
{
_logger.LogWarning(e, "Could not obtain Pet Nicknames data");

View File

@@ -1,5 +1,4 @@
using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.LightlessConfiguration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Reflection;

View File

@@ -1,4 +1,4 @@
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.UI;
using Microsoft.Extensions.Logging;
@@ -13,10 +13,14 @@ public class LightlessConfig : ILightlessConfiguration
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 UseLightlessRedesign { get; set; } = true;
public bool EnableRightClickMenus { get; set; } = true;
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
public string ExportFolder { get; set; } = string.Empty;
@@ -40,6 +44,7 @@ public class LightlessConfig : ILightlessConfiguration
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;
@@ -60,4 +65,12 @@ public class LightlessConfig : ILightlessConfiguration
public int Version { get; set; } = 1;
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both;
public bool UseFocusTarget { get; set; } = false;
public bool overrideFriendColor { get; set; } = false;
public bool overridePartyColor { get; set; } = false;
public bool overrideFcTagColor { get; set; } = false;
public bool useColoredUIDs { get; set; } = true;
public bool BroadcastEnabled { get; set; } = false;
public DateTime BroadcastTtl { get; set; } = DateTime.MinValue;
public bool SyncshellFinderEnabled { get; set; } = false;
public string? SelectedFinderSyncshell { get; set; } = null;
}

View File

@@ -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;
}

View File

@@ -13,4 +13,7 @@ public class PlayerPerformanceConfig : ILightlessConfiguration
public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 550;
public int TrisAutoPauseThresholdThousands { get; set; } = 250;
public List<string> UIDsToIgnore { get; set; } = new();
public bool PauseInInstanceDuty { get; set; } = false;
public bool PauseWhilePerforming { get; set; } = true;
public bool PauseInCombat { get; set; } = true;
}

View File

@@ -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;
}

View File

@@ -1,5 +1,4 @@
using LightlessSync.API.Data.Enum;
using LightlessSync.LightlessConfiguration.Configurations;
namespace LightlessSync.LightlessConfiguration.Configurations;

View File

@@ -1,7 +1,7 @@
namespace LightlessSync.LightlessConfiguration.Models;
[Serializable]
public class ServerTagStorage
public class PairTagStorage
{
public HashSet<string> OpenPairTags { get; set; } = new(StringComparer.Ordinal);
public HashSet<string> ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal);

View File

@@ -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);
}

View File

@@ -2,11 +2,11 @@
namespace LightlessSync.LightlessConfiguration;
public class ServerTagConfigService : ConfigurationServiceBase<ServerTagConfig>
public class PairTagConfigService : ConfigurationServiceBase<PairTagStorage>
{
public const string ConfigName = "servertags.json";
public ServerTagConfigService(string configDir) : base(configDir)
public PairTagConfigService(string configDir) : base(configDir)
{
}

View File

@@ -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;
}

View File

@@ -1,10 +1,11 @@
using LightlessSync.FileCache;
using LightlessSync.FileCache;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.PlayerData.Services;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -98,6 +99,7 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut());
UIColors.Initialize(_lightlessConfigService);
Mediator.StartQueueProcessing();
return Task.CompletedTask;
@@ -151,6 +153,7 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
_runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<VisibleUserDataDistributor>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<NameplateService>();
#if !DEBUG
if (_lightlessConfigService.Current.LogLevel != LogLevel.Information)

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Dalamud.NET.Sdk/13.0.0">
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
<PropertyGroup>
<Authors></Authors>
<Company></Company>
<Version>1.11.1</Version>
<Version>1.12.0</Version>
<Description></Description>
<Copyright></Copyright>
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>

View File

@@ -1,9 +0,0 @@
using LightlessSync.LightlessConfiguration.Models;
namespace LightlessSync.LightlessConfiguration.Configurations;
public class ServerTagConfig : ILightlessConfiguration
{
public Dictionary<string, ServerTagStorage> ServerTagStorage { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public int Version { get; set; } = 0;
}

View File

@@ -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;

View File

@@ -88,20 +88,30 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
_redrawOnNextApplication = true;
}
});
Mediator.Subscribe<CombatOrPerformanceEndMessage>(this, (msg) =>
Mediator.Subscribe<CombatEndMessage>(this, (msg) =>
{
if (IsVisible && _dataReceivedInDowntime != null)
{
ApplyCharacterData(_dataReceivedInDowntime.ApplicationId,
_dataReceivedInDowntime.CharacterData, _dataReceivedInDowntime.Forced);
_dataReceivedInDowntime = null;
}
EnableSync();
});
Mediator.Subscribe<CombatOrPerformanceStartMessage>(this, _ =>
Mediator.Subscribe<CombatStartMessage>(this, _ =>
{
_dataReceivedInDowntime = null;
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate();
_applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate();
DisableSync();
});
Mediator.Subscribe<PerformanceEndMessage>(this, (msg) =>
{
EnableSync();
});
Mediator.Subscribe<PerformanceStartMessage>(this, _ =>
{
DisableSync();
});
Mediator.Subscribe<InstanceOrDutyStartMessage>(this, _ =>
{
DisableSync();
});
Mediator.Subscribe<InstanceOrDutyEndMessage>(this, (msg) =>
{
EnableSync();
});
LastAppliedDataBytes = -1;
@@ -119,6 +129,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler),
EventSeverity.Informational, text)));
Mediator.Publish(new RefreshUiMessage());
Mediator.Publish(new VisibilityChange());
}
}
}
@@ -134,11 +145,31 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
{
if (_dalamudUtil.IsInCombatOrPerforming)
if (_dalamudUtil.IsInCombat)
{
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
"Cannot apply character data: you are in combat or performing music, deferring application")));
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat or performing", applicationBase);
"Cannot apply character data: you are in combat, deferring application")));
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase);
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
SetUploading(isUploading: false);
return;
}
if (_dalamudUtil.IsPerforming)
{
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
"Cannot apply character data: you are performing music, deferring application")));
Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase);
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
SetUploading(isUploading: false);
return;
}
if (_dalamudUtil.IsInInstance)
{
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
"Cannot apply character data: you are in an instance, deferring application")));
Logger.LogDebug("[BASE-{appBase}] Received data but player is in instance", applicationBase);
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
SetUploading(isUploading: false);
return;
@@ -715,4 +746,21 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
Logger.LogDebug("[BASE-{appBase}] ModdedPaths calculated in {time}ms, missing files: {count}, total files: {total}", applicationBase, st.ElapsedMilliseconds, missingFiles.Count, moddedDictionary.Keys.Count);
return [.. missingFiles];
}
private void DisableSync()
{
_dataReceivedInDowntime = null;
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate();
_applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate();
}
private void EnableSync()
{
if (IsVisible && _dataReceivedInDowntime != null)
{
ApplyCharacterData(_dataReceivedInDowntime.ApplicationId,
_dataReceivedInDowntime.CharacterData, _dataReceivedInDowntime.Forced);
_dataReceivedInDowntime = null;
}
}
}

View File

@@ -48,6 +48,7 @@ public class Pair
public long LastAppliedDataTris { get; set; } = -1;
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
public string Ident => _onlineUserIdentDto?.Ident ?? string.Empty;
public uint PlayerCharacterId => CachedPlayer?.PlayerCharacterId ?? uint.MaxValue;
public UserData UserData => UserPair.User;
@@ -71,8 +72,8 @@ public class Pair
Name = openProfileSeString,
OnClicked = (a) => _mediator.Publish(new ProfileOpenStandaloneMessage(this)),
UseDefaultPrefix = false,
PrefixChar = 'M',
PrefixColor = 526
PrefixChar = 'L',
PrefixColor = 708
});
args.AddMenuItem(new MenuItem()
@@ -80,8 +81,8 @@ public class Pair
Name = reapplyDataSeString,
OnClicked = (a) => ApplyLastReceivedData(forced: true),
UseDefaultPrefix = false,
PrefixChar = 'M',
PrefixColor = 526
PrefixChar = 'L',
PrefixColor = 708
});
args.AddMenuItem(new MenuItem()
@@ -89,8 +90,8 @@ public class Pair
Name = changePermissions,
OnClicked = (a) => _mediator.Publish(new OpenPermissionWindow(this)),
UseDefaultPrefix = false,
PrefixChar = 'M',
PrefixColor = 526
PrefixChar = 'L',
PrefixColor = 708
});
args.AddMenuItem(new MenuItem()
@@ -98,8 +99,8 @@ public class Pair
Name = cyclePauseState,
OnClicked = (a) => _mediator.Publish(new CyclePauseMessage(UserData)),
UseDefaultPrefix = false,
PrefixChar = 'M',
PrefixColor = 526
PrefixChar = 'L',
PrefixColor = 708
});
}

View File

@@ -1,4 +1,5 @@
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
@@ -12,6 +13,7 @@ using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.PlayerData.Services;
using LightlessSync.Services;
using LightlessSync.Services.CharaData;
using LightlessSync.Services.Events;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
@@ -28,8 +30,6 @@ using Microsoft.Extensions.Logging;
using NReco.Logging.File;
using System.Net.Http.Headers;
using System.Reflection;
using LightlessSync.Services.CharaData;
using Dalamud.Game;
namespace LightlessSync;
@@ -41,7 +41,7 @@ public sealed class Plugin : IDalamudPlugin
IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui,
IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager,
ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, IGameConfig gameConfig,
ISigScanner sigScanner)
ISigScanner sigScanner, INamePlateGui namePlateGui, IAddonLifecycle addonLifecycle)
{
if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName))
Directory.CreateDirectory(pluginInterface.ConfigDirectory.FullName);
@@ -90,6 +90,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton(new WindowSystem("LightlessSync"));
collection.AddSingleton<FileDialogManager>();
collection.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", "", useEmbedded: true));
collection.AddSingleton(gameGui);
// add lightless related singletons
collection.AddSingleton<LightlessMediator>();
@@ -112,6 +113,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<PluginWarningNotificationService>();
collection.AddSingleton<FileCompactor>();
collection.AddSingleton<TagHandler>();
collection.AddSingleton<PairRequestService>();
collection.AddSingleton<IdDisplayHandler>();
collection.AddSingleton<PlayerPerformanceService>();
collection.AddSingleton<TransientResourceManager>();
@@ -130,17 +132,25 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<CharaDataManager>(),
s.GetRequiredService<LightlessMediator>()));
collection.AddSingleton<SelectPairForTagUi>();
collection.AddSingleton<RenamePairTagUi>();
collection.AddSingleton<SelectSyncshellForTagUi>();
collection.AddSingleton<RenameSyncshellTagUi>();
collection.AddSingleton((s) => new EventAggregator(pluginInterface.ConfigDirectory.FullName,
s.GetRequiredService<ILogger<EventAggregator>>(), s.GetRequiredService<LightlessMediator>()));
collection.AddSingleton((s) => new DalamudUtilService(s.GetRequiredService<ILogger<DalamudUtilService>>(),
clientState, objectTable, framework, gameGui, condition, gameData, targetManager, gameConfig,
s.GetRequiredService<BlockedCharacterHandler>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(),
s.GetRequiredService<LightlessConfigService>()));
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<PlayerPerformanceConfigService>()));
collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService<ILogger<DtrEntry>>(), dtrBar, s.GetRequiredService<LightlessConfigService>(),
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>()));
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>()));
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu));
collection.AddSingleton<RedrawManager>();
collection.AddSingleton<BroadcastService>();
collection.AddSingleton(addonLifecycle);
collection.AddSingleton(p => new ContextMenuService(contextMenu, pluginInterface, gameData,
p.GetRequiredService<ILogger<ContextMenuService>>(), p.GetRequiredService<DalamudUtilService>(), p.GetRequiredService<ApiController>(), objectTable,
p.GetRequiredService<LightlessConfigService>(), p.GetRequiredService<PairRequestService>(), p.GetRequiredService<PairManager>(), clientState));
collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService<ILogger<IpcCallerPenumbra>>(), pluginInterface,
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<RedrawManager>()));
collection.AddSingleton((s) => new IpcCallerGlamourer(s.GetRequiredService<ILogger<IpcCallerGlamourer>>(), pluginInterface,
@@ -171,10 +181,16 @@ public sealed class Plugin : IDalamudPlugin
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
return httpClient;
});
collection.AddSingleton((s) => new LightlessConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) =>
{
var cfg = new LightlessConfigService(pluginInterface.ConfigDirectory.FullName);
LightlessSync.UI.Style.MainStyle.Init(cfg);
return cfg;
});
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ServerTagConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new PairTagConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new SyncshellTagConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new TransientConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new XivDataStorageService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName));
@@ -182,21 +198,25 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<LightlessConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<ServerTagConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<PairTagConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<SyncshellTagConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<TransientConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<XivDataStorageService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<PlayerPerformanceConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
collection.AddSingleton<ConfigurationMigrator>();
collection.AddSingleton<ConfigurationSaveService>();
collection.AddSingleton<HubFactory>();
collection.AddSingleton<NameplateHandler>();
collection.AddSingleton(s => new BroadcastScannerService( s.GetRequiredService<ILogger<BroadcastScannerService>>(), clientState, objectTable, framework, s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<NameplateHandler>(), s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessConfigService>()));
// add scoped services
collection.AddScoped<DrawEntityFactory>();
collection.AddScoped<CacheMonitor>();
collection.AddScoped<UiFactory>();
collection.AddScoped<SelectTagForPairUi>();
collection.AddScoped<SelectTagForSyncshellUi>();
collection.AddScoped<WindowMediatorSubscriberBase, SettingsUi>();
collection.AddScoped<WindowMediatorSubscriberBase, CompactUi>();
collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
@@ -212,6 +232,8 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<FileDialogManager>(),
s.GetRequiredService<LightlessProfileManager>(), s.GetRequiredService<PerformanceCollectorService>()));
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
collection.AddScoped<WindowMediatorSubscriberBase, BroadcastUI>((s) => new BroadcastUI(s.GetRequiredService<ILogger<BroadcastUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>()));
collection.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>((s) => new SyncshellFinderUI(s.GetRequiredService<ILogger<SyncshellFinderUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<DalamudUtilService>()));
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
collection.AddScoped<IPopupHandler, CensusPopupHandler>();
collection.AddScoped<CacheCreationService>();
@@ -228,6 +250,8 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<DalamudUtilService>(),
pluginInterface, textureProvider, s.GetRequiredService<Dalamud.Localization>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<TokenProvider>(),
s.GetRequiredService<LightlessMediator>()));
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), namePlateGui, clientState,
s.GetRequiredService<PairManager>(), s.GetRequiredService<LightlessMediator>()));
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
collection.AddHostedService(p => p.GetRequiredService<LightlessMediator>());
@@ -240,6 +264,8 @@ public sealed class Plugin : IDalamudPlugin
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
collection.AddHostedService(p => p.GetRequiredService<LightlessPlugin>());
collection.AddHostedService(p => p.GetRequiredService<ContextMenuService>());
collection.AddHostedService(p => p.GetRequiredService<BroadcastService>());
})
.Build();
@@ -251,4 +277,4 @@ public sealed class Plugin : IDalamudPlugin
_host.StopAsync().GetAwaiter().GetResult();
_host.Dispose();
}
}
}

View File

@@ -0,0 +1,222 @@
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Plugin.Services;
using LightlessSync.API.Dto.User;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
namespace LightlessSync.Services;
public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDisposable
{
private readonly ILogger<BroadcastScannerService> _logger;
private readonly IObjectTable _objectTable;
private readonly IFramework _framework;
private readonly BroadcastService _broadcastService;
private readonly NameplateHandler _nameplateHandler;
private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new();
private readonly Queue<string> _lookupQueue = new();
private readonly HashSet<string> _lookupQueuedCids = new();
private readonly HashSet<string> _syncshellCids = new();
private static readonly TimeSpan MaxAllowedTtl = TimeSpan.FromMinutes(4);
private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1);
private readonly CancellationTokenSource _cleanupCts = new();
private Task? _cleanupTask;
private int _checkEveryFrames = 20;
private int _frameCounter = 0;
private int _lookupsThisFrame = 0;
private const int MaxLookupsPerFrame = 30;
private const int MaxQueueSize = 100;
private volatile bool _batchRunning = false;
public IReadOnlyDictionary<string, BroadcastEntry> BroadcastCache => _broadcastCache;
public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID);
public BroadcastScannerService(ILogger<BroadcastScannerService> logger,
IClientState clientState,
IObjectTable objectTable,
IFramework framework,
BroadcastService broadcastService,
LightlessMediator mediator,
NameplateHandler nameplateHandler,
DalamudUtilService dalamudUtil,
LightlessConfigService configService) : base(logger, mediator)
{
_logger = logger;
_objectTable = objectTable;
_broadcastService = broadcastService;
_nameplateHandler = nameplateHandler;
_logger = logger;
_framework = framework;
_framework.Update += OnFrameworkUpdate;
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
_cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop);
_nameplateHandler.Init();
}
private void OnFrameworkUpdate(IFramework framework) => Update();
public void Update()
{
_frameCounter++;
_lookupsThisFrame = 0;
if (!_broadcastService.IsBroadcasting)
return;
var now = DateTime.UtcNow;
foreach (var obj in _objectTable)
{
if (obj is not IPlayerCharacter player || player.Address == IntPtr.Zero)
continue;
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(player.Address);
var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now;
if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < MaxQueueSize)
_lookupQueue.Enqueue(cid);
}
if (_frameCounter % _checkEveryFrames == 0 && _lookupQueue.Count > 0)
{
var cidsToLookup = new List<string>();
while (_lookupQueue.Count > 0 && _lookupsThisFrame < MaxLookupsPerFrame)
{
var cid = _lookupQueue.Dequeue();
_lookupQueuedCids.Remove(cid);
cidsToLookup.Add(cid);
_lookupsThisFrame++;
}
if (cidsToLookup.Count > 0 && !_batchRunning)
{
_batchRunning = true;
_ = BatchUpdateBroadcastCacheAsync(cidsToLookup).ContinueWith(_ => _batchRunning = false);
}
}
}
private async Task BatchUpdateBroadcastCacheAsync(List<string> cids)
{
var results = await _broadcastService.AreUsersBroadcastingAsync(cids).ConfigureAwait(false);
var now = DateTime.UtcNow;
foreach (var (cid, info) in results)
{
if (string.IsNullOrWhiteSpace(cid) || info == null)
continue;
var ttl = info.IsBroadcasting && info.TTL.HasValue
? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, MaxAllowedTtl.Ticks))
: RetryDelay;
var expiry = now + ttl;
_broadcastCache.AddOrUpdate(cid,
new BroadcastEntry(info.IsBroadcasting, expiry, info.GID),
(_, old) => new BroadcastEntry(info.IsBroadcasting, expiry, info.GID));
}
var activeCids = _broadcastCache
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
.Select(e => e.Key)
.ToList();
_nameplateHandler.UpdateBroadcastingCids(activeCids);
UpdateSyncshellBroadcasts();
}
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
{
if (!msg.Enabled)
{
_broadcastCache.Clear();
_lookupQueue.Clear();
_lookupQueuedCids.Clear();
_syncshellCids.Clear();
_nameplateHandler.UpdateBroadcastingCids(Enumerable.Empty<string>());
}
}
private void UpdateSyncshellBroadcasts()
{
var now = DateTime.UtcNow;
var newSet = _broadcastCache
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
.Select(e => e.Key)
.ToHashSet();
if (!_syncshellCids.SetEquals(newSet))
{
_syncshellCids.Clear();
foreach (var cid in newSet)
_syncshellCids.Add(cid);
Mediator.Publish(new SyncshellBroadcastsUpdatedMessage());
}
}
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts()
{
var now = DateTime.UtcNow;
return _broadcastCache
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
.Select(e => new BroadcastStatusInfoDto
{
HashedCID = e.Key,
IsBroadcasting = true,
TTL = e.Value.ExpiryTime - now,
GID = e.Value.GID
})
.ToList();
}
private async Task ExpiredBroadcastCleanupLoop()
{
var token = _cleanupCts.Token;
try
{
while (!token.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(10), token);
var now = DateTime.UtcNow;
foreach (var (cid, entry) in _broadcastCache.ToArray())
{
if (entry.ExpiryTime <= now)
_broadcastCache.TryRemove(cid, out _);
}
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
_logger.LogError(ex, "Broadcast cleanup loop crashed");
}
UpdateSyncshellBroadcasts();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_framework.Update -= OnFrameworkUpdate;
_cleanupCts.Cancel();
_cleanupTask?.Wait(100);
_nameplateHandler.Uninit();
}
}

View File

@@ -0,0 +1,377 @@
using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public class BroadcastService : IHostedService, IMediatorSubscriber
{
private readonly ILogger<BroadcastService> _logger;
private readonly ApiController _apiController;
private readonly LightlessMediator _mediator;
private readonly LightlessConfigService _config;
private readonly DalamudUtilService _dalamudUtil;
public LightlessMediator Mediator => _mediator;
public bool IsLightFinderAvailable { get; private set; } = true;
public bool IsBroadcasting => _config.Current.BroadcastEnabled;
private bool _syncedOnStartup = false;
private bool _waitingForTtlFetch = false;
private TimeSpan? _remainingTtl = null;
private DateTime _lastTtlCheck = DateTime.MinValue;
private DateTime _lastForcedDisableTime = DateTime.MinValue;
private static readonly TimeSpan _disableCooldown = TimeSpan.FromSeconds(5);
public TimeSpan? RemainingTtl => _remainingTtl;
public TimeSpan? RemainingCooldown
{
get
{
var elapsed = DateTime.UtcNow - _lastForcedDisableTime;
if (elapsed >= _disableCooldown) return null;
return _disableCooldown - elapsed;
}
}
public BroadcastService(ILogger<BroadcastService> logger, LightlessMediator mediator, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController)
{
_logger = logger;
_mediator = mediator;
_config = config;
_dalamudUtil = dalamudUtil;
_apiController = apiController;
}
private async Task RequireConnectionAsync(string context, Func<Task> action)
{
if (!_apiController.IsConnected)
{
_logger.LogDebug(context + " skipped, not connected");
return;
}
await action().ConfigureAwait(false);
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_mediator.Subscribe<EnableBroadcastMessage>(this, OnEnableBroadcast);
_mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
_apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken);
//_ = CheckLightfinderSupportAsync(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
{
_mediator.UnsubscribeAll(this);
_apiController.OnConnected -= () => _ = CheckLightfinderSupportAsync(cancellationToken);
return Task.CompletedTask;
}
// need to rework this, this is cooked
private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken)
{
try
{
while (!_apiController.IsConnected && !cancellationToken.IsCancellationRequested)
await Task.Delay(250, cancellationToken).ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested)
return;
var dummy = "0".PadLeft(64, '0');
await _apiController.IsUserBroadcasting(dummy).ConfigureAwait(false);
await _apiController.SetBroadcastStatus(dummy, true, null).ConfigureAwait(false);
await _apiController.GetBroadcastTtl(dummy).ConfigureAwait(false);
await _apiController.AreUsersBroadcasting([dummy]).ConfigureAwait(false);
IsLightFinderAvailable = true;
_logger.LogInformation("Lightfinder is available.");
}
catch (HubException ex) when (ex.Message.Contains("Method does not exist"))
{
_logger.LogWarning("Lightfinder unavailable: required method missing.");
IsLightFinderAvailable = false;
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
}
catch (OperationCanceledException)
{
_logger.LogInformation("Lightfinder check was canceled.");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Lightfinder check failed.");
IsLightFinderAvailable = false;
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
}
}
private void OnEnableBroadcast(EnableBroadcastMessage msg)
{
_ = RequireConnectionAsync(nameof(OnEnableBroadcast), async () =>
{
try
{
GroupBroadcastRequestDto? groupDto = null;
if (_config.Current.SyncshellFinderEnabled && _config.Current.SelectedFinderSyncshell != null)
{
groupDto = new GroupBroadcastRequestDto
{
HashedCID = msg.HashedCid,
GID = _config.Current.SelectedFinderSyncshell,
Enabled = msg.Enabled,
};
}
await _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false);
_logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid);
if (!msg.Enabled)
{
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}")));
return;
}
_waitingForTtlFetch = true;
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
if (ttl is { } remaining && remaining > TimeSpan.Zero)
{
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
_config.Current.BroadcastEnabled = true;
_config.Save();
_logger.LogDebug("Fetched TTL from server: {TTL}", remaining);
_mediator.Publish(new BroadcastStatusChangedMessage(true, remaining));
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
}
else
{
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
}
_waitingForTtlFetch = false;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to toggle broadcast for {Cid}", msg.HashedCid);
_waitingForTtlFetch = false;
}
});
}
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
{
_config.Current.BroadcastEnabled = msg.Enabled;
_config.Save();
}
public async Task<bool> CheckIfBroadcastingAsync(string targetCid)
{
bool result = false;
await RequireConnectionAsync(nameof(CheckIfBroadcastingAsync), async () =>
{
try
{
_logger.LogDebug("[BroadcastCheck] Checking CID: {cid}", targetCid);
var info = await _apiController.IsUserBroadcasting(targetCid).ConfigureAwait(false);
result = info?.TTL > TimeSpan.Zero;
_logger.LogDebug("[BroadcastCheck] Result for {cid}: {result} (TTL: {ttl}, GID: {gid})", targetCid, result, info?.TTL, info?.GID);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to check broadcast status for {cid}", targetCid);
}
}).ConfigureAwait(false);
return result;
}
public async Task<TimeSpan?> GetBroadcastTtlAsync(string cid)
{
TimeSpan? ttl = null;
await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => {
try
{
ttl = await _apiController.GetBroadcastTtl(cid).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch broadcast TTL for {cid}", cid);
}
}).ConfigureAwait(false);
return ttl;
}
public async Task<Dictionary<string, BroadcastStatusInfoDto?>> AreUsersBroadcastingAsync(List<string> hashedCids)
{
Dictionary<string, BroadcastStatusInfoDto?> result = new();
await RequireConnectionAsync(nameof(AreUsersBroadcastingAsync), async () =>
{
try
{
var batch = await _apiController.AreUsersBroadcasting(hashedCids).ConfigureAwait(false);
if (batch?.Results != null)
{
foreach (var kv in batch.Results)
result[kv.Key] = kv.Value;
}
_logger.LogTrace("Batch broadcast status check complete for {Count} CIDs", hashedCids.Count);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to batch check broadcast status");
}
}).ConfigureAwait(false);
return result;
}
public async void ToggleBroadcast()
{
if (!IsLightFinderAvailable)
{
_logger.LogWarning("ToggleBroadcast - Lightfinder is not available.");
return;
}
await RequireConnectionAsync(nameof(ToggleBroadcast), async () =>
{
var cooldown = RemainingCooldown;
if (!_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero)
{
_logger.LogWarning("Cooldown active. Must wait {Remaining}s before re-enabling.", cd.TotalSeconds);
return;
}
var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
try
{
var isCurrentlyBroadcasting = await CheckIfBroadcastingAsync(hashedCid).ConfigureAwait(false);
var newStatus = !isCurrentlyBroadcasting;
if (!newStatus)
{
_lastForcedDisableTime = DateTime.UtcNow;
_logger.LogDebug("Manual disable: cooldown timer started.");
}
_logger.LogDebug("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus);
_mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to determine current broadcast status for toggle");
}
}).ConfigureAwait(false);
}
private async void OnTick(PriorityFrameworkUpdateMessage _)
{
if (!IsLightFinderAvailable)
return;
if (_config?.Current == null)
return;
if ((DateTime.UtcNow - _lastTtlCheck).TotalSeconds < 1)
return;
_lastTtlCheck = DateTime.UtcNow;
await RequireConnectionAsync(nameof(OnTick), async () => {
if (!_syncedOnStartup && _config.Current.BroadcastEnabled)
{
_syncedOnStartup = true;
try
{
string hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
if (ttl is { }
remaining && remaining > TimeSpan.Zero)
{
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
_config.Current.BroadcastEnabled = true;
_config.Save();
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining);
}
else
{
_logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state.");
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to refresh TTL in OnTick");
}
}
if (_config.Current.BroadcastEnabled)
{
if (_waitingForTtlFetch)
{
_logger.LogDebug("OnTick skipped: waiting for TTL fetch");
return;
}
DateTime expiry = _config.Current.BroadcastTtl;
TimeSpan remaining = expiry - DateTime.UtcNow;
_remainingTtl = remaining > TimeSpan.Zero ? remaining : null;
if (_remainingTtl == null)
{
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
}
}
else
{
_remainingTtl = null;
}
}).ConfigureAwait(false);
}
}

View File

@@ -944,9 +944,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
Logger.LogTrace("[{appId}] Computing local missing files", applicationId);
Dictionary<string, string> modPaths;
List<FileReplacementData> missingFiles;
_fileHandler.ComputeMissingFiles(charaDataDownloadDto, out modPaths, out missingFiles);
_fileHandler.ComputeMissingFiles(charaDataDownloadDto, out Dictionary<string, string> modPaths, out List<FileReplacementData> missingFiles);
Logger.LogTrace("[{appId}] Computing local missing files", applicationId);
@@ -990,7 +988,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
{
_uploadCts = _uploadCts.CancelRecreate();
var missingFiles = await _fileHandler.UploadFiles([.. missingFileList.Select(k => k.HashOrFileSwap)], UploadProgress, _uploadCts.Token).ConfigureAwait(false);
if (missingFiles.Any())
if (missingFiles.Count != 0)
{
Logger.LogInformation("Failed to upload {files}", string.Join(", ", missingFiles));
return ($"Upload failed: {missingFiles.Count} missing or forbidden to upload local files.", false);

View File

@@ -236,7 +236,7 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
}
}
if (_charaDataConfigService.Current.NearbyDrawWisps && !_dalamudUtilService.IsInGpose && !_dalamudUtilService.IsInCombatOrPerforming)
if (_charaDataConfigService.Current.NearbyDrawWisps && !_dalamudUtilService.IsInGpose && !_dalamudUtilService.IsInCombat && !_dalamudUtilService.IsPerforming && !_dalamudUtilService.IsInInstance)
await _dalamudUtilService.RunOnFrameworkThread(() => ManageWispsNearby(previousPoses)).ConfigureAwait(false);
}
@@ -253,7 +253,7 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
return;
}
if (!_charaDataConfigService.Current.NearbyDrawWisps || _dalamudUtilService.IsInGpose || _dalamudUtilService.IsInCombatOrPerforming)
if (!_charaDataConfigService.Current.NearbyDrawWisps || _dalamudUtilService.IsInGpose || _dalamudUtilService.IsInCombat || _dalamudUtilService.IsPerforming || _dalamudUtilService.IsInInstance)
ClearAllVfx();
var camera = CameraManager.Instance()->CurrentCamera;

View File

@@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LightlessSync.Services.CharaData
namespace LightlessSync.Services.CharaData
{
internal class CharaDataTogetherManager
{

View File

@@ -1,6 +1,6 @@
using Dalamud.Utility;
using Lumina.Excel.Sheets;
using LightlessSync.API.Dto.CharaData;
using Lumina.Excel.Sheets;
using System.Globalization;
using System.Numerics;
using System.Text;

View File

@@ -1,10 +1,10 @@
using Lumina.Data.Files;
using LightlessSync.API.Data;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum;
using LightlessSync.FileCache;
using LightlessSync.Services.Mediator;
using LightlessSync.UI;
using LightlessSync.Utils;
using Lumina.Data.Files;
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;

View File

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

View File

@@ -1,4 +1,3 @@
using Dalamud.Game;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.SubKinds;
@@ -10,13 +9,13 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.Sheets;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.Interop;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Numerics;
@@ -40,6 +39,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
private readonly IObjectTable _objectTable;
private readonly PerformanceCollectorService _performanceCollector;
private readonly LightlessConfigService _configService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private uint? _classJobId = 0;
private DateTime _delayedFrameworkUpdateCheck = DateTime.UtcNow;
private string _lastGlobalBlockPlayer = string.Empty;
@@ -53,7 +53,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public DalamudUtilService(ILogger<DalamudUtilService> logger, IClientState clientState, IObjectTable objectTable, IFramework framework,
IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, IGameConfig gameConfig,
BlockedCharacterHandler blockedCharacterHandler, LightlessMediator mediator, PerformanceCollectorService performanceCollector,
LightlessConfigService configService)
LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService)
{
_logger = logger;
_clientState = clientState;
@@ -67,6 +67,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
Mediator = mediator;
_performanceCollector = performanceCollector;
_configService = configService;
_playerPerformanceConfigService = playerPerformanceConfigService;
WorldData = new(() =>
{
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(Dalamud.Game.ClientLanguage.English)!
@@ -135,7 +136,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
_cid = RebuildCID();
}
private Lazy<ulong> RebuildCID() => new(GetCID);
private Lazy<ulong> RebuildCID() => new(GetCID);
public bool IsWine { get; init; }
@@ -161,7 +162,9 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public bool IsLoggedIn { get; private set; }
public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread;
public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
public bool IsInCombatOrPerforming { get; private set; } = false;
public bool IsInCombat { get; private set; } = false;
public bool IsPerforming { get; private set; } = false;
public bool IsInInstance { get; private set; } = false;
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
public uint ClassJobId => _classJobId!.Value;
public Lazy<Dictionary<uint, string>> JobData { get; private set; }
@@ -310,7 +313,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return await RunOnFrameworkThread(() => _cid.Value.ToString().GetHash256()).ConfigureAwait(false);
}
private unsafe static string GetHashedCIDFromPlayerPointer(nint ptr)
public unsafe static string GetHashedCIDFromPlayerPointer(nint ptr)
{
return ((BattleChara*)ptr)->Character.ContentId.ToString().GetHash256();
}
@@ -418,6 +421,16 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return await RunOnFrameworkThread(() => IsObjectPresent(obj)).ConfigureAwait(false);
}
public IPlayerCharacter? GetPlayerByNameAndWorld(string name, ushort homeWorldId)
{
EnsureIsOnFramework();
return _objectTable
.OfType<IPlayerCharacter>()
.FirstOrDefault(p =>
string.Equals(p.Name.TextValue, name, StringComparison.Ordinal) &&
p.HomeWorld.RowId == homeWorldId);
}
public async Task RunOnFrameworkThread(System.Action act, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)
{
var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
@@ -528,7 +541,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
curWaitTime += tick;
Thread.Sleep(tick);
}
Thread.Sleep(tick * 2);
}
@@ -544,6 +556,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return result;
}
public string? GetWorldNameFromPlayerAddress(nint address)
{
if (address == nint.Zero) return null;
EnsureIsOnFramework();
var playerCharacter = _objectTable.OfType<IPlayerCharacter>().FirstOrDefault(p => p.Address == address);
if (playerCharacter == null) return null;
var worldId = (ushort)playerCharacter.HomeWorld.RowId;
return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
}
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
{
var gameObj = (GameObject*)address;
@@ -668,19 +692,47 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
Mediator.Publish(new GposeEndMessage());
}
if ((_condition[ConditionFlag.Performing] || _condition[ConditionFlag.InCombat]) && !IsInCombatOrPerforming)
{
_logger.LogDebug("Combat/Performance start");
IsInCombatOrPerforming = true;
Mediator.Publish(new CombatOrPerformanceStartMessage());
Mediator.Publish(new HaltScanMessage(nameof(IsInCombatOrPerforming)));
if ((_condition[ConditionFlag.InCombat]) && !IsInCombat && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat)
{
_logger.LogDebug("Combat start");
IsInCombat = true;
Mediator.Publish(new CombatStartMessage());
Mediator.Publish(new HaltScanMessage(nameof(IsInCombat)));
}
else if ((!_condition[ConditionFlag.Performing] && !_condition[ConditionFlag.InCombat]) && IsInCombatOrPerforming)
else if ((!_condition[ConditionFlag.InCombat]) && IsInCombat && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat)
{
_logger.LogDebug("Combat/Performance end");
IsInCombatOrPerforming = false;
Mediator.Publish(new CombatOrPerformanceEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(IsInCombatOrPerforming)));
_logger.LogDebug("Combat end");
IsInCombat = false;
Mediator.Publish(new CombatEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(IsInCombat)));
}
if (_condition[ConditionFlag.Performing] && !IsPerforming && _playerPerformanceConfigService.Current.PauseWhilePerforming)
{
_logger.LogDebug("Performance start");
IsInCombat = true;
Mediator.Publish(new PerformanceStartMessage());
Mediator.Publish(new HaltScanMessage(nameof(IsPerforming)));
}
else if (!_condition[ConditionFlag.Performing] && IsPerforming && _playerPerformanceConfigService.Current.PauseWhilePerforming)
{
_logger.LogDebug("Performance end");
IsInCombat = false;
Mediator.Publish(new PerformanceEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(IsPerforming)));
}
if ((_condition[ConditionFlag.BoundByDuty]) && !IsInInstance && _playerPerformanceConfigService.Current.PauseInInstanceDuty)
{
_logger.LogDebug("Instance start");
IsInInstance = true;
Mediator.Publish(new InstanceOrDutyStartMessage());
Mediator.Publish(new HaltScanMessage(nameof(IsInInstance)));
}
else if (((!_condition[ConditionFlag.BoundByDuty]) && IsInInstance && _playerPerformanceConfigService.Current.PauseInInstanceDuty) || ((_condition[ConditionFlag.BoundByDuty]) && IsInInstance && !_playerPerformanceConfigService.Current.PauseInInstanceDuty))
{
_logger.LogDebug("Instance end");
IsInInstance = false;
Mediator.Publish(new InstanceOrDutyEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(IsInInstance)));
}
if (_condition[ConditionFlag.WatchingCutscene] && !IsInCutscene)
@@ -736,7 +788,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
_classJobId = localPlayer.ClassJob.RowId;
}
if (!IsInCombatOrPerforming)
if (!IsInCombat || !IsPerforming || !IsInInstance)
Mediator.Publish(new FrameworkUpdateMessage());
Mediator.Publish(new PriorityFrameworkUpdateMessage());
@@ -765,7 +817,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
IsLodEnabled = lodEnabled;
}
if (IsInCombatOrPerforming)
if (IsInCombat || IsPerforming || IsInInstance)
Mediator.Publish(new FrameworkUpdateMessage());
Mediator.Publish(new DelayedFrameworkUpdateMessage());

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -79,8 +79,12 @@ public record OpenPermissionWindow(Pair Pair) : MessageBase;
public record DownloadLimitChangedMessage() : SameThreadMessage;
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
public record TargetPairMessage(Pair Pair) : MessageBase;
public record CombatOrPerformanceStartMessage : MessageBase;
public record CombatOrPerformanceEndMessage : MessageBase;
public record CombatStartMessage : MessageBase;
public record CombatEndMessage : MessageBase;
public record PerformanceStartMessage : MessageBase;
public record PerformanceEndMessage : MessageBase;
public record InstanceOrDutyStartMessage : MessageBase;
public record InstanceOrDutyEndMessage : MessageBase;
public record EventMessage(Event Event) : MessageBase;
public record PenumbraDirectoryChangedMessage(string? ModDirectory) : MessageBase;
public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage;
@@ -93,5 +97,10 @@ public record GPoseLobbyReceiveCharaData(CharaDataDownloadDto CharaDataDownloadD
public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase;
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
public record SyncshellBroadcastsUpdatedMessage : MessageBase;
public record PairRequestsUpdatedMessage : MessageBase;
public record VisibilityChange : MessageBase;
#pragma warning restore S2094
#pragma warning restore MA0048 // File name must match type name

View File

@@ -1,4 +1,5 @@
using Dalamud.Interface.Windowing;
using LightlessSync.UI.Style;
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services.Mediator;
@@ -33,6 +34,18 @@ public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber
GC.SuppressFinalize(this);
}
public override void PreDraw()
{
base.PreDraw();
MainStyle.PushStyle(); // internally checks ShouldUseTheme
}
public override void PostDraw()
{
MainStyle.PopStyle(); // always attempts to pop if pushed
base.PostDraw();
}
public override void Draw()
{
_performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal);

View File

@@ -0,0 +1,301 @@
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LightlessSync.Services.Mediator;
using LightlessSync.UI;
using LightlessSync.Utils;
// Created using https://github.com/PunishedPineapple/Distance as a reference, thank you!
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public unsafe class NameplateHandler : IMediatorSubscriber
{
private readonly ILogger<NameplateHandler> _logger;
private readonly IAddonLifecycle _addonLifecycle;
private readonly IGameGui _gameGui;
private readonly DalamudUtilService _dalamudUtil;
private readonly LightlessMediator _mediator;
public LightlessMediator Mediator => _mediator;
private bool mEnabled = false;
private bool _needsLabelRefresh = false;
private AddonNamePlate* mpNameplateAddon = null;
private readonly AtkTextNode*[] mTextNodes = new AtkTextNode*[AddonNamePlate.NumNamePlateObjects];
internal const uint mNameplateNodeIDBase = 0x7D99D500;
private volatile HashSet<string> _activeBroadcastingCids = new();
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessMediator mediator)
{
_logger = logger;
_addonLifecycle = addonLifecycle;
_gameGui = gameGui;
_dalamudUtil = dalamudUtil;
_mediator = mediator;
}
internal void Init()
{
EnableNameplate();
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
}
internal void Uninit()
{
DisableNameplate();
DestroyNameplateNodes();
_mediator.Unsubscribe<PriorityFrameworkUpdateMessage>(this);
mpNameplateAddon = null;
}
internal void EnableNameplate()
{
if (!mEnabled)
{
try
{
_addonLifecycle.RegisterListener(AddonEvent.PostDraw, "NamePlate", NameplateDrawDetour);
mEnabled = true;
}
catch (Exception e)
{
_logger.LogError($"Unknown error while trying to enable nameplate distances:\n{e}");
DisableNameplate();
}
}
}
internal void DisableNameplate()
{
if (mEnabled)
{
try
{
_addonLifecycle.UnregisterListener(NameplateDrawDetour);
}
catch (Exception e)
{
_logger.LogError($"Unknown error while unregistering nameplate listener:\n{e}");
}
mEnabled = false;
HideAllNameplateNodes();
}
}
private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
{
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
if (mpNameplateAddon != pNameplateAddon)
{
for (int i = 0; i < mTextNodes.Length; ++i) mTextNodes[i] = null;
mpNameplateAddon = pNameplateAddon;
if (mpNameplateAddon != null) CreateNameplateNodes();
}
UpdateNameplateNodes();
}
private void CreateNameplateNodes()
{
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
{
var nameplateObject = GetNameplateObject(i);
if (nameplateObject == null)
continue;
var pNameplateResNode = nameplateObject.Value.NameContainer;
var pNewNode = AtkNodeHelpers.CreateOrphanTextNode(mNameplateNodeIDBase + (uint)i, TextFlags.Edge | TextFlags.Glare);
if (pNewNode != null)
{
var pLastChild = pNameplateResNode->ChildNode;
while (pLastChild->PrevSiblingNode != null) pLastChild = pLastChild->PrevSiblingNode;
pNewNode->AtkResNode.NextSiblingNode = pLastChild;
pNewNode->AtkResNode.ParentNode = pNameplateResNode;
pLastChild->PrevSiblingNode = (AtkResNode*)pNewNode;
nameplateObject.Value.RootComponentNode->Component->UldManager.UpdateDrawNodeList();
pNewNode->AtkResNode.SetUseDepthBasedPriority(true);
mTextNodes[i] = pNewNode;
}
}
}
private void DestroyNameplateNodes()
{
var pCurrentNameplateAddon = (AddonNamePlate*)_gameGui.GetAddonByName("NamePlate", 1).Address;
if (mpNameplateAddon == null || mpNameplateAddon != pCurrentNameplateAddon)
return;
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
{
var pTextNode = mTextNodes[i];
var pNameplateNode = GetNameplateComponentNode(i);
if (pTextNode != null && pNameplateNode != null)
{
try
{
if (pTextNode->AtkResNode.PrevSiblingNode != null)
pTextNode->AtkResNode.PrevSiblingNode->NextSiblingNode = pTextNode->AtkResNode.NextSiblingNode;
if (pTextNode->AtkResNode.NextSiblingNode != null)
pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode;
pNameplateNode->Component->UldManager.UpdateDrawNodeList();
pTextNode->AtkResNode.Destroy(true);
mTextNodes[i] = null;
}
catch (Exception e)
{
_logger.LogError($"Unknown error while removing text node 0x{(IntPtr)pTextNode:X} for nameplate {i} on component node 0x{(IntPtr)pNameplateNode:X}:\n{e}");
}
}
}
}
private void HideAllNameplateNodes()
{
for (int i = 0; i < mTextNodes.Length; ++i)
{
HideNameplateTextNode(i);
}
}
private void UpdateNameplateNodes()
{
var framework = Framework.Instance();
var ui3DModule = framework->GetUIModule()->GetUI3DModule();
if (ui3DModule == null)
return;
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
{
var objectInfo = ui3DModule->NamePlateObjectInfoPointers[i].Value;
if (objectInfo == null || objectInfo->GameObject == null)
continue;
var nameplateIndex = objectInfo->NamePlateIndex;
if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects)
continue;
var pNode = mTextNodes[nameplateIndex];
if (pNode == null)
continue;
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject);
//_logger.LogInformation($"checking cid: {cid}", cid);
if (cid == null || !_activeBroadcastingCids.Contains(cid))
{
pNode->AtkResNode.ToggleVisibility(false);
continue;
}
pNode->AtkResNode.ToggleVisibility(true);
var nameplateObject = mpNameplateAddon->NamePlateObjectArray[nameplateIndex];
nameplateObject.RootComponentNode->Component->UldManager.UpdateDrawNodeList();
var nameContainer = nameplateObject.NameContainer;
var nameText = nameplateObject.NameText;
var labelColor = UIColors.Get("LightlessPurple");
var edgeColor = UIColors.Get("FullBlack");
var labelY = nameContainer->Height - nameplateObject.TextH - (int)(24 * nameText->AtkResNode.ScaleY);
pNode->AtkResNode.SetPositionShort(58, (short)labelY);
pNode->AtkResNode.SetUseDepthBasedPriority(true);
pNode->AtkResNode.SetScale(0.5f, 0.5f);
pNode->AtkResNode.Color.A = 255;
pNode->TextColor.R = (byte)(labelColor.X * 255);
pNode->TextColor.G = (byte)(labelColor.Y * 255);
pNode->TextColor.B = (byte)(labelColor.Z * 255);
pNode->TextColor.A = (byte)(labelColor.W * 255);
pNode->EdgeColor.R = (byte)(edgeColor.X * 255);
pNode->EdgeColor.G = (byte)(edgeColor.Y * 255);
pNode->EdgeColor.B = (byte)(edgeColor.Z * 255);
pNode->EdgeColor.A = (byte)(edgeColor.W * 255);
pNode->FontSize = 24;
pNode->AlignmentType = AlignmentType.Center;
pNode->FontType = FontType.MiedingerMed;
pNode->LineSpacing = 24;
pNode->CharSpacing = 1;
pNode->TextFlags = TextFlags.Edge | TextFlags.Glare;
pNode->SetText("Lightfinder");
}
}
private void HideNameplateTextNode(int i)
{
var pNode = mTextNodes[i];
if (pNode != null)
{
pNode->AtkResNode.ToggleVisibility(false);
}
}
private AddonNamePlate.NamePlateObject? GetNameplateObject(int i)
{
if (i < AddonNamePlate.NumNamePlateObjects &&
mpNameplateAddon != null &&
mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null)
{
return mpNameplateAddon->NamePlateObjectArray[i];
}
else
{
return null;
}
}
private AtkComponentNode* GetNameplateComponentNode(int i)
{
var nameplateObject = GetNameplateObject(i);
return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null;
}
public void FlagRefresh()
{
_needsLabelRefresh = true;
}
public void OnTick(PriorityFrameworkUpdateMessage _)
{
if (_needsLabelRefresh)
{
UpdateNameplateNodes();
_needsLabelRefresh = false;
}
}
public void UpdateBroadcastingCids(IEnumerable<string> cids)
{
var newSet = cids.ToHashSet();
var changed = !_activeBroadcastingCids.SetEquals(newSet);
if (!changed)
return;
_activeBroadcastingCids.Clear();
foreach (var cid in newSet)
_activeBroadcastingCids.Add(cid);
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(",", _activeBroadcastingCids));
FlagRefresh();
}
}

View File

@@ -0,0 +1,115 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.Gui.NamePlate;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.UI;
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public class NameplateService : DisposableMediatorSubscriberBase
{
private readonly ILogger<NameplateService> _logger;
private readonly LightlessConfigService _configService;
private readonly IClientState _clientState;
private readonly INamePlateGui _namePlateGui;
private readonly PairManager _pairManager;
public NameplateService(ILogger<NameplateService> logger,
LightlessConfigService configService,
INamePlateGui namePlateGui,
IClientState clientState,
PairManager pairManager,
LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
{
_logger = logger;
_configService = configService;
_namePlateGui = namePlateGui;
_clientState = clientState;
_pairManager = pairManager;
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
_namePlateGui.RequestRedraw();
Mediator.Subscribe<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw());
}
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
{
if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen))
return;
var visibleUsersIds = _pairManager.GetOnlineUserPairs()
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
.Select(u => (ulong)u.PlayerCharacterId)
.ToHashSet();
var now = DateTime.UtcNow;
var colors = _configService.Current.NameplateColors;
foreach (var handler in handlers)
{
var playerCharacter = handler.PlayerCharacter;
if (playerCharacter == null)
continue;
var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember);
var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend);
bool partyColorAllowed = (_configService.Current.overridePartyColor && isInParty);
bool friendColorAllowed = (_configService.Current.overrideFriendColor && isFriend);
if (visibleUsersIds.Contains(handler.GameObjectId) &&
!(
(isInParty && !partyColorAllowed) ||
(isFriend && !friendColorAllowed)
))
{
handler.NameParts.TextWrap = CreateTextWrap(colors);
if (_configService.Current.overrideFcTagColor)
{
bool hasActualFcTag = playerCharacter.CompanyTag.TextValue.Length > 0;
bool isFromDifferentRealm = playerCharacter.HomeWorld.RowId != playerCharacter.CurrentWorld.RowId;
bool shouldColorFcArea = hasActualFcTag || (!hasActualFcTag && isFromDifferentRealm);
if (shouldColorFcArea)
{
handler.FreeCompanyTagParts.OuterWrap = CreateTextWrap(colors);
handler.FreeCompanyTagParts.TextWrap = CreateTextWrap(colors);
}
}
}
}
}
public void RequestRedraw()
{
_namePlateGui.RequestRedraw();
}
private static (SeString, SeString) CreateTextWrap(DtrEntry.Colors color)
{
var left = new Lumina.Text.SeStringBuilder();
var right = new Lumina.Text.SeStringBuilder();
left.PushColorRgba(color.Foreground);
right.PopColor();
left.PushEdgeColorRgba(color.Glow);
right.PopEdgeColor();
return (left.ToReadOnlySeString().ToDalamudString(), right.ToReadOnlySeString().ToDalamudString());
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate;
_namePlateGui.RequestRedraw();
}
}

View File

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

View File

@@ -23,15 +23,18 @@ public class ServerConfigurationManager
private readonly ILogger<ServerConfigurationManager> _logger;
private readonly LightlessMediator _lightlessMediator;
private readonly NotesConfigService _notesConfig;
private readonly ServerTagConfigService _serverTagConfig;
private readonly PairTagConfigService _pairTagConfig;
private readonly SyncshellTagConfigService _syncshellTagConfig;
private readonly int _maxCharactersFolder = 20;
public ServerConfigurationManager(ILogger<ServerConfigurationManager> logger, ServerConfigService configService,
ServerTagConfigService serverTagConfig, NotesConfigService notesConfig, DalamudUtilService dalamudUtil,
PairTagConfigService pairTagConfig, SyncshellTagConfigService syncshellTagConfig, NotesConfigService notesConfig, DalamudUtilService dalamudUtil,
LightlessConfigService lightlessConfigService, HttpClient httpClient, LightlessMediator lightlessMediator)
{
_logger = logger;
_configService = configService;
_serverTagConfig = serverTagConfig;
_pairTagConfig = pairTagConfig;
_syncshellTagConfig = syncshellTagConfig;
_notesConfig = notesConfig;
_dalamudUtil = dalamudUtil;
_lightlessConfigService = lightlessConfigService;
@@ -258,7 +261,7 @@ public class ServerConfigurationManager
{
if (serverSelectionIndex == -1) serverSelectionIndex = CurrentServerIndex;
var server = GetServerByIndex(serverSelectionIndex);
if (server.Authentications.Any(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), StringComparison.Ordinal)
if (server.Authentications.Exists(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), StringComparison.Ordinal)
&& c.WorldId == _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult()))
return;
@@ -277,15 +280,15 @@ public class ServerConfigurationManager
var server = GetServerByIndex(serverSelectionIndex);
server.Authentications.Add(new Authentication()
{
SecretKeyIdx = server.SecretKeys.Any() ? server.SecretKeys.First().Key : -1,
SecretKeyIdx = server.SecretKeys.Count != 0 ? server.SecretKeys.First().Key : -1,
});
Save();
}
internal void AddOpenPairTag(string tag)
{
CurrentServerTagStorage().OpenPairTags.Add(tag);
_serverTagConfig.Save();
CurrentPairTagStorage().OpenPairTags.Add(tag);
_pairTagConfig.Save();
}
internal void AddServer(ServerStorage serverStorage)
@@ -294,36 +297,79 @@ public class ServerConfigurationManager
Save();
}
internal void AddTag(string tag)
internal void AddPairTag(string tag)
{
CurrentServerTagStorage().ServerAvailablePairTags.Add(tag);
_serverTagConfig.Save();
_lightlessMediator.Publish(new RefreshUiMessage());
if (tag.Length <= _maxCharactersFolder)
{
CurrentPairTagStorage().ServerAvailablePairTags.Add(tag);
_pairTagConfig.Save();
_lightlessMediator.Publish(new RefreshUiMessage());
}
else
{
_logger.LogInformation("Couldn't save/add {tag}. Name too long to be saved", tag);
}
}
internal void AddSyncshellTag(string tag)
{
if (tag.Length <= _maxCharactersFolder)
{
CurrentSyncshellTagStorage().ServerAvailableSyncshellTags.Add(tag);
_syncshellTagConfig.Save();
_lightlessMediator.Publish(new RefreshUiMessage());
}
else
{
_logger.LogInformation("Couldn't save/add {tag}. Name too long to be saved", tag);
}
}
internal void AddTagForUid(string uid, string tagName)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
if (CurrentPairTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
tags.Add(tagName);
_lightlessMediator.Publish(new RefreshUiMessage());
}
else
{
CurrentServerTagStorage().UidServerPairedUserTags[uid] = [tagName];
CurrentPairTagStorage().UidServerPairedUserTags[uid] = [tagName];
}
_serverTagConfig.Save();
_pairTagConfig.Save();
}
internal bool ContainsOpenPairTag(string tag)
internal void AddTagForSyncshell(string syncshellName, string tagName)
{
return CurrentServerTagStorage().OpenPairTags.Contains(tag);
if (CurrentSyncshellTagStorage().SyncshellPairedTags.TryGetValue(syncshellName, out var tags))
{
tags.Add(tagName);
_lightlessMediator.Publish(new RefreshUiMessage());
}
else
{
CurrentSyncshellTagStorage().SyncshellPairedTags[syncshellName] = [tagName];
}
_syncshellTagConfig.Save();
}
internal bool ContainsTag(string uid, string tag)
internal bool ContainsOpenPairTag(string tag) => CurrentPairTagStorage().OpenPairTags.Contains(tag);
internal bool ContainsPairTag(string uid, string tag)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
if (CurrentPairTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
return tags.Contains(tag, StringComparer.Ordinal);
}
return false;
}
internal bool ContainsSyncshellTag(string name, string tag)
{
if (CurrentSyncshellTagStorage().SyncshellPairedTags.TryGetValue(name, out var tags))
{
return tags.Contains(tag, StringComparer.Ordinal);
}
@@ -364,30 +410,19 @@ public class ServerConfigurationManager
return null;
}
internal HashSet<string> GetServerAvailablePairTags()
{
return CurrentServerTagStorage().ServerAvailablePairTags;
}
internal HashSet<string> GetServerAvailablePairTags() => CurrentPairTagStorage().ServerAvailablePairTags;
internal Dictionary<string, List<string>> GetUidServerPairedUserTags()
{
return CurrentServerTagStorage().UidServerPairedUserTags;
}
internal HashSet<string> GetServerAvailableSyncshellTags() => CurrentSyncshellTagStorage().ServerAvailableSyncshellTags;
internal HashSet<string> GetUidsForTag(string tag)
{
return CurrentServerTagStorage().UidServerPairedUserTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal);
}
internal Dictionary<string, List<string>> GetUidServerPairedUserTags() => CurrentPairTagStorage().UidServerPairedUserTags;
internal bool HasTags(string uid)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
return tags.Any();
}
internal HashSet<string> GetUidsForPairTag(string tag) => CurrentPairTagStorage().UidServerPairedUserTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal);
return false;
}
internal HashSet<string> GetNamesForSyncshellTag(string tag) => CurrentSyncshellTagStorage().SyncshellPairedTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal);
internal bool HasPairTags(string uid) => CurrentPairTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags) && tags.Count != 0;
internal bool HasSyncshellTags(string name) => CurrentSyncshellTagStorage().SyncshellPairedTags.TryGetValue(name, out var tags) && tags.Count != 0;
internal void RemoveCharacterFromServer(int serverSelectionIndex, Authentication item)
{
@@ -398,51 +433,96 @@ public class ServerConfigurationManager
internal void RemoveOpenPairTag(string tag)
{
CurrentServerTagStorage().OpenPairTags.Remove(tag);
_serverTagConfig.Save();
CurrentPairTagStorage().OpenPairTags.Remove(tag);
_pairTagConfig.Save();
}
internal void RemoveTag(string tag)
internal void RemovePairTag(string tag)
{
CurrentServerTagStorage().ServerAvailablePairTags.Remove(tag);
foreach (var uid in GetUidsForTag(tag))
{
RemoveTagForUid(uid, tag, save: false);
}
_serverTagConfig.Save();
RemoveTag(CurrentPairTagStorage().ServerAvailablePairTags, tag);
_pairTagConfig.Save();
_lightlessMediator.Publish(new RefreshUiMessage());
}
internal void RemoveSyncshellTag(string tag)
{
RemoveTag(CurrentSyncshellTagStorage().ServerAvailableSyncshellTags, tag, true);
_syncshellTagConfig.Save();
_lightlessMediator.Publish(new RefreshUiMessage());
}
internal void RemoveTag(HashSet<string> storage, string tag, bool syncshell = false)
{
storage.Remove(tag);
if (syncshell)
{
foreach (var uid in GetNamesForSyncshellTag(tag))
{
RemoveTagForSyncshell(uid, tag, save: false);
}
}
else
{
foreach (var uid in GetUidsForPairTag(tag))
{
RemoveTagForUid(uid, tag, save: false);
}
}
}
internal void RemoveTagForUid(string uid, string tagName, bool save = true)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
if (CurrentPairTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
tags.Remove(tagName);
if (save)
{
_serverTagConfig.Save();
_pairTagConfig.Save();
_lightlessMediator.Publish(new RefreshUiMessage());
}
}
}
internal void RenameTag(string oldName, string newName)
internal void RemoveTagForSyncshell(string name, string tagName, bool save = true)
{
CurrentServerTagStorage().ServerAvailablePairTags.Remove(oldName);
CurrentServerTagStorage().ServerAvailablePairTags.Add(newName);
foreach (var existingTags in CurrentServerTagStorage().UidServerPairedUserTags.Select(k => k.Value))
if (CurrentSyncshellTagStorage().SyncshellPairedTags.TryGetValue(name, out var tags))
{
if (existingTags.Remove(oldName))
existingTags.Add(newName);
tags.Remove(tagName);
if (save)
{
_syncshellTagConfig.Save();
_lightlessMediator.Publish(new RefreshUiMessage());
}
}
}
internal void SaveNotes()
internal void RenamePairTag(string oldName, string newName) => RenameTag(CurrentPairTagStorage().UidServerPairedUserTags, CurrentPairTagStorage().ServerAvailablePairTags, oldName, newName);
internal void RenameSyncshellTag(string oldName, string newName) => RenameTag(CurrentSyncshellTagStorage().SyncshellPairedTags, CurrentSyncshellTagStorage().ServerAvailableSyncshellTags, oldName, newName);
internal void RenameTag(Dictionary<string, List<string>> tags, HashSet<string> storage, string oldName, string newName)
{
_notesConfig.Save();
if (newName.Length > _maxCharactersFolder)
{
storage.Remove(oldName);
storage.Add(newName);
foreach (var existingTags in tags.Select(k => k.Value))
{
if (existingTags.Remove(oldName))
existingTags.Add(newName);
}
_lightlessMediator.Publish(new RefreshUiMessage());
}
else
{
_logger.LogInformation("Couldn't save/add {tag}. Name too long to be saved", newName);
}
}
internal void SaveNotes() => _notesConfig.Save();
internal void SetNoteForGid(string gid, string note, bool save = true)
{
if (string.IsNullOrEmpty(gid)) return;
@@ -476,10 +556,16 @@ public class ServerConfigurationManager
return _notesConfig.Current.ServerNotes[CurrentApiUrl];
}
private ServerTagStorage CurrentServerTagStorage()
private PairTagStorage CurrentPairTagStorage()
{
TryCreateCurrentServerTagStorage();
return _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl];
TryCreateCurrentPairTagStorage();
return _pairTagConfig.Current.ServerTagStorage[CurrentApiUrl];
}
private SyncshellTagStorage CurrentSyncshellTagStorage()
{
TryCreateCurrentSyncshellTagStorage();
return _syncshellTagConfig.Current.ServerTagStorage[CurrentApiUrl];
}
private void EnsureMainExists()
@@ -499,11 +585,19 @@ public class ServerConfigurationManager
}
}
private void TryCreateCurrentServerTagStorage()
private void TryCreateCurrentPairTagStorage()
{
if (!_serverTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl))
if (!_pairTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl))
{
_serverTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new();
_pairTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new();
}
}
private void TryCreateCurrentSyncshellTagStorage()
{
if (!_syncshellTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl))
{
_syncshellTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new();
}
}

View File

@@ -3,7 +3,6 @@ using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI;
using LightlessSync.UI.Components.Popup;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;

View File

@@ -4,7 +4,6 @@ using Dalamud.Interface.Windowing;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator;
using LightlessSync.UI;
using LightlessSync.UI.Components.Popup;
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;

View File

@@ -0,0 +1,421 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
using LightlessSync.API.Dto.Group;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using System.Numerics;
namespace LightlessSync.UI
{
public class BroadcastUI : WindowMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly LightlessConfigService _configService;
private readonly BroadcastService _broadcastService;
private readonly UiSharedService _uiSharedService;
private readonly BroadcastScannerService _broadcastScannerService;
private IReadOnlyList<GroupFullInfoDto> _allSyncshells;
private string _userUid = string.Empty;
private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new();
public BroadcastUI(
ILogger<BroadcastUI> logger,
LightlessMediator mediator,
PerformanceCollectorService performanceCollectorService,
BroadcastService broadcastService,
LightlessConfigService configService,
UiSharedService uiShared,
ApiController apiController,
BroadcastScannerService broadcastScannerService
) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
{
_broadcastService = broadcastService;
_uiSharedService = uiShared;
_configService = configService;
_apiController = apiController;
_broadcastScannerService = broadcastScannerService;
IsOpen = false;
this.SizeConstraints = new()
{
MinimumSize = new(600, 465),
MaximumSize = new(750, 525)
};
mediator.Subscribe<RefreshUiMessage>(this, async _ => await RefreshSyncshells().ConfigureAwait(false));
}
private void RebuildSyncshellDropdownOptions()
{
var selectedGid = _configService.Current.SelectedFinderSyncshell;
var allSyncshells = _allSyncshells ?? Array.Empty<GroupFullInfoDto>();
var ownedSyncshells = allSyncshells
.Where(g => string.Equals(g.OwnerUID, _userUid, StringComparison.Ordinal))
.ToList();
_syncshellOptions.Clear();
_syncshellOptions.Add(("None", null, true));
var addedGids = new HashSet<string>(StringComparer.Ordinal);
foreach (var shell in ownedSyncshells)
{
var label = shell.GroupAliasOrGID ?? shell.GID;
_syncshellOptions.Add((label, shell.GID, true));
addedGids.Add(shell.GID);
}
if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid))
{
var matching = allSyncshells.FirstOrDefault(g => string.Equals(g.GID, selectedGid, StringComparison.Ordinal));
if (matching != null)
{
var label = matching.GroupAliasOrGID ?? matching.GID;
_syncshellOptions.Add((label, matching.GID, true));
addedGids.Add(matching.GID);
}
}
if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid))
{
_syncshellOptions.Add(($"[Unavailable] {selectedGid}", selectedGid, false));
}
}
public Task RefreshSyncshells()
{
return RefreshSyncshellsInternal();
}
private async Task RefreshSyncshellsInternal()
{
if (!_apiController.IsConnected)
{
_allSyncshells = [];
RebuildSyncshellDropdownOptions();
return;
}
try
{
_allSyncshells = await _apiController.GroupsGetAll().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch Syncshells.");
_allSyncshells = [];
}
RebuildSyncshellDropdownOptions();
}
public override void OnOpen()
{
_userUid = _apiController.UID;
_ = RefreshSyncshellsInternal();
}
protected override void DrawInternal()
{
if (!_broadcastService.IsLightFinderAvailable)
{
_uiSharedService.MediumText("This server doesn't support Lightfinder.", UIColors.Get("LightlessYellow"));
ImGuiHelpers.ScaledDummy(0.25f);
}
if (ImGui.BeginTabBar("##BroadcastTabs"))
{
if (ImGui.BeginTabItem("Lightfinder"))
{
_uiSharedService.MediumText("Lightfinder", UIColors.Get("PairBlue"));
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, -2));
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"),"This lets other Lightless users know you use Lightless. While enabled, you and others using Lightfinder can see each other identified as Lightless users.");
ImGui.Indent(15f);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
ImGui.Text("- This is done using a 'Lightless' label above player nameplates.");
ImGui.PopStyleColor();
ImGui.Unindent(15f);
ImGuiHelpers.ScaledDummy(3f);
_uiSharedService.MediumText("Pairing", UIColors.Get("PairBlue"));
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Pairing may be initiated via the right-click context menu on another player." +
" The process requires mutual confirmation: the sender initiates the request, and the recipient completes it by responding with a request in return.");
_uiSharedService.DrawNoteLine(
"! ",
UIColors.Get("LightlessYellow"),
new SeStringUtils.RichTextEntry("If Lightfinder is "),
new SeStringUtils.RichTextEntry("ENABLED", UIColors.Get("LightlessGreen"), true),
new SeStringUtils.RichTextEntry(" when a pair request is made, the receiving user will get notified about it."));
_uiSharedService.DrawNoteLine(
"! ",
UIColors.Get("LightlessYellow"),
new SeStringUtils.RichTextEntry("If Lightfinder is "),
new SeStringUtils.RichTextEntry("DISABLED", UIColors.Get("DimRed"), true),
new SeStringUtils.RichTextEntry(" when a pair request is made, the receiving user will "),
new SeStringUtils.RichTextEntry("NOT", UIColors.Get("DimRed"), true),
new SeStringUtils.RichTextEntry(" get a notification, and the request will not be visible to them in any way."));
ImGuiHelpers.ScaledDummy(3f);
_uiSharedService.MediumText("Privacy", UIColors.Get("PairBlue"));
_uiSharedService.DrawNoteLine(
"! ",
UIColors.Get("DimRed"),
new SeStringUtils.RichTextEntry("Lightfinder is entirely "),
new SeStringUtils.RichTextEntry("opt-in", UIColors.Get("LightlessYellow"), true),
new SeStringUtils.RichTextEntry(" and does not share any data with other users. All identifying information remains private to the server."));
_uiSharedService.DrawNoteLine("! ", UIColors.Get("DimRed"), "Pairing is intended as a mutual agreement between both parties. A pair request will not be visible to the recipient unless Lightfinder is enabled.");
ImGuiHelpers.ScaledDummy(5f);
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.TextWrapped("Use Lightfinder when you're okay with being visible to other users and understand that you are responsible for your own experience.");
ImGui.PopStyleColor();
ImGui.PopStyleVar();
ImGuiHelpers.ScaledDummy(3f);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
if (_configService.Current.BroadcastEnabled)
{
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGreen"));
ImGui.Text("The Lightfinder calls, and somewhere, a soul may answer."); // cringe..
ImGui.PopStyleColor();
var ttl = _broadcastService.RemainingTtl;
if (ttl is { } remaining && remaining > TimeSpan.Zero)
{
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
ImGui.Text($"Still shining, for {remaining:hh\\:mm\\:ss}");
ImGui.PopStyleColor();
}
else
{
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.Text("The Lightfinder<65>s light wanes, but not in vain."); // cringe..
ImGui.PopStyleColor();
}
}
else
{
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.Text("The Lightfinder rests, waiting to shine again."); // cringe..
ImGui.PopStyleColor();
}
var cooldown = _broadcastService.RemainingCooldown;
if (cooldown is { } cd)
{
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.Text($"The Lightfinder gathers its strength... ({Math.Ceiling(cd.TotalSeconds)}s)");
ImGui.PopStyleColor();
}
ImGuiHelpers.ScaledDummy(0.5f);
bool isBroadcasting = _broadcastService.IsBroadcasting;
bool isOnCooldown = cooldown.HasValue && cooldown.Value.TotalSeconds > 0;
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f);
if (isOnCooldown)
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("DimRed"));
else if (isBroadcasting)
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
else
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("PairBlue"));
if (isOnCooldown || !_broadcastService.IsLightFinderAvailable)
ImGui.BeginDisabled();
string buttonText = isBroadcasting ? "Disable Lightfinder" : "Enable Lightfinder";
if (ImGui.Button(buttonText, new Vector2(200 * ImGuiHelpers.GlobalScale, 0)))
{
_broadcastService.ToggleBroadcast();
}
if (isOnCooldown || !_broadcastService.IsLightFinderAvailable)
ImGui.EndDisabled();
ImGui.PopStyleColor();
ImGui.PopStyleVar();
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Syncshell Finder"))
{
if (_allSyncshells == null)
{
ImGui.Text("Loading Syncshells...");
return;
}
_uiSharedService.MediumText("Syncshell Finder", UIColors.Get("PairBlue"));
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
ImGui.PushTextWrapPos();
ImGui.Text("Allow your owned Syncshell to be indexed by the Nearby Syncshell Finder.");
ImGui.Text("To enable this, select one of your owned Syncshells from the dropdown menu below and ensure that \"Toggle Syncshell Finder\" is enabled. Your Syncshell will be visible in the Nearby Syncshell Finder as long as Lightfinder is active.");
ImGui.PopTextWrapPos();
ImGuiHelpers.ScaledDummy(0.2f);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled;
bool isBroadcasting = _broadcastService.IsBroadcasting;
if (isBroadcasting)
ImGui.BeginDisabled();
if (ImGui.Checkbox("Toggle Syncshell Finder", ref ShellFinderEnabled))
{
_configService.Current.SyncshellFinderEnabled = ShellFinderEnabled;
_configService.Save();
}
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.Text("Toggle to broadcast specified Syncshell.");
ImGui.EndTooltip();
}
var selectedGid = _configService.Current.SelectedFinderSyncshell;
var currentOption = _syncshellOptions.FirstOrDefault(o => string.Equals(o.GID, selectedGid, StringComparison.Ordinal));
var preview = currentOption.Label ?? "Select a Syncshell...";
if (ImGui.BeginCombo("##SyncshellDropdown", preview))
{
foreach (var (label, gid, available) in _syncshellOptions)
{
bool isSelected = string.Equals(gid, selectedGid, StringComparison.Ordinal);
if (!available)
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
if (ImGui.Selectable(label, isSelected))
{
_configService.Current.SelectedFinderSyncshell = gid;
_configService.Save();
}
if (!available && ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.Text("This Syncshell is not available on the current service.");
ImGui.EndTooltip();
}
if (!available)
ImGui.PopStyleColor();
if (isSelected)
ImGui.SetItemDefaultFocus();
}
ImGui.EndCombo();
}
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.Text("Choose one of the available options.");
ImGui.EndTooltip();
}
if (isBroadcasting)
ImGui.EndDisabled();
ImGui.EndTabItem();
}
#if DEBUG
if (ImGui.BeginTabItem("Debug"))
{
ImGui.Text("Broadcast Cache");
if (ImGui.BeginTable("##BroadcastCacheTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 225f)))
{
ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Expires In", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Syncshell GID", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableHeadersRow();
var now = DateTime.UtcNow;
foreach (var (cid, entry) in _broadcastScannerService.BroadcastCache)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted(cid.Truncate(12));
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.TextUnformatted(cid);
ImGui.EndTooltip();
}
ImGui.TableNextColumn();
var colorBroadcast = entry.IsBroadcasting
? UIColors.Get("LightlessGreen")
: UIColors.Get("DimRed");
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorBroadcast));
ImGui.TextUnformatted(entry.IsBroadcasting.ToString());
ImGui.TableNextColumn();
var remaining = entry.ExpiryTime - now;
var colorTtl =
remaining <= TimeSpan.Zero ? UIColors.Get("DimRed") :
remaining < TimeSpan.FromSeconds(10) ? UIColors.Get("LightlessYellow") :
(Vector4?)null;
if (colorTtl != null)
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(colorTtl.Value));
ImGui.TextUnformatted(remaining > TimeSpan.Zero
? remaining.ToString("hh\\:mm\\:ss")
: "Expired");
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.GID ?? "-");
}
ImGui.EndTable();
}
ImGui.EndTabItem();
}
#endif
ImGui.EndTabBar();
}
}
}
}

View File

@@ -8,187 +8,187 @@ namespace LightlessSync.UI;
internal sealed partial class CharaDataHubUi
{
private static string GetAccessTypeString(AccessTypeDto dto) => dto switch
{
AccessTypeDto.AllPairs => "All Pairs",
AccessTypeDto.ClosePairs => "Direct Pairs",
AccessTypeDto.Individuals => "Specified",
AccessTypeDto.Public => "Everyone"
};
private static string GetAccessTypeString(AccessTypeDto dto) => dto switch
{
AccessTypeDto.AllPairs => "All Pairs",
AccessTypeDto.ClosePairs => "Direct Pairs",
AccessTypeDto.Individuals => "Specified",
AccessTypeDto.Public => "Everyone"
};
private static string GetShareTypeString(ShareTypeDto dto) => dto switch
{
ShareTypeDto.Private => "Code Only",
ShareTypeDto.Shared => "Shared"
};
private static string GetShareTypeString(ShareTypeDto dto) => dto switch
{
ShareTypeDto.Private => "Code Only",
ShareTypeDto.Shared => "Shared"
};
private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry)
{
if (!poseEntry.HasWorldData) return "This Pose has no world data attached.";
return poseEntry.WorldDataDescriptor;
}
private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry)
{
if (!poseEntry.HasWorldData) return "This Pose has no world data attached.";
return poseEntry.WorldDataDescriptor;
}
private void GposeMetaInfoAction(Action<CharaDataMetaInfoExtendedDto?> gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning)
{
StringBuilder sb = new StringBuilder();
private void GposeMetaInfoAction(Action<CharaDataMetaInfoExtendedDto?> gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(actionDescription);
bool isDisabled = false;
sb.AppendLine(actionDescription);
bool isDisabled = false;
void AddErrorStart(StringBuilder sb)
{
sb.Append(UiSharedService.TooltipSeparator);
sb.AppendLine("Cannot execute:");
}
void AddErrorStart(StringBuilder sb)
{
sb.Append(UiSharedService.TooltipSeparator);
sb.AppendLine("Cannot execute:");
}
if (dto == null)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- No metainfo present");
isDisabled = true;
}
if (!dto?.CanBeDownloaded ?? false)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Character is not downloadable");
isDisabled = true;
}
if (!_uiSharedService.IsInGpose)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires to be in GPose");
isDisabled = true;
}
if (!hasValidGposeTarget && !isSpawning)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires a valid GPose target");
isDisabled = true;
}
if (isSpawning && !_charaDataManager.BrioAvailable)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires Brio to be installed.");
isDisabled = true;
}
if (dto == null)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- No metainfo present");
isDisabled = true;
}
if (!dto?.CanBeDownloaded ?? false)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Character is not downloadable");
isDisabled = true;
}
if (!_uiSharedService.IsInGpose)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires to be in GPose");
isDisabled = true;
}
if (!hasValidGposeTarget && !isSpawning)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires a valid GPose target");
isDisabled = true;
}
if (isSpawning && !_charaDataManager.BrioAvailable)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires Brio to be installed.");
isDisabled = true;
}
using (ImRaii.Group())
{
using var dis = ImRaii.Disabled(isDisabled);
gposeActionDraw.Invoke(dto);
}
if (sb.Length > 0)
{
UiSharedService.AttachToolTip(sb.ToString());
}
}
using (ImRaii.Group())
{
using var dis = ImRaii.Disabled(isDisabled);
gposeActionDraw.Invoke(dto);
}
if (sb.Length > 0)
{
UiSharedService.AttachToolTip(sb.ToString());
}
}
private void GposePoseAction(Action poseActionDraw, string poseDescription, bool hasValidGposeTarget)
{
StringBuilder sb = new StringBuilder();
private void GposePoseAction(Action poseActionDraw, string poseDescription, bool hasValidGposeTarget)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(poseDescription);
bool isDisabled = false;
sb.AppendLine(poseDescription);
bool isDisabled = false;
void AddErrorStart(StringBuilder sb)
{
sb.Append(UiSharedService.TooltipSeparator);
sb.AppendLine("Cannot execute:");
}
void AddErrorStart(StringBuilder sb)
{
sb.Append(UiSharedService.TooltipSeparator);
sb.AppendLine("Cannot execute:");
}
if (!_uiSharedService.IsInGpose)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires to be in GPose");
isDisabled = true;
}
if (!hasValidGposeTarget)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires a valid GPose target");
isDisabled = true;
}
if (!_charaDataManager.BrioAvailable)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires Brio to be installed.");
isDisabled = true;
}
if (!_uiSharedService.IsInGpose)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires to be in GPose");
isDisabled = true;
}
if (!hasValidGposeTarget)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires a valid GPose target");
isDisabled = true;
}
if (!_charaDataManager.BrioAvailable)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires Brio to be installed.");
isDisabled = true;
}
using (ImRaii.Group())
{
using var dis = ImRaii.Disabled(isDisabled);
poseActionDraw.Invoke();
}
if (sb.Length > 0)
{
UiSharedService.AttachToolTip(sb.ToString());
}
}
using (ImRaii.Group())
{
using var dis = ImRaii.Disabled(isDisabled);
poseActionDraw.Invoke();
}
if (sb.Length > 0)
{
UiSharedService.AttachToolTip(sb.ToString());
}
}
private void SetWindowSizeConstraints(bool? inGposeTab = null)
{
SizeConstraints = new()
{
MinimumSize = new((inGposeTab ?? false) ? 400 : 1000, 500),
MaximumSize = new((inGposeTab ?? false) ? 400 : 1000, 2000)
};
}
private void SetWindowSizeConstraints(bool? inGposeTab = null)
{
SizeConstraints = new()
{
MinimumSize = new((inGposeTab ?? false) ? 400 : 1000, 500),
MaximumSize = new((inGposeTab ?? false) ? 400 : 1000, 2000)
};
}
private void UpdateFilteredFavorites()
{
_ = Task.Run(async () =>
{
if (_charaDataManager.DownloadMetaInfoTask != null)
{
await _charaDataManager.DownloadMetaInfoTask.ConfigureAwait(false);
}
Dictionary<string, (CharaDataFavorite, CharaDataMetaInfoExtendedDto?, bool)> newFiltered = [];
foreach (var favorite in _configService.Current.FavoriteCodes)
{
var uid = favorite.Key.Split(":")[0];
var note = _serverConfigurationManager.GetNoteForUid(uid) ?? string.Empty;
bool hasMetaInfo = _charaDataManager.TryGetMetaInfo(favorite.Key, out var metaInfo);
bool addFavorite =
(string.IsNullOrEmpty(_filterCodeNote)
|| (note.Contains(_filterCodeNote, StringComparison.OrdinalIgnoreCase)
|| uid.Contains(_filterCodeNote, StringComparison.OrdinalIgnoreCase)))
&& (string.IsNullOrEmpty(_filterDescription)
|| (favorite.Value.CustomDescription.Contains(_filterDescription, StringComparison.OrdinalIgnoreCase)
|| (metaInfo != null && metaInfo!.Description.Contains(_filterDescription, StringComparison.OrdinalIgnoreCase))))
&& (!_filterPoseOnly
|| (metaInfo != null && metaInfo!.HasPoses))
&& (!_filterWorldOnly
|| (metaInfo != null && metaInfo!.HasWorldData));
if (addFavorite)
{
newFiltered[favorite.Key] = (favorite.Value, metaInfo, hasMetaInfo);
}
}
private void UpdateFilteredFavorites()
{
_ = Task.Run(async () =>
{
if (_charaDataManager.DownloadMetaInfoTask != null)
{
await _charaDataManager.DownloadMetaInfoTask.ConfigureAwait(false);
}
Dictionary<string, (CharaDataFavorite, CharaDataMetaInfoExtendedDto?, bool)> newFiltered = [];
foreach (var favorite in _configService.Current.FavoriteCodes)
{
var uid = favorite.Key.Split(":")[0];
var note = _serverConfigurationManager.GetNoteForUid(uid) ?? string.Empty;
bool hasMetaInfo = _charaDataManager.TryGetMetaInfo(favorite.Key, out var metaInfo);
bool addFavorite =
(string.IsNullOrEmpty(_filterCodeNote)
|| (note.Contains(_filterCodeNote, StringComparison.OrdinalIgnoreCase)
|| uid.Contains(_filterCodeNote, StringComparison.OrdinalIgnoreCase)))
&& (string.IsNullOrEmpty(_filterDescription)
|| (favorite.Value.CustomDescription.Contains(_filterDescription, StringComparison.OrdinalIgnoreCase)
|| (metaInfo != null && metaInfo!.Description.Contains(_filterDescription, StringComparison.OrdinalIgnoreCase))))
&& (!_filterPoseOnly
|| (metaInfo != null && metaInfo!.HasPoses))
&& (!_filterWorldOnly
|| (metaInfo != null && metaInfo!.HasWorldData));
if (addFavorite)
{
newFiltered[favorite.Key] = (favorite.Value, metaInfo, hasMetaInfo);
}
}
_filteredFavorites = newFiltered;
});
}
_filteredFavorites = newFiltered;
});
}
private void UpdateFilteredItems()
{
if (_charaDataManager.GetSharedWithYouTask == null)
{
_filteredDict = _charaDataManager.SharedWithYouData
.SelectMany(k => k.Value)
.Where(k =>
(!_sharedWithYouDownloadableFilter || k.CanBeDownloaded)
&& (string.IsNullOrEmpty(_sharedWithYouDescriptionFilter) || k.Description.Contains(_sharedWithYouDescriptionFilter, StringComparison.OrdinalIgnoreCase)))
.GroupBy(k => k.Uploader)
.ToDictionary(k =>
{
var note = _serverConfigurationManager.GetNoteForUid(k.Key.UID);
if (note == null) return k.Key.AliasOrUID;
return $"{note} ({k.Key.AliasOrUID})";
}, k => k.ToList(), StringComparer.OrdinalIgnoreCase)
.Where(k => (string.IsNullOrEmpty(_sharedWithYouOwnerFilter) || k.Key.Contains(_sharedWithYouOwnerFilter, StringComparison.OrdinalIgnoreCase)))
.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToDictionary();
}
}
private void UpdateFilteredItems()
{
if (_charaDataManager.GetSharedWithYouTask == null)
{
_filteredDict = _charaDataManager.SharedWithYouData
.SelectMany(k => k.Value)
.Where(k =>
(!_sharedWithYouDownloadableFilter || k.CanBeDownloaded)
&& (string.IsNullOrEmpty(_sharedWithYouDescriptionFilter) || k.Description.Contains(_sharedWithYouDescriptionFilter, StringComparison.OrdinalIgnoreCase)))
.GroupBy(k => k.Uploader)
.ToDictionary(k =>
{
var note = _serverConfigurationManager.GetNoteForUid(k.Key.UID);
if (note == null) return k.Key.AliasOrUID;
return $"{note} ({k.Key.AliasOrUID})";
}, k => k.ToList(), StringComparer.OrdinalIgnoreCase)
.Where(k => (string.IsNullOrEmpty(_sharedWithYouOwnerFilter) || k.Key.Contains(_sharedWithYouOwnerFilter, StringComparison.OrdinalIgnoreCase)))
.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToDictionary();
}
}
}

View File

@@ -63,7 +63,7 @@ internal sealed partial class CharaDataHubUi
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("GPose Lobby");
ImGui.SameLine();
UiSharedService.ColorTextWrapped(_charaDataGposeTogetherManager.CurrentGPoseLobbyId, ImGuiColors.ParsedGreen);
UiSharedService.ColorTextWrapped(_charaDataGposeTogetherManager.CurrentGPoseLobbyId, UIColors.Get("LightlessBlue"));
ImGui.SameLine();
if (_uiSharedService.IconButton(FontAwesomeIcon.Clipboard))
{
@@ -90,7 +90,7 @@ internal sealed partial class CharaDataHubUi
if (!_uiSharedService.IsInGpose)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Assigning users to characters is only available in GPose.", ImGuiColors.DalamudYellow, 300);
UiSharedService.DrawGroupedCenteredColorText("Assigning users to characters is only available in GPose.", UIColors.Get("LightlessYellow"), 300);
}
UiSharedService.DistanceSeparator();
ImGui.TextUnformatted("Users In Lobby");
@@ -104,7 +104,7 @@ internal sealed partial class CharaDataHubUi
if (!_charaDataGposeTogetherManager.UsersInLobby.Any() && !string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId))
{
UiSharedService.DrawGroupedCenteredColorText("No other users in current GPose lobby", ImGuiColors.DalamudYellow);
UiSharedService.DrawGroupedCenteredColorText("No other users in current GPose lobby", UIColors.Get("LightlessYellow"));
}
else
{
@@ -130,7 +130,7 @@ internal sealed partial class CharaDataHubUi
ImGui.AlignTextToFramePadding();
var note = _serverConfigurationManager.GetNoteForUid(user.UserData.UID);
var userText = note == null ? user.UserData.AliasOrUID : $"{note} ({user.UserData.AliasOrUID})";
UiSharedService.ColorText(userText, ImGuiColors.ParsedGreen);
UiSharedService.ColorText(userText, UIColors.Get("LightlessBlue"));
var buttonsize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.ArrowRight).X;
var buttonsize2 = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X;
@@ -165,7 +165,7 @@ internal sealed partial class CharaDataHubUi
UiSharedService.AttachToolTip(user.WorldDataDescriptor + UiSharedService.TooltipSeparator);
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.Map, sameMapAndServer.SameMap ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
_uiSharedService.IconText(FontAwesomeIcon.Map, sameMapAndServer.SameMap ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudRed);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && user.WorldData != null)
{
_dalamudUtilService.SetMarkerAndOpenMap(new(user.WorldData.Value.PositionX, user.WorldData.Value.PositionY, user.WorldData.Value.PositionZ), user.Map);
@@ -175,12 +175,12 @@ internal sealed partial class CharaDataHubUi
+ "Note: For GPose synchronization to work properly, you must be on the same map.");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.Globe, sameMapAndServer.SameServer ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
_uiSharedService.IconText(FontAwesomeIcon.Globe, sameMapAndServer.SameServer ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudRed);
UiSharedService.AttachToolTip((sameMapAndServer.SameMap ? "You are on the same server." : "You are not on the same server.") + UiSharedService.TooltipSeparator
+ "Note: GPose synchronization is not dependent on the current server, but you will have to spawn a character for the other lobby users.");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.Running, sameMapAndServer.SameEverything ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
_uiSharedService.IconText(FontAwesomeIcon.Running, sameMapAndServer.SameEverything ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudRed);
UiSharedService.AttachToolTip(sameMapAndServer.SameEverything ? "You are in the same instanced area." : "You are not the same instanced area." + UiSharedService.TooltipSeparator +
"Note: Users not in your instance, but on the same map, will be drawn as floating wisps." + Environment.NewLine
+ "Note: GPose synchronization is not dependent on the current instance, but you will have to spawn a character for the other lobby users.");

View File

@@ -1,8 +1,8 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Utility;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.Services.CharaData.Models;
using System.Numerics;
@@ -23,7 +23,7 @@ internal sealed partial class CharaDataHubUi
if (dataDto == null)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Select an entry above to edit its data.", ImGuiColors.DalamudYellow);
UiSharedService.DrawGroupedCenteredColorText("Select an entry above to edit its data.", UIColors.Get("LightlessYellow"));
return;
}
@@ -31,7 +31,7 @@ internal sealed partial class CharaDataHubUi
if (updateDto == null)
{
UiSharedService.DrawGroupedCenteredColorText("Something went awfully wrong and there's no update DTO. Try updating Character Data via the button above.", ImGuiColors.DalamudYellow);
UiSharedService.DrawGroupedCenteredColorText("Something went awfully wrong and there's no update DTO. Try updating Character Data via the button above.", UIColors.Get("LightlessYellow"));
return;
}
@@ -75,7 +75,7 @@ internal sealed partial class CharaDataHubUi
}
if (_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted)
{
UiSharedService.ColorTextWrapped("Updating data on server, please wait.", ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped("Updating data on server, please wait.", UIColors.Get("LightlessYellow"));
}
}
@@ -85,7 +85,7 @@ internal sealed partial class CharaDataHubUi
{
if (_charaDataManager.UploadProgress != null)
{
UiSharedService.ColorTextWrapped(_charaDataManager.UploadProgress.Value ?? string.Empty, ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped(_charaDataManager.UploadProgress.Value ?? string.Empty, UIColors.Get("LightlessYellow"));
}
if ((!_charaDataManager.UploadTask?.IsCompleted ?? false) && _uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Cancel Upload"))
{
@@ -112,7 +112,7 @@ internal sealed partial class CharaDataHubUi
UiSharedService.DrawGrouped(() =>
{
ImGui.AlignTextToFramePadding();
UiSharedService.ColorTextWrapped($"You have {otherUpdates} other entries with unsaved changes.", ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped($"You have {otherUpdates} other entries with unsaved changes.", UIColors.Get("LightlessYellow"));
ImGui.SameLine();
using (ImRaii.Disabled(_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted))
{
@@ -241,7 +241,7 @@ internal sealed partial class CharaDataHubUi
ImGui.SameLine(pos);
if (!dataDto.HasMissingFiles)
{
UiSharedService.ColorTextWrapped("All files to download this character data are present on the server", ImGuiColors.HealerGreen);
UiSharedService.ColorTextWrapped("All files to download this character data are present on the server", UIColors.Get("LightlessBlue"));
}
else
{
@@ -259,7 +259,7 @@ internal sealed partial class CharaDataHubUi
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(20, 1);
ImGui.SameLine();
UiSharedService.ColorTextWrapped("New data was set. It may contain files that require to be uploaded (will happen on Saving to server)", ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped("New data was set. It may contain files that require to be uploaded (will happen on Saving to server)", UIColors.Get("LightlessYellow"));
}
ImGui.TextUnformatted("Contains Manipulation Data");
@@ -414,7 +414,7 @@ internal sealed partial class CharaDataHubUi
}
}
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, poseCount == maxPoses))
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"), poseCount == maxPoses))
ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached");
ImGuiHelpers.ScaledDummy(5);
@@ -424,7 +424,7 @@ internal sealed partial class CharaDataHubUi
if (!_uiSharedService.IsInGpose && _charaDataManager.BrioAvailable)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data you need to be in GPose.", ImGuiColors.DalamudYellow);
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data you need to be in GPose.", UIColors.Get("LightlessYellow"));
ImGuiHelpers.ScaledDummy(5);
}
else if (!_charaDataManager.BrioAvailable)
@@ -443,7 +443,7 @@ internal sealed partial class CharaDataHubUi
if (pose.Id == null)
{
UiSharedService.ScaledSameLine(50);
_uiSharedService.IconText(FontAwesomeIcon.Plus, ImGuiColors.DalamudYellow);
_uiSharedService.IconText(FontAwesomeIcon.Plus, UIColors.Get("LightlessYellow"));
UiSharedService.AttachToolTip("This pose has not been added to the server yet. Save changes to upload this Pose data.");
}
@@ -451,14 +451,14 @@ internal sealed partial class CharaDataHubUi
if (poseHasChanges)
{
UiSharedService.ScaledSameLine(50);
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudYellow);
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
UiSharedService.AttachToolTip("This pose has changes that have not been saved to the server yet.");
}
UiSharedService.ScaledSameLine(75);
if (pose.Description == null && pose.WorldData == null && pose.PoseData == null)
{
UiSharedService.ColorText("Pose scheduled for deletion", ImGuiColors.DalamudYellow);
UiSharedService.ColorText("Pose scheduled for deletion", UIColors.Get("LightlessYellow"));
}
else
{
@@ -669,7 +669,7 @@ internal sealed partial class CharaDataHubUi
var idText = entry.FullId;
if (uDto?.HasChanges ?? false)
{
UiSharedService.ColorText(idText, ImGuiColors.DalamudYellow);
UiSharedService.ColorText(idText, UIColors.Get("LightlessYellow"));
UiSharedService.AttachToolTip("This entry has unsaved changes");
}
else
@@ -724,7 +724,7 @@ internal sealed partial class CharaDataHubUi
FontAwesomeIcon eIcon = FontAwesomeIcon.None;
if (!Equals(DateTime.MaxValue, entry.ExpiryDate))
eIcon = FontAwesomeIcon.Clock;
_uiSharedService.IconText(eIcon, ImGuiColors.DalamudYellow);
_uiSharedService.IconText(eIcon, UIColors.Get("LightlessYellow"));
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
if (eIcon != FontAwesomeIcon.None)
{
@@ -759,17 +759,17 @@ internal sealed partial class CharaDataHubUi
if (_charaDataManager.OwnCharaData.Count == _charaDataManager.MaxCreatableCharaData)
{
ImGui.AlignTextToFramePadding();
UiSharedService.ColorTextWrapped("You have reached the maximum Character Data entries and cannot create more.", ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped("You have reached the maximum Character Data entries and cannot create more.", UIColors.Get("LightlessYellow"));
}
}
if (_charaDataManager.DataCreationTask != null && !_charaDataManager.DataCreationTask.IsCompleted)
{
UiSharedService.ColorTextWrapped("Creating new character data entry on server...", ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped("Creating new character data entry on server...", UIColors.Get("LightlessYellow"));
}
else if (_charaDataManager.DataCreationTask != null && _charaDataManager.DataCreationTask.IsCompleted)
{
var color = _charaDataManager.DataCreationTask.Result.Success ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed;
var color = _charaDataManager.DataCreationTask.Result.Success ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudRed;
UiSharedService.ColorTextWrapped(_charaDataManager.DataCreationTask.Result.Output, color);
}

View File

@@ -1,8 +1,8 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Utility;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using System.Numerics;
namespace LightlessSync.UI;
@@ -78,7 +78,7 @@ internal partial class CharaDataHubUi
if (!_uiSharedService.IsInGpose)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Spawning and applying pose data is only available in GPose.", ImGuiColors.DalamudYellow);
UiSharedService.DrawGroupedCenteredColorText("Spawning and applying pose data is only available in GPose.", UIColors.Get("LightlessYellow"));
ImGuiHelpers.ScaledDummy(5);
}
@@ -93,7 +93,7 @@ internal partial class CharaDataHubUi
using var indent = ImRaii.PushIndent(5f);
if (_charaDataNearbyManager.NearbyData.Count == 0)
{
UiSharedService.DrawGroupedCenteredColorText("No Shared World Poses found nearby.", ImGuiColors.DalamudYellow);
UiSharedService.DrawGroupedCenteredColorText("No Shared World Poses found nearby.", UIColors.Get("LightlessYellow"));
}
bool wasAnythingHovered = false;
@@ -112,7 +112,7 @@ internal partial class CharaDataHubUi
var noteText = pose.Key.MetaInfo.IsOwnData ? "YOU" : (userNote == null ? pose.Key.MetaInfo.Uploader.AliasOrUID : $"{userNote} ({pose.Key.MetaInfo.Uploader.AliasOrUID})");
ImGui.TextUnformatted("Pose by");
ImGui.SameLine();
UiSharedService.ColorText(noteText, ImGuiColors.ParsedGreen);
UiSharedService.ColorText(noteText, UIColors.Get("LightlessBlue"));
using (ImRaii.Group())
{
UiSharedService.ColorText("Character Data Description", ImGuiColors.DalamudGrey);
@@ -176,7 +176,7 @@ internal partial class CharaDataHubUi
Vector2 coneBase2 = circleCenter + baseDir2 * circleRadius;
// Draw the cone as a filled triangle
drawList.AddTriangleFilled(circleCenter, coneBase1, coneBase2, UiSharedService.Color(ImGuiColors.ParsedGreen));
drawList.AddTriangleFilled(circleCenter, coneBase1, coneBase2, UiSharedService.Color(UIColors.Get("LightlessBlue")));
drawList.AddCircle(circleCenter, circleDiameter / 2, UiSharedService.Color(ImGuiColors.DalamudWhite), 360, 2);
var distance = pose.Value.Distance.ToString("0.0") + "y";
var textSize = ImGui.CalcTextSize(distance);

View File

@@ -190,7 +190,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
}
if (!string.IsNullOrEmpty(_charaDataManager.DataApplicationProgress))
{
UiSharedService.ColorTextWrapped(_charaDataManager.DataApplicationProgress, ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped(_charaDataManager.DataApplicationProgress, UIColors.Get("LightlessYellow"));
}
if (_charaDataManager.DataApplicationTask != null)
{
@@ -383,7 +383,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
UiSharedService.AttachToolTip($"Target the GPose Character {CharaName(actor.Name.TextValue)}");
ImGui.AlignTextToFramePadding();
var pos = ImGui.GetCursorPosX();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen, actor.Address == (_dalamudUtilService.GetGposeTargetGameObjectAsync().GetAwaiter().GetResult()?.Address ?? nint.Zero)))
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessBlue"), actor.Address == (_dalamudUtilService.GetGposeTargetGameObjectAsync().GetAwaiter().GetResult()?.Address ?? nint.Zero)))
{
ImGui.TextUnformatted(CharaName(actor.Name.TextValue));
}
@@ -436,7 +436,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
if (!_hasValidGposeTarget)
{
ImGuiHelpers.ScaledDummy(3);
UiSharedService.DrawGroupedCenteredColorText("Applying data is only available in GPose with a valid selected GPose target.", ImGuiColors.DalamudYellow, 350);
UiSharedService.DrawGroupedCenteredColorText("Applying data is only available in GPose with a valid selected GPose target.", UIColors.Get("LightlessYellow"), 350);
}
ImGuiHelpers.ScaledDummy(10);
@@ -595,7 +595,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
if (_configService.Current.FavoriteCodes.Count == 0)
{
UiSharedService.ColorTextWrapped("You have no favorites added. Add Favorites through the other tabs before you can use this tab.", ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped("You have no favorites added. Add Favorites through the other tabs before you can use this tab.", UIColors.Get("LightlessYellow"));
}
}
}
@@ -644,7 +644,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
ImGui.NewLine();
if (!_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false)
{
UiSharedService.ColorTextWrapped("Downloading meta info. Please wait.", ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped("Downloading meta info. Please wait.", UIColors.Get("LightlessYellow"));
}
if ((_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false) && !_charaDataManager.DownloadMetaInfoTask.Result.Success)
{
@@ -850,12 +850,12 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
UiSharedService.ColorTextWrapped("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.",
ImGuiColors.DalamudRed);
UiSharedService.ColorTextWrapped("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. " +
"If you received it from someone else have them do the same.", ImGuiColors.DalamudYellow);
"If you received it from someone else have them do the same.", UIColors.Get("LightlessYellow"));
}
}
else
{
UiSharedService.ColorTextWrapped("Loading Character...", ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped("Loading Character...", UIColors.Get("LightlessYellow"));
}
}
}
@@ -896,7 +896,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
}, Directory.Exists(_configService.Current.LastSavedCharaDataLocation) ? _configService.Current.LastSavedCharaDataLocation : null);
}
UiSharedService.ColorTextWrapped("Note: For best results make sure you have everything you want to be shared as well as the correct character appearance" +
" equipped and redraw your character before exporting.", ImGuiColors.DalamudYellow);
" equipped and redraw your character before exporting.", UIColors.Get("LightlessYellow"));
ImGui.Unindent();
}

View File

@@ -1,9 +1,9 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.Interop.Ipc;
@@ -15,6 +15,7 @@ using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Components;
using LightlessSync.UI.Handlers;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using LightlessSync.WebAPI.Files;
using LightlessSync.WebAPI.Files.Models;
@@ -23,6 +24,7 @@ using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Reflection;
@@ -30,35 +32,61 @@ namespace LightlessSync.UI;
public class CompactUi : WindowMediatorSubscriberBase
{
private readonly CharacterAnalyzer _characterAnalyzer;
private readonly ApiController _apiController;
private readonly LightlessConfigService _configService;
private readonly LightlessMediator _lightlessMediator;
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private readonly DrawEntityFactory _drawEntityFactory;
private readonly FileUploadManager _fileTransferManager;
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
private readonly PairManager _pairManager;
private readonly SelectTagForPairUi _selectGroupForPairUi;
private readonly SelectTagForPairUi _selectTagForPairUi;
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
private readonly RenameSyncshellTagUi _renameSyncshellTagUi;
private readonly SelectPairForTagUi _selectPairsForGroupUi;
private readonly RenamePairTagUi _renamePairTagUi;
private readonly IpcManager _ipcManager;
private readonly ServerConfigurationManager _serverManager;
private readonly TopTabMenu _tabMenu;
private readonly TagHandler _tagHandler;
private readonly UiSharedService _uiSharedService;
private readonly BroadcastService _broadcastService;
private List<IDrawFolder> _drawFolders;
private Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>? _cachedAnalysis;
private Pair? _lastAddedUser;
private string _lastAddedUserComment = string.Empty;
private Vector2 _lastPosition = Vector2.One;
private Vector2 _lastSize = Vector2.One;
private int _secretKeyIdx = -1;
private bool _showModalForUserAddition;
private float _transferPartHeight;
private bool _wasOpen;
private float _windowContentWidth;
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, LightlessConfigService configService, ApiController apiController, PairManager pairManager,
ServerConfigurationManager serverManager, LightlessMediator mediator, FileUploadManager fileTransferManager,
TagHandler tagHandler, DrawEntityFactory drawEntityFactory, SelectTagForPairUi selectTagForPairUi, SelectPairForTagUi selectPairForTagUi,
PerformanceCollectorService performanceCollectorService, IpcManager ipcManager)
: base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
public CompactUi(
ILogger<CompactUi> logger,
UiSharedService uiShared,
LightlessConfigService configService,
ApiController apiController,
PairManager pairManager,
ServerConfigurationManager serverManager,
LightlessMediator mediator,
FileUploadManager fileTransferManager,
TagHandler tagHandler,
DrawEntityFactory drawEntityFactory,
SelectTagForPairUi selectTagForPairUi,
SelectPairForTagUi selectPairForTagUi,
RenamePairTagUi renameTagUi,
SelectTagForSyncshellUi selectTagForSyncshellUi,
SelectSyncshellForTagUi selectSyncshellForTagUi,
RenameSyncshellTagUi renameSyncshellTagUi,
PerformanceCollectorService performanceCollectorService,
IpcManager ipcManager,
BroadcastService broadcastService,
CharacterAnalyzer characterAnalyzer,
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
{
_uiSharedService = uiShared;
_configService = configService;
@@ -68,12 +96,17 @@ public class CompactUi : WindowMediatorSubscriberBase
_fileTransferManager = fileTransferManager;
_tagHandler = tagHandler;
_drawEntityFactory = drawEntityFactory;
_selectGroupForPairUi = selectTagForPairUi;
_selectTagForPairUi = selectTagForPairUi;
_selectTagForSyncshellUi = selectTagForSyncshellUi;
_selectSyncshellForTagUi = selectSyncshellForTagUi;
_renameSyncshellTagUi = renameSyncshellTagUi;
_selectPairsForGroupUi = selectPairForTagUi;
_renamePairTagUi = renameTagUi;
_ipcManager = ipcManager;
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService);
_broadcastService = broadcastService;
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService);
AllowPinning = false;
AllowPinning = true;
AllowClickthrough = false;
TitleBarButtons = new()
{
@@ -106,10 +139,10 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGui.Text("Open Lightless Event Viewer");
ImGui.EndTooltip();
}
}
},
};
_drawFolders = GetDrawFolders().ToList();
_drawFolders = [.. DrawFolders];
#if DEBUG
string dev = "Dev Build";
@@ -126,7 +159,7 @@ public class CompactUi : WindowMediatorSubscriberBase
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = GetDrawFolders().ToList());
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = DrawFolders.ToList());
Flags |= ImGuiWindowFlags.NoDocking;
@@ -135,6 +168,9 @@ public class CompactUi : WindowMediatorSubscriberBase
MinimumSize = new Vector2(375, 400),
MaximumSize = new Vector2(375, 2000),
};
_characterAnalyzer = characterAnalyzer;
_playerPerformanceConfig = playerPerformanceConfig;
_lightlessMediator = mediator;
}
protected override void DrawInternal()
@@ -149,10 +185,10 @@ public class CompactUi : WindowMediatorSubscriberBase
var uidTextSize = ImGui.CalcTextSize(unsupported);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 - uidTextSize.X / 2);
ImGui.AlignTextToFramePadding();
ImGui.TextColored(ImGuiColors.DalamudRed, unsupported);
ImGui.TextColored(UIColors.Get("DimRed"), unsupported);
}
UiSharedService.ColorTextWrapped($"Your Lightless Sync installation is out of date, the current version is {ver.Major}.{ver.Minor}.{ver.Build}. " +
$"It is highly recommended to keep Lightless Sync up to date. Open /xlplugins and update the plugin.", ImGuiColors.DalamudRed);
$"It is highly recommended to keep Lightless Sync up to date. Open /xlplugins and update the plugin.", UIColors.Get("DimRed"));
}
if (!_ipcManager.Initialized)
@@ -164,12 +200,12 @@ public class CompactUi : WindowMediatorSubscriberBase
var uidTextSize = ImGui.CalcTextSize(unsupported);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 - uidTextSize.X / 2);
ImGui.AlignTextToFramePadding();
ImGui.TextColored(ImGuiColors.DalamudRed, unsupported);
ImGui.TextColored(UIColors.Get("DimRed"), unsupported);
}
var penumAvailable = _ipcManager.Penumbra.APIAvailable;
var glamAvailable = _ipcManager.Glamourer.APIAvailable;
UiSharedService.ColorTextWrapped($"One or more Plugins essential for Lightless operation are unavailable. Enable or update following plugins:", ImGuiColors.DalamudRed);
UiSharedService.ColorTextWrapped($"One or more Plugins essential for Lightless operation are unavailable. Enable or update following plugins:", UIColors.Get("DimRed"));
using var indent = ImRaii.PushIndent(10f);
if (!penumAvailable)
{
@@ -185,7 +221,7 @@ public class CompactUi : WindowMediatorSubscriberBase
}
using (ImRaii.PushId("header")) DrawUIDHeader();
ImGui.Separator();
_uiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f);
using (ImRaii.PushId("serverstatus")) DrawServerStatus();
ImGui.Separator();
@@ -197,8 +233,12 @@ public class CompactUi : WindowMediatorSubscriberBase
float pairlistEnd = ImGui.GetCursorPosY();
using (ImRaii.PushId("transfers")) DrawTransfers();
_transferPartHeight = ImGui.GetCursorPosY() - pairlistEnd - ImGui.GetTextLineHeight();
using (ImRaii.PushId("group-user-popup")) _selectPairsForGroupUi.Draw(_pairManager.DirectPairs);
using (ImRaii.PushId("grouping-popup")) _selectGroupForPairUi.Draw();
using (ImRaii.PushId("group-pair-popup")) _selectPairsForGroupUi.Draw(_pairManager.DirectPairs);
using (ImRaii.PushId("group-syncshell-popup")) _selectSyncshellForTagUi.Draw([.. _pairManager.Groups.Values]);
using (ImRaii.PushId("group-pair-edit")) _renamePairTagUi.Draw();
using (ImRaii.PushId("group-syncshell-edit")) _renameSyncshellTagUi.Draw();
using (ImRaii.PushId("grouping-pair-popup")) _selectTagForPairUi.Draw();
using (ImRaii.PushId("grouping-syncshell-popup")) _selectTagForSyncshellUi.Draw();
}
if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null)
@@ -276,7 +316,7 @@ public class CompactUi : WindowMediatorSubscriberBase
{
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2);
if (!printShard) ImGui.AlignTextToFramePadding();
ImGui.TextColored(ImGuiColors.ParsedGreen, userCount);
ImGui.TextColored(UIColors.Get("LightlessPurple"), userCount);
ImGui.SameLine();
if (!printShard) ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Users Online");
@@ -284,7 +324,7 @@ public class CompactUi : WindowMediatorSubscriberBase
else
{
ImGui.AlignTextToFramePadding();
ImGui.TextColored(ImGuiColors.DalamudRed, "Not connected to any server");
ImGui.TextColored(UIColors.Get("DimRed"), "Not connected to any server");
}
if (printShard)
@@ -362,7 +402,7 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGui.TextUnformatted("No uploads in progress");
}
var currentDownloads = _currentDownloads.SelectMany(d => d.Value.Values).ToList();
var currentDownloads = BuildCurrentDownloadSnapshot();
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(FontAwesomeIcon.Download);
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
@@ -389,35 +429,250 @@ public class CompactUi : WindowMediatorSubscriberBase
}
}
private List<FileDownloadStatus> BuildCurrentDownloadSnapshot()
{
List<FileDownloadStatus> snapshot = new();
foreach (var kvp in _currentDownloads.ToArray())
{
var value = kvp.Value;
if (value == null || value.Count == 0)
continue;
try
{
snapshot.AddRange(value.Values.ToArray());
}
catch (System.ArgumentException)
{
// skibidi
}
}
return snapshot;
}
private void DrawUIDHeader()
{
var uidText = GetUidText();
Vector4? vanityTextColor = null;
Vector4? vanityGlowColor = null;
bool useVanityColors = false;
if (_configService.Current.useColoredUIDs && _apiController.HasVanity)
{
if (!string.IsNullOrWhiteSpace(_apiController.TextColorHex))
{
vanityTextColor = UIColors.HexToRgba(_apiController.TextColorHex);
}
if (!string.IsNullOrWhiteSpace(_apiController.TextGlowColorHex))
{
vanityGlowColor = UIColors.HexToRgba(_apiController.TextGlowColorHex);
}
useVanityColors = vanityTextColor is not null || vanityGlowColor is not null;
}
//Getting information of character and triangles threshold to show overlimit status in UID bar.
_cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone();
Vector2 uidTextSize, iconSize;
using (_uiSharedService.UidFont.Push())
uidTextSize = ImGui.CalcTextSize(uidText);
using (_uiSharedService.IconFont.Push())
iconSize = ImGui.CalcTextSize(FontAwesomeIcon.PersonCirclePlus.ToIconString());
float contentWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
float uidStartX = (contentWidth - uidTextSize.X) / 2f;
float cursorY = ImGui.GetCursorPosY();
if (_configService.Current.BroadcastEnabled && _apiController.IsConnected)
{
float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f;
var buttonSize = new Vector2(iconSize.X, uidTextSize.Y);
ImGui.SetCursorPos(new Vector2(ImGui.GetStyle().ItemSpacing.X + 5f, cursorY));
ImGui.InvisibleButton("BroadcastIcon", buttonSize);
var iconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset);
using (_uiSharedService.IconFont.Push())
ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.PersonCirclePlus.ToIconString());
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("PairBlue"));
ImGui.Text("Lightfinder");
ImGui.PopStyleColor();
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.TextWrapped("Use Lightfinder when you're okay with being visible to other users and understand that you are responsible for your own experience.");
ImGui.PopStyleColor();
ImGuiHelpers.ScaledDummy(0.2f);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
if (_configService.Current.BroadcastEnabled)
{
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGreen"));
ImGui.Text("The Lightfinder calls, and somewhere, a soul may answer."); // cringe..
ImGui.PopStyleColor();
var ttl = _broadcastService.RemainingTtl;
if (ttl is { } remaining && remaining > TimeSpan.Zero)
{
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
ImGui.Text($"Still shining, for {remaining:hh\\:mm\\:ss}");
ImGui.PopStyleColor();
}
else
{
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.Text("The Lightfinder's light wanes, but not in vain."); // cringe..
ImGui.PopStyleColor();
}
}
else
{
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.Text("The Lightfinder rests, waiting to shine again."); // cringe..
ImGui.PopStyleColor();
}
var cooldown = _broadcastService.RemainingCooldown;
if (cooldown is { } cd)
{
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.Text($"The Lightfinder gathers its strength... ({Math.Ceiling(cd.TotalSeconds)}s)");
ImGui.PopStyleColor();
}
ImGui.EndTooltip();
}
if (ImGui.IsItemClicked())
_lightlessMediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
}
ImGui.SetCursorPosY(cursorY);
ImGui.SetCursorPosX(uidStartX);
bool headerItemClicked;
using (_uiSharedService.UidFont.Push())
{
var uidTextSize = ImGui.CalcTextSize(uidText);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (uidTextSize.X / 2));
ImGui.TextColored(GetUidColor(), uidText);
if (useVanityColors)
{
var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor);
var cursorPos = ImGui.GetCursorScreenPos();
var fontPtr = ImGui.GetFont();
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr);
}
else
{
ImGui.TextColored(GetUidColor(), uidText);
}
}
headerItemClicked = ImGui.IsItemClicked();
if (headerItemClicked)
{
ImGui.SetClipboardText(uidText);
}
UiSharedService.AttachToolTip("Click to copy");
if (_cachedAnalysis != null && _apiController.ServerState is ServerState.Connected)
{
var firstEntry = _cachedAnalysis.FirstOrDefault();
var valueDict = firstEntry.Value;
if (valueDict != null && valueDict.Count > 0)
{
var groupedfiles = valueDict
.Select(v => v.Value)
.Where(v => v != null)
.GroupBy(f => f.FileType, StringComparer.Ordinal)
.OrderBy(k => k.Key, StringComparer.Ordinal)
.ToList();
var actualTriCount = valueDict
.Select(v => v.Value)
.Where(v => v != null)
.Sum(f => f.Triangles);
if (groupedfiles != null)
{
//Checking of VRAM threshhold
var texGroup = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
var actualVramUsage = texGroup != null ? texGroup.Sum(f => f.OriginalSize) : 0L;
var isOverVRAMUsage = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < actualVramUsage;
var isOverTriHold = actualTriCount > (_playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000);
if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds)
{
ImGui.SameLine();
ImGui.SetCursorPosY(cursorY + 15f);
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
string warningMessage = "";
if (isOverTriHold)
{
warningMessage += $"You exceed your own triangles threshold by " +
$"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles.";
warningMessage += Environment.NewLine;
}
if (isOverVRAMUsage)
{
warningMessage += $"You exceed your own VRAM threshold by " +
$"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}.";
}
UiSharedService.AttachToolTip(warningMessage);
if (ImGui.IsItemClicked())
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
}
}
}
}
}
if (_apiController.ServerState is ServerState.Connected)
{
if (ImGui.IsItemClicked())
if (headerItemClicked)
{
ImGui.SetClipboardText(_apiController.DisplayName);
}
UiSharedService.AttachToolTip("Click to copy");
if (!string.Equals(_apiController.DisplayName, _apiController.UID, StringComparison.Ordinal))
{
var origTextSize = ImGui.CalcTextSize(_apiController.UID);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (origTextSize.X / 2));
ImGui.TextColored(GetUidColor(), _apiController.UID);
if (ImGui.IsItemClicked())
if (useVanityColors)
{
ImGui.SetClipboardText(_apiController.UID);
var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.UID, vanityTextColor, vanityGlowColor);
var cursorPos = ImGui.GetCursorScreenPos();
var fontPtr = ImGui.GetFont();
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr);
}
else
{
ImGui.TextColored(GetUidColor(), _apiController.UID);
}
bool uidFooterClicked = ImGui.IsItemClicked();
UiSharedService.AttachToolTip("Click to copy");
if (uidFooterClicked)
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
}
}
}
else
@@ -426,138 +681,164 @@ public class CompactUi : WindowMediatorSubscriberBase
}
}
private IEnumerable<IDrawFolder> GetDrawFolders()
private IEnumerable<IDrawFolder> DrawFolders
{
List<IDrawFolder> drawFolders = [];
get
{
var drawFolders = new List<IDrawFolder>();
var filter = _tabMenu.Filter;
var allPairs = _pairManager.PairsWithGroups
.ToDictionary(k => k.Key, k => k.Value);
var filteredPairs = allPairs
.Where(p =>
var allPairs = _pairManager.PairsWithGroups.ToDictionary(k => k.Key, k => k.Value);
var filteredPairs = allPairs.Where(p => PassesFilter(p.Key, filter)).ToDictionary(k => k.Key, k => k.Value);
//Filter of online/visible pairs
if (_configService.Current.ShowVisibleUsersSeparately)
{
if (_tabMenu.Filter.IsNullOrEmpty()) return true;
return p.Key.UserData.AliasOrUID.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ||
(p.Key.GetNote()?.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ?? false) ||
(p.Key.PlayerName?.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ?? false);
})
.ToDictionary(k => k.Key, k => k.Value);
var allVisiblePairs = ImmutablePairList(allPairs.Where(p => FilterVisibleUsers(p.Key)));
var filteredVisiblePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterVisibleUsers(p.Key)));
string? AlphabeticalSort(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(u.Key.PlayerName)
? (_configService.Current.PreferNotesOverNamesForVisible ? u.Key.GetNote() : u.Key.PlayerName)
: (u.Key.GetNote() ?? u.Key.UserData.AliasOrUID));
bool FilterOnlineOrPausedSelf(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> (u.Key.IsOnline || (!u.Key.IsOnline && !_configService.Current.ShowOfflineUsersSeparately)
|| u.Key.UserPair.OwnPermissions.IsPaused());
Dictionary<Pair, List<GroupFullInfoDto>> BasicSortedDictionary(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> u)
=> u.OrderByDescending(u => u.Key.IsVisible)
.ThenByDescending(u => u.Key.IsOnline)
.ThenBy(AlphabeticalSort, StringComparer.OrdinalIgnoreCase)
.ToDictionary(u => u.Key, u => u.Value);
ImmutableList<Pair> ImmutablePairList(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> u)
=> u.Select(k => k.Key).ToImmutableList();
bool FilterVisibleUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> u.Key.IsVisible
&& (_configService.Current.ShowSyncshellUsersInVisible || !(!_configService.Current.ShowSyncshellUsersInVisible && !u.Key.IsDirectlyPaired));
bool FilterTagusers(KeyValuePair<Pair, List<GroupFullInfoDto>> u, string tag)
=> u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && _tagHandler.HasTag(u.Key.UserData.UID, tag);
bool FilterGroupUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u, GroupFullInfoDto group)
=> u.Value.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal));
bool FilterNotTaggedUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && !_tagHandler.HasAnyTag(u.Key.UserData.UID);
bool FilterOfflineUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> ((u.Key.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately)
|| !_configService.Current.ShowSyncshellOfflineUsersSeparately)
&& (!u.Key.IsOneSidedPair || u.Value.Any()) && !u.Key.IsOnline && !u.Key.UserPair.OwnPermissions.IsPaused();
bool FilterOfflineSyncshellUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
=> (!u.Key.IsDirectlyPaired && !u.Key.IsOnline && !u.Key.UserPair.OwnPermissions.IsPaused());
if (_configService.Current.ShowVisibleUsersSeparately)
{
var allVisiblePairs = ImmutablePairList(allPairs
.Where(FilterVisibleUsers));
var filteredVisiblePairs = BasicSortedDictionary(filteredPairs
.Where(FilterVisibleUsers));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs));
}
List<IDrawFolder> groupFolders = new();
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
{
var allGroupPairs = ImmutablePairList(allPairs
.Where(u => FilterGroupUsers(u, group)));
var filteredGroupPairs = filteredPairs
.Where(u => FilterGroupUsers(u, group) && FilterOnlineOrPausedSelf(u))
.OrderByDescending(u => u.Key.IsOnline)
.ThenBy(u =>
{
if (string.Equals(u.Key.UserData.UID, group.OwnerUID, StringComparison.Ordinal)) return 0;
if (group.GroupPairUserInfos.TryGetValue(u.Key.UserData.UID, out var info))
{
if (info.IsModerator()) return 1;
if (info.IsPinned()) return 2;
}
return u.Key.IsVisible ? 3 : 4;
})
.ThenBy(AlphabeticalSort, StringComparer.OrdinalIgnoreCase)
.ToDictionary(k => k.Key, k => k.Value);
groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs));
}
if (_configService.Current.GroupUpSyncshells)
drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler, _uiSharedService));
else
drawFolders.AddRange(groupFolders);
var tags = _tagHandler.GetAllTagsSorted();
foreach (var tag in tags)
{
var allTagPairs = ImmutablePairList(allPairs
.Where(u => FilterTagusers(u, tag)));
var filteredTagPairs = BasicSortedDictionary(filteredPairs
.Where(u => FilterTagusers(u, tag) && FilterOnlineOrPausedSelf(u)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(tag, filteredTagPairs, allTagPairs));
}
var allOnlineNotTaggedPairs = ImmutablePairList(allPairs
.Where(FilterNotTaggedUsers));
var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs
.Where(u => FilterNotTaggedUsers(u) && FilterOnlineOrPausedSelf(u)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag),
onlineNotTaggedPairs, allOnlineNotTaggedPairs));
if (_configService.Current.ShowOfflineUsersSeparately)
{
var allOfflinePairs = ImmutablePairList(allPairs
.Where(FilterOfflineUsers));
var filteredOfflinePairs = BasicSortedDictionary(filteredPairs
.Where(FilterOfflineUsers));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs));
if (_configService.Current.ShowSyncshellOfflineUsersSeparately)
{
var allOfflineSyncshellUsers = ImmutablePairList(allPairs
.Where(FilterOfflineSyncshellUsers));
var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs
.Where(FilterOfflineSyncshellUsers));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag,
filteredOfflineSyncshellUsers,
allOfflineSyncshellUsers));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs));
}
//Filter of not foldered syncshells
var groupFolders = new List<IDrawFolder>();
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
{
GetGroups(allPairs, filteredPairs, group, out ImmutableList<Pair> allGroupPairs, out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
if (FilterNotTaggedSyncshells(group))
{
groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs));
}
}
//Filter of grouped up syncshells (All Syncshells Folder)
if (_configService.Current.GroupUpSyncshells)
drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler, _uiSharedService,
_selectSyncshellForTagUi, _renameSyncshellTagUi, ""));
else
drawFolders.AddRange(groupFolders);
//Filter of grouped/foldered pairs
foreach (var tag in _tagHandler.GetAllPairTagsSorted())
{
var allTagPairs = ImmutablePairList(allPairs.Where(p => FilterTagUsers(p.Key, tag)));
var filteredTagPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterTagUsers(p.Key, tag) && FilterOnlineOrPausedSelf(p.Key)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(tag, filteredTagPairs, allTagPairs));
}
//Filter of grouped/foldered syncshells
foreach (var syncshellTag in _tagHandler.GetAllSyncshellTagsSorted())
{
var syncshellFolderTags = new List<IDrawFolder>();
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
{
if (_tagHandler.HasSyncshellTag(group.GID, syncshellTag))
{
GetGroups(allPairs, filteredPairs, group,
out ImmutableList<Pair> allGroupPairs,
out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
syncshellFolderTags.Add(_drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs));
}
}
drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshellTag));
}
//Filter of not grouped/foldered and offline pairs
var allOnlineNotTaggedPairs = ImmutablePairList(allPairs.Where(p => FilterNotTaggedUsers(p.Key)));
var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterNotTaggedUsers(p.Key) && FilterOnlineOrPausedSelf(p.Key)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag), onlineNotTaggedPairs, allOnlineNotTaggedPairs));
if (_configService.Current.ShowOfflineUsersSeparately)
{
var allOfflinePairs = ImmutablePairList(allPairs.Where(p => FilterOfflineUsers(p.Key, p.Value)));
var filteredOfflinePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineUsers(p.Key, p.Value)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs));
if (_configService.Current.ShowSyncshellOfflineUsersSeparately)
{
var allOfflineSyncshellUsers = ImmutablePairList(allPairs.Where(p => FilterOfflineSyncshellUsers(p.Key)));
var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineSyncshellUsers(p.Key)));
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag, filteredOfflineSyncshellUsers, allOfflineSyncshellUsers));
}
}
//Unpaired
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag,
BasicSortedDictionary(filteredPairs.Where(p => p.Key.IsOneSidedPair)),
ImmutablePairList(allPairs.Where(p => p.Key.IsOneSidedPair))));
return drawFolders;
}
}
private static bool PassesFilter(Pair pair, string filter)
{
if (string.IsNullOrEmpty(filter)) return true;
return pair.UserData.AliasOrUID.Contains(filter, StringComparison.OrdinalIgnoreCase) || (pair.GetNote()?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false) || (pair.PlayerName?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false);
}
private string AlphabeticalSortKey(Pair pair)
{
if (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(pair.PlayerName))
{
return _configService.Current.PreferNotesOverNamesForVisible ? (pair.GetNote() ?? string.Empty) : pair.PlayerName;
}
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag,
BasicSortedDictionary(filteredPairs.Where(u => u.Key.IsOneSidedPair)),
ImmutablePairList(allPairs.Where(u => u.Key.IsOneSidedPair))));
return pair.GetNote() ?? pair.UserData.AliasOrUID;
}
return drawFolders;
private bool FilterOnlineOrPausedSelf(Pair pair) => pair.IsOnline || (!pair.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) || pair.UserPair.OwnPermissions.IsPaused();
private bool FilterVisibleUsers(Pair pair) => pair.IsVisible && (_configService.Current.ShowSyncshellUsersInVisible || pair.IsDirectlyPaired);
private bool FilterTagUsers(Pair pair, string tag) => pair.IsDirectlyPaired && !pair.IsOneSidedPair && _tagHandler.HasPairTag(pair.UserData.UID, tag);
private static bool FilterGroupUsers(List<GroupFullInfoDto> groups, GroupFullInfoDto group) => groups.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal));
private bool FilterNotTaggedUsers(Pair pair) => pair.IsDirectlyPaired && !pair.IsOneSidedPair && !_tagHandler.HasAnyPairTag(pair.UserData.UID);
private bool FilterNotTaggedSyncshells(GroupFullInfoDto group) => !_tagHandler.HasAnySyncshellTag(group.GID) || _configService.Current.ShowGroupedSyncshellsInAll;
private bool FilterOfflineUsers(Pair pair, List<GroupFullInfoDto> groups) => ((pair.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately) || !_configService.Current.ShowSyncshellOfflineUsersSeparately) && (!pair.IsOneSidedPair || groups.Count != 0) && !pair.IsOnline && !pair.UserPair.OwnPermissions.IsPaused();
private static bool FilterOfflineSyncshellUsers(Pair pair) => !pair.IsDirectlyPaired && !pair.IsOnline && !pair.UserPair.OwnPermissions.IsPaused();
private Dictionary<Pair, List<GroupFullInfoDto>> BasicSortedDictionary(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> pairs) => pairs.OrderByDescending(u => u.Key.IsVisible).ThenByDescending(u => u.Key.IsOnline).ThenBy(u => AlphabeticalSortKey(u.Key), StringComparer.OrdinalIgnoreCase).ToDictionary(u => u.Key, u => u.Value);
private static ImmutableList<Pair> ImmutablePairList(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> pairs) => [.. pairs.Select(k => k.Key)];
private void GetGroups(Dictionary<Pair, List<GroupFullInfoDto>> allPairs,
Dictionary<Pair, List<GroupFullInfoDto>> filteredPairs,
GroupFullInfoDto group,
out ImmutableList<Pair> allGroupPairs,
out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs)
{
allGroupPairs = ImmutablePairList(allPairs
.Where(u => FilterGroupUsers(u.Value, group)));
filteredGroupPairs = filteredPairs
.Where(u => FilterGroupUsers( u.Value, group) && FilterOnlineOrPausedSelf(u.Key))
.OrderByDescending(u => u.Key.IsOnline)
.ThenBy(u =>
{
if (string.Equals(u.Key.UserData.UID, group.OwnerUID, StringComparison.Ordinal)) return 0;
if (group.GroupPairUserInfos.TryGetValue(u.Key.UserData.UID, out var info))
{
if (info.IsModerator()) return 1;
if (info.IsPinned()) return 2;
}
return u.Key.IsVisible ? 3 : 4;
})
.ThenBy(u => AlphabeticalSortKey(u.Key), StringComparer.OrdinalIgnoreCase)
.ToDictionary(k => k.Key, k => k.Value);
}
private string GetServerError()
@@ -587,21 +868,21 @@ public class CompactUi : WindowMediatorSubscriberBase
{
return _apiController.ServerState switch
{
ServerState.Connecting => ImGuiColors.DalamudYellow,
ServerState.Reconnecting => ImGuiColors.DalamudRed,
ServerState.Connected => ImGuiColors.ParsedGreen,
ServerState.Disconnected => ImGuiColors.DalamudYellow,
ServerState.Disconnecting => ImGuiColors.DalamudYellow,
ServerState.Unauthorized => ImGuiColors.DalamudRed,
ServerState.VersionMisMatch => ImGuiColors.DalamudRed,
ServerState.Offline => ImGuiColors.DalamudRed,
ServerState.RateLimited => ImGuiColors.DalamudYellow,
ServerState.NoSecretKey => ImGuiColors.DalamudYellow,
ServerState.MultiChara => ImGuiColors.DalamudYellow,
ServerState.OAuthMisconfigured => ImGuiColors.DalamudRed,
ServerState.OAuthLoginTokenStale => ImGuiColors.DalamudRed,
ServerState.NoAutoLogon => ImGuiColors.DalamudYellow,
_ => ImGuiColors.DalamudRed
ServerState.Connecting => UIColors.Get("LightlessYellow"),
ServerState.Reconnecting => UIColors.Get("DimRed"),
ServerState.Connected => UIColors.Get("LightlessPurple"),
ServerState.Disconnected => UIColors.Get("LightlessYellow"),
ServerState.Disconnecting => UIColors.Get("LightlessYellow"),
ServerState.Unauthorized => UIColors.Get("DimRed"),
ServerState.VersionMisMatch => UIColors.Get("DimRed"),
ServerState.Offline => UIColors.Get("DimRed"),
ServerState.RateLimited => UIColors.Get("LightlessYellow"),
ServerState.NoSecretKey => UIColors.Get("LightlessYellow"),
ServerState.MultiChara => UIColors.Get("LightlessYellow"),
ServerState.OAuthMisconfigured => UIColors.Get("DimRed"),
ServerState.OAuthLoginTokenStale => UIColors.Get("DimRed"),
ServerState.NoAutoLogon => UIColors.Get("LightlessYellow"),
_ => UIColors.Get("DimRed")
};
}

View File

@@ -19,16 +19,18 @@ public class DrawFolderGroup : DrawFolderBase
private readonly GroupFullInfoDto _groupFullInfoDto;
private readonly IdDisplayHandler _idDisplayHandler;
private readonly LightlessMediator _lightlessMediator;
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
public DrawFolderGroup(string id, GroupFullInfoDto groupFullInfoDto, ApiController apiController,
IImmutableList<DrawUserPair> drawPairs, IImmutableList<Pair> allPairs, TagHandler tagHandler, IdDisplayHandler idDisplayHandler,
LightlessMediator lightlessMediator, UiSharedService uiSharedService) :
LightlessMediator lightlessMediator, UiSharedService uiSharedService, SelectTagForSyncshellUi selectTagForSyncshellUi) :
base(id, drawPairs, allPairs, tagHandler, uiSharedService)
{
_groupFullInfoDto = groupFullInfoDto;
_apiController = apiController;
_idDisplayHandler = idDisplayHandler;
_lightlessMediator = lightlessMediator;
_selectTagForSyncshellUi = selectTagForSyncshellUi;
}
protected override bool RenderIfEmpty => true;
@@ -99,6 +101,13 @@ public class DrawFolderGroup : DrawFolderBase
}
UiSharedService.AttachToolTip("Copies all your notes for all users in this Syncshell to the clipboard." + Environment.NewLine + "They can be imported via Settings -> General -> Notes -> Import notes from clipboard");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Folder, "Syncshell Groups", menuWidth, true))
{
ImGui.CloseCurrentPopup();
_selectTagForSyncshellUi.Open(_groupFullInfoDto);
}
UiSharedService.AttachToolTip("Choose syncshell groups for " + _groupFullInfoDto.GID);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell", menuWidth, true) && UiSharedService.CtrlPressed())
{
_ = _apiController.GroupLeave(_groupFullInfoDto);
@@ -185,7 +194,7 @@ public class DrawFolderGroup : DrawFolderBase
_uiSharedService.IconText(FontAwesomeIcon.UsersCog, (_groupFullInfoDto.GroupPermissions.IsPreferDisableAnimations() != individualAnimDisabled
|| _groupFullInfoDto.GroupPermissions.IsPreferDisableSounds() != individualSoundsDisabled
|| _groupFullInfoDto.GroupPermissions.IsPreferDisableVFX() != individualVFXDisabled) ? ImGuiColors.DalamudYellow : null);
|| _groupFullInfoDto.GroupPermissions.IsPreferDisableVFX() != individualVFXDisabled) ? UIColors.Get("LightlessYellow") : null);
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();

View File

@@ -13,13 +13,15 @@ public class DrawFolderTag : DrawFolderBase
{
private readonly ApiController _apiController;
private readonly SelectPairForTagUi _selectPairForTagUi;
private readonly RenamePairTagUi _renameTagUi;
public DrawFolderTag(string id, IImmutableList<DrawUserPair> drawPairs, IImmutableList<Pair> allPairs,
TagHandler tagHandler, ApiController apiController, SelectPairForTagUi selectPairForTagUi, UiSharedService uiSharedService)
TagHandler tagHandler, ApiController apiController, SelectPairForTagUi selectPairForTagUi, RenamePairTagUi renameTagUi, UiSharedService uiSharedService)
: base(id, drawPairs, allPairs, tagHandler, uiSharedService)
{
_apiController = apiController;
_selectPairForTagUi = selectPairForTagUi;
_renameTagUi = renameTagUi;
}
protected override bool RenderIfEmpty => _id switch
@@ -100,14 +102,17 @@ public class DrawFolderTag : DrawFolderBase
protected override void DrawMenu(float menuWidth)
{
ImGui.TextUnformatted("Group Menu");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Select Pairs", menuWidth, true))
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Select Pairs", menuWidth, isInPopup: true))
{
_selectPairForTagUi.Open(_id);
}
UiSharedService.AttachToolTip("Select Individual Pairs for this Pair Group");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Pair Group", menuWidth, true) && UiSharedService.CtrlPressed())
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Edit, "Rename Pair Group", menuWidth, isInPopup: true))
{
_tagHandler.RemoveTag(_id);
_renameTagUi.Open(_id);
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Pair Group", menuWidth, isInPopup: true) && UiSharedService.CtrlPressed())
{
_tagHandler.RemovePairTag(_id);
}
UiSharedService.AttachToolTip("Hold CTRL to remove this Group permanently." + Environment.NewLine +
"Note: this will not unpair with users in this Group.");

View File

@@ -9,27 +9,37 @@ namespace LightlessSync.UI.Components;
public class DrawGroupedGroupFolder : IDrawFolder
{
private readonly string _tag;
private readonly IEnumerable<IDrawFolder> _groups;
private readonly TagHandler _tagHandler;
private readonly UiSharedService _uiSharedService;
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
private readonly RenameSyncshellTagUi _renameSyncshellTagUi;
private bool _wasHovered = false;
private float _menuWidth;
public IImmutableList<DrawUserPair> DrawPairs => throw new NotSupportedException();
public int OnlinePairs => _groups.SelectMany(g => g.DrawPairs).Where(g => g.Pair.IsOnline).DistinctBy(g => g.Pair.UserData.UID).Count();
public int TotalPairs => _groups.Sum(g => g.TotalPairs);
public DrawGroupedGroupFolder(IEnumerable<IDrawFolder> groups, TagHandler tagHandler, UiSharedService uiSharedService)
public DrawGroupedGroupFolder(IEnumerable<IDrawFolder> groups, TagHandler tagHandler, UiSharedService uiSharedService, SelectSyncshellForTagUi selectSyncshellForTagUi, RenameSyncshellTagUi renameSyncshellTagUi, string tag)
{
_groups = groups;
_tagHandler = tagHandler;
_uiSharedService = uiSharedService;
_selectSyncshellForTagUi = selectSyncshellForTagUi;
_renameSyncshellTagUi = renameSyncshellTagUi;
_tag = tag;
}
public void Draw()
{
if (!_groups.Any()) return;
string _id = "__folder_syncshells";
if (_tag != "")
{
_id = $"__folder_{_tag}";
}
using var id = ImRaii.PushId(_id);
var color = ImRaii.PushColor(ImGuiCol.ChildBg, ImGui.GetColorU32(ImGuiCol.FrameBgHovered), _wasHovered);
using (ImRaii.Child("folder__" + _id, new System.Numerics.Vector2(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX(), ImGui.GetFrameHeight())))
@@ -49,18 +59,36 @@ public class DrawGroupedGroupFolder : IDrawFolder
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(FontAwesomeIcon.UsersRectangle);
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X / 2f }))
if (_tag != "")
{
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("[" + OnlinePairs.ToString() + "]");
_uiSharedService.IconText(FontAwesomeIcon.FolderPlus);
}
else
{
_uiSharedService.IconText(FontAwesomeIcon.UsersRectangle);
}
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X / 2f }))
{
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("[" + OnlinePairs.ToString() + "]");
}
UiSharedService.AttachToolTip(OnlinePairs + " online in all of your joined syncshells" + Environment.NewLine +
TotalPairs + " pairs combined in all of your joined syncshells");
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("All Syncshells");
if (_tag != "")
{
ImGui.TextUnformatted(_tag);
ImGui.SameLine();
DrawMenu();
} else
{
ImGui.TextUnformatted("All Syncshells");
}
}
color.Dispose();
_wasHovered = ImGui.IsItemHovered();
@@ -76,4 +104,40 @@ public class DrawGroupedGroupFolder : IDrawFolder
}
}
}
protected void DrawMenu()
{
var barButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.EllipsisV);
var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
ImGui.SameLine(windowEndX - barButtonSize.X);
if (_uiSharedService.IconButton(FontAwesomeIcon.EllipsisV))
{
ImGui.OpenPopup("User Flyout Menu");
}
if (ImGui.BeginPopup("User Flyout Menu"))
{
using (ImRaii.PushId($"buttons-syncshell-{_tag}")) GroupMenu(_menuWidth);
_menuWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
ImGui.EndPopup();
}
}
protected void GroupMenu(float menuWidth)
{
ImGui.TextUnformatted("Syncshell Group Menu");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Select Syncshells", menuWidth, isInPopup: true))
{
_selectSyncshellForTagUi.Open(_tag);
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Edit, "Rename Syncshell Group", menuWidth, isInPopup: true))
{
_renameSyncshellTagUi.Open(_tag);
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Syncshell Group", menuWidth, isInPopup: true) && UiSharedService.CtrlPressed())
{
_tagHandler.RemoveSyncshellTag(_tag);
}
UiSharedService.AttachToolTip("Hold CTRL to remove this Group permanently.");
}
}

View File

@@ -1,6 +1,5 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Extensions;
@@ -12,6 +11,7 @@ using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Handlers;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
namespace LightlessSync.UI.Components;
@@ -196,13 +196,13 @@ public class DrawUserPair
if (_pair.IsPaused)
{
using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow);
using var _ = ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
_uiSharedService.IconText(FontAwesomeIcon.PauseCircle);
userPairText = _pair.UserData.AliasOrUID + " is paused";
}
else if (!_pair.IsOnline)
{
using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
using var _ = ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed"));
_uiSharedService.IconText(_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.OneSided
? FontAwesomeIcon.ArrowsLeftRight
: (_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional
@@ -211,7 +211,7 @@ public class DrawUserPair
}
else if (_pair.IsVisible)
{
_uiSharedService.IconText(FontAwesomeIcon.Eye, ImGuiColors.ParsedGreen);
_uiSharedService.IconText(FontAwesomeIcon.Eye, UIColors.Get("LightlessBlue"));
userPairText = _pair.UserData.AliasOrUID + " is visible: " + _pair.PlayerName + Environment.NewLine + "Click to target this player";
if (ImGui.IsItemClicked())
{
@@ -220,7 +220,7 @@ public class DrawUserPair
}
else
{
using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen);
using var _ = ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("PairBlue"));
_uiSharedService.IconText(_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional
? FontAwesomeIcon.User : FontAwesomeIcon.Users);
userPairText = _pair.UserData.AliasOrUID + " is online";
@@ -274,7 +274,7 @@ public class DrawUserPair
{
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudYellow);
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
string userWarningText = "WARNING: This user exceeds one or more of your defined thresholds:" + UiSharedService.TooltipSeparator;
bool shownVram = false;
@@ -295,6 +295,31 @@ public class DrawUserPair
}
ImGui.SameLine();
if (_pair.UserData.IsAdmin || _pair.UserData.IsModerator)
{
ImGui.SameLine();
var iconId = _pair.UserData.IsAdmin ? 67 : 68;
var colorKey = _pair.UserData.IsAdmin ? "LightlessAdminText" : "LightlessModeratorText";
var roleColor = UIColors.Get(colorKey);
var iconPos = ImGui.GetCursorScreenPos();
SeStringUtils.RenderIconWithHitbox(iconId, iconPos);
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
using (ImRaii.PushColor(ImGuiCol.Text, roleColor))
{
ImGui.TextUnformatted(_pair.UserData.IsAdmin
? "Official Lightless Admin"
: "Official Lightless Moderator");
}
ImGui.EndTooltip();
}
}
}
private void DrawName(float leftSide, float rightSide)
@@ -376,7 +401,7 @@ public class DrawUserPair
currentRightSide -= (_uiSharedService.GetIconSize(individualIcon).X + spacingX);
ImGui.SameLine(currentRightSide);
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled))
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"), individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled))
_uiSharedService.IconText(individualIcon);
if (ImGui.IsItemHovered())
{

View File

@@ -0,0 +1,79 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.UI.Handlers;
using System.Numerics;
namespace LightlessSync.UI.Components;
public class RenamePairTagUi
{
private readonly TagHandler _tagHandler;
private readonly UiSharedService _uiSharedService;
private string _desiredName = string.Empty;
private bool _opened = false;
private bool _show = false;
private string _tag = string.Empty;
public RenamePairTagUi(TagHandler tagHandler, UiSharedService uiSharedService)
{
_tagHandler = tagHandler;
_uiSharedService = uiSharedService;
}
public void Draw()
{
var workHeight = ImGui.GetMainViewport().WorkSize.Y / ImGuiHelpers.GlobalScale;
var minSize = new Vector2(300, workHeight < 110 ? workHeight : 110) * ImGuiHelpers.GlobalScale;
var maxSize = new Vector2(300, 110) * ImGuiHelpers.GlobalScale;
var popupName = $"Renaming Pair Group {_tag}";
if (!_show)
{
_opened = false;
}
if (_show && !_opened)
{
ImGui.SetNextWindowSize(minSize);
UiSharedService.CenterNextWindow(minSize.X, minSize.Y, ImGuiCond.Always);
ImGui.OpenPopup(popupName);
_opened = true;
}
ImGui.SetNextWindowSizeConstraints(minSize, maxSize);
if (ImGui.BeginPopupModal(popupName, ref _show, ImGuiWindowFlags.Popup | ImGuiWindowFlags.Modal))
{
ImGui.TextUnformatted($"Renaming {_tag}");
ImGui.InputTextWithHint("##desiredname", "Enter new group name", ref _desiredName, 20, ImGuiInputTextFlags.None);
using (ImRaii.Disabled(string.IsNullOrEmpty(_desiredName)))
{
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, "Rename Group"))
{
RenameTag(_tag, _desiredName);
_show = false;
}
}
ImGui.EndPopup();
}
else
{
_show = false;
}
}
public void Open(string tag)
{
_tag = tag;
_desiredName = "";
_show = true;
}
public void RenameTag(string oldTag, string newTag)
{
_tagHandler.RenamePairTag(oldTag, newTag);
}
}

View File

@@ -0,0 +1,79 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.UI.Handlers;
using System.Numerics;
namespace LightlessSync.UI.Components;
public class RenameSyncshellTagUi
{
private readonly TagHandler _tagHandler;
private readonly UiSharedService _uiSharedService;
private string _desiredName = string.Empty;
private bool _opened = false;
private bool _show = false;
private string _tag = string.Empty;
public RenameSyncshellTagUi(TagHandler tagHandler, UiSharedService uiSharedService)
{
_tagHandler = tagHandler;
_uiSharedService = uiSharedService;
}
public void Draw()
{
var workHeight = ImGui.GetMainViewport().WorkSize.Y / ImGuiHelpers.GlobalScale;
var minSize = new Vector2(300, workHeight < 110 ? workHeight : 110) * ImGuiHelpers.GlobalScale;
var maxSize = new Vector2(300, 110) * ImGuiHelpers.GlobalScale;
var popupName = $"Renaming Syncshell Group {_tag}";
if (!_show)
{
_opened = false;
}
if (_show && !_opened)
{
ImGui.SetNextWindowSize(minSize);
UiSharedService.CenterNextWindow(minSize.X, minSize.Y, ImGuiCond.Always);
ImGui.OpenPopup(popupName);
_opened = true;
}
ImGui.SetNextWindowSizeConstraints(minSize, maxSize);
if (ImGui.BeginPopupModal(popupName, ref _show, ImGuiWindowFlags.Popup | ImGuiWindowFlags.Modal))
{
ImGui.TextUnformatted($"Renaming {_tag}");
ImGui.InputTextWithHint("##desiredname", "Enter new group name", ref _desiredName, 20, ImGuiInputTextFlags.None);
using (ImRaii.Disabled(string.IsNullOrEmpty(_desiredName)))
{
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, "Rename Group"))
{
RenameTag(_tag, _desiredName);
_show = false;
}
}
ImGui.EndPopup();
}
else
{
_show = false;
}
}
public void Open(string tag)
{
_tag = tag;
_desiredName = "";
_show = true;
}
public void RenameTag(string oldTag, string newTag)
{
_tagHandler.RenameSyncshellTag(oldTag, newTag);
}
}

View File

@@ -60,7 +60,7 @@ public class SelectPairForTagUi
{
if (isInGroup)
{
_tagHandler.AddTagToPairedUid(item.UserData.UID, _tag);
_tagHandler.AddPairTagToPairedUid(item.UserData.UID, _tag);
_peopleInGroup.Add(item.UserData.UID);
}
else

View File

@@ -0,0 +1,86 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using LightlessSync.API.Dto.Group;
using LightlessSync.UI.Handlers;
using System.Numerics;
namespace LightlessSync.UI.Components;
public class SelectSyncshellForTagUi
{
private readonly TagHandler _tagHandler;
private string _filter = string.Empty;
private bool _opened = false;
private HashSet<string> _syncshellsInGroup = new(StringComparer.Ordinal);
private bool _show = false;
private string _tag = string.Empty;
public SelectSyncshellForTagUi(TagHandler tagHandler)
{
_tagHandler = tagHandler;
}
public void Draw(List<GroupFullInfoDto> groups)
{
var workHeight = ImGui.GetMainViewport().WorkSize.Y / ImGuiHelpers.GlobalScale;
var minSize = new Vector2(300, workHeight < 400 ? workHeight : 400) * ImGuiHelpers.GlobalScale;
var maxSize = new Vector2(300, 1000) * ImGuiHelpers.GlobalScale;
var popupName = $"Choose Syncshells for Group {_tag}";
if (!_show)
{
_opened = false;
}
if (_show && !_opened)
{
ImGui.SetNextWindowSize(minSize);
UiSharedService.CenterNextWindow(minSize.X, minSize.Y, ImGuiCond.Always);
ImGui.OpenPopup(popupName);
_opened = true;
}
ImGui.SetNextWindowSizeConstraints(minSize, maxSize);
if (ImGui.BeginPopupModal(popupName, ref _show, ImGuiWindowFlags.Popup | ImGuiWindowFlags.Modal))
{
ImGui.TextUnformatted($"Select syncshells for group {_tag}");
ImGui.InputTextWithHint("##filter", "Filter", ref _filter, 255, ImGuiInputTextFlags.None);
foreach (var group in groups
.Where(g => string.IsNullOrEmpty(_filter) || g.GID.Contains(_filter, StringComparison.OrdinalIgnoreCase))
.OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)
.ToList())
{
var isInGroup = _syncshellsInGroup.Contains(group.GID);
if (ImGui.Checkbox(group.GroupAliasOrGID, ref isInGroup))
{
if (isInGroup)
{
_tagHandler.AddTagToSyncshell(group.GID, _tag);
_syncshellsInGroup.Add(group.GID);
}
else
{
_tagHandler.RemoveTagFromSyncshell(group.GID, _tag);
_syncshellsInGroup.Remove(group.GID);
}
}
}
ImGui.EndPopup();
}
else
{
_filter = string.Empty;
_show = false;
}
}
public void Open(string tag)
{
_syncshellsInGroup = _tagHandler.GetOtherSyncshellsForTag(tag);
_tag = tag;
_show = true;
}
}

View File

@@ -59,7 +59,7 @@ public class SelectTagForPairUi
if (ImGui.BeginPopup(popupName))
{
var tags = _tagHandler.GetAllTagsSorted();
var tags = _tagHandler.GetAllPairTagsSorted();
var childHeight = tags.Count != 0 ? tags.Count * 25 : 1;
var childSize = new Vector2(0, childHeight > 100 ? 100 : childHeight) * ImGuiHelpers.GlobalScale;
@@ -80,7 +80,7 @@ public class SelectTagForPairUi
HandleAddTag();
}
ImGui.SameLine();
ImGui.InputTextWithHint("##category_name", "New Group", ref _tagNameToAdd, 40);
ImGui.InputTextWithHint("##category_name", "New Group", ref _tagNameToAdd, 20);
if (ImGui.IsKeyDown(ImGuiKey.Enter))
{
HandleAddTag();
@@ -101,13 +101,13 @@ public class SelectTagForPairUi
private void DrawGroupName(Pair pair, string name)
{
var hasTagBefore = _tagHandler.HasTag(pair.UserData.UID, name);
var hasTagBefore = _tagHandler.HasPairTag(pair.UserData.UID, name);
var hasTag = hasTagBefore;
if (ImGui.Checkbox(name, ref hasTag))
{
if (hasTag)
{
_tagHandler.AddTagToPairedUid(pair.UserData.UID, name);
_tagHandler.AddPairTagToPairedUid(pair.UserData.UID, name);
}
else
{
@@ -120,10 +120,10 @@ public class SelectTagForPairUi
{
if (!_tagNameToAdd.IsNullOrWhitespace() && _tagNameToAdd is not (TagHandler.CustomOfflineTag or TagHandler.CustomOnlineTag or TagHandler.CustomVisibleTag))
{
_tagHandler.AddTag(_tagNameToAdd);
_tagHandler.AddPairTag(_tagNameToAdd);
if (_pair != null)
{
_tagHandler.AddTagToPairedUid(_pair.UserData.UID, _tagNameToAdd);
_tagHandler.AddPairTagToPairedUid(_pair.UserData.UID, _tagNameToAdd);
}
_tagNameToAdd = string.Empty;
}

View File

@@ -0,0 +1,132 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using LightlessSync.API.Dto.Group;
using LightlessSync.UI.Handlers;
using System.Numerics;
namespace LightlessSync.UI.Components;
public class SelectTagForSyncshellUi
{
private readonly TagHandler _tagHandler;
private readonly UiSharedService _uiSharedService;
/// <summary>
/// The group UI is always open for a specific pair. This defines which pair the UI is open for.
/// </summary>
/// <returns></returns>
private GroupFullInfoDto? _group;
/// <summary>
/// Should the panel show, yes/no
/// </summary>
private bool _show;
/// <summary>
/// For the add category option, this stores the currently typed in tag name
/// </summary>
private string _tagNameToAdd = "";
public SelectTagForSyncshellUi(TagHandler tagHandler, UiSharedService uiSharedService)
{
_show = false;
_group = null;
_tagHandler = tagHandler;
_uiSharedService = uiSharedService;
}
public void Draw()
{
if (_group == null)
{
return;
}
var name = _group.GroupAliasOrGID;
var popupName = $"Choose Groups for {_group.GroupAliasOrGID}";
// Is the popup supposed to show but did not open yet? Open it
if (_show)
{
ImGui.OpenPopup(popupName);
_show = false;
}
if (ImGui.BeginPopup(popupName))
{
var tags = _tagHandler.GetAllSyncshellTagsSorted();
var childHeight = tags.Count != 0 ? tags.Count * 25 : 1;
var childSize = new Vector2(0, childHeight > 100 ? 100 : childHeight) * ImGuiHelpers.GlobalScale;
ImGui.TextUnformatted($"Select the groups you want {name} to be in.");
if (ImGui.BeginChild(name + "##listGroups", childSize))
{
foreach (var tag in tags)
{
using (ImRaii.PushId($"groups-syncshell-{_group.GID}-{tag}")) DrawGroupName(_group, tag);
}
ImGui.EndChild();
}
ImGui.Separator();
ImGui.TextUnformatted($"Create a new group for {name}.");
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
{
HandleAddTag();
}
ImGui.SameLine();
ImGui.InputTextWithHint("##category_name", "New Group", ref _tagNameToAdd, 20);
if (ImGui.IsKeyDown(ImGuiKey.Enter))
{
HandleAddTag();
}
ImGui.EndPopup();
}
}
public void Open(GroupFullInfoDto group)
{
_group = group;
// Using "_show" here to de-couple the opening of the popup
// The popup name is derived from the name the user currently sees, which is
// based on the showUidForEntry dictionary.
// We'd have to derive the name here to open it popup modal here, when the Open() is called
_show = true;
}
private void DrawGroupName(GroupFullInfoDto group, string name)
{
var hasTag = _tagHandler.HasSyncshellTag(group.GID, name);
if (ImGui.Checkbox(name, ref hasTag))
{
if (hasTag)
{
_tagHandler.AddTagToSyncshell(group.GID, name);
}
else
{
_tagHandler.RemoveTagFromSyncshell(group.GID, name);
}
}
}
private void HandleAddTag()
{
if (!_tagNameToAdd.IsNullOrWhitespace())
{
_tagHandler.AddSyncshellTag(_tagNameToAdd);
if (_group != null)
{
_tagHandler.AddTagToSyncshell(_group.GID, _tagNameToAdd);
}
_tagNameToAdd = string.Empty;
}
else
{
_tagNameToAdd = string.Empty;
}
}
}

View File

@@ -371,7 +371,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Important Note: If you need to fix an animation that should apply across multiple jobs, you need to repeat this process with at least one additional job, " +
"otherwise the animation will only be fixed for the currently active job. This goes primarily for emotes that are used across multiple jobs.",
ImGuiColors.DalamudYellow, 800);
UIColors.Get("LightlessYellow"), 800);
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("WARNING: WHILE RECORDING TRANSIENT DATA, DO NOT CHANGE YOUR APPEARANCE, ENABLED MODS OR ANYTHING. JUST DO THE ANIMATION(S) OR WHATEVER YOU NEED DOING AND STOP THE RECORDING.",
ImGuiColors.DalamudRed, 800);
@@ -399,7 +399,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
if (_transientResourceManager.IsTransientRecording)
{
ImGui.SameLine();
UiSharedService.ColorText($"RECORDING - Time Remaining: {_transientResourceManager.RecordTimeRemaining.Value}", ImGuiColors.DalamudYellow);
UiSharedService.ColorText($"RECORDING - Time Remaining: {_transientResourceManager.RecordTimeRemaining.Value}", UIColors.Get("LightlessYellow"));
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("DO NOT CHANGE YOUR APPEARANCE OR MODS WHILE RECORDING, YOU CAN ACCIDENTALLY MAKE SOME OF YOUR APPEARANCE RELATED MODS PERMANENT.", ImGuiColors.DalamudRed, 800);
}
@@ -422,7 +422,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
if (_transientResourceManager.RecordedTransients.Any(k => !k.AlreadyTransient))
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Please review the recorded mod files before saving and deselect files that got into the recording on accident.", ImGuiColors.DalamudYellow);
UiSharedService.DrawGroupedCenteredColorText("Please review the recorded mod files before saving and deselect files that got into the recording on accident.", UIColors.Get("LightlessYellow"));
ImGuiHelpers.ScaledDummy(5);
}
@@ -485,7 +485,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
if (isAnalyzing)
{
UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}",
ImGuiColors.DalamudYellow);
UIColors.Get("LightlessYellow"));
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis"))
{
_characterAnalyzer.CancelAnalyze();
@@ -496,7 +496,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
if (_cachedAnalysis!.Any(c => c.Value.Any(f => !f.Value.IsComputed)))
{
UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data",
ImGuiColors.DalamudYellow);
UIColors.Get("LightlessYellow"));
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)"))
{
_ = _characterAnalyzer.ComputeAnalysis(print: false);
@@ -547,73 +547,147 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString());
if (tab.Success)
{
var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal)
.OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal).OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
ImGui.TextUnformatted("Files for " + kvp.Key);
ImGui.SameLine();
ImGui.TextUnformatted(kvp.Value.Count.ToString());
ImGui.SameLine();
ImGui.PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(1f, 1f));
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1f, 1f));
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
if (ImGui.BeginTable($"##fileStats_{kvp.Key}", 3,
ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingFixedFit))
{
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
}
if (ImGui.IsItemHovered())
{
string text = "";
text = string.Join(Environment.NewLine, groupedfiles
.Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))
+ ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))));
ImGui.SetTooltip(text);
}
ImGui.TextUnformatted($"{kvp.Key} size (actual):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize)));
ImGui.TextUnformatted($"{kvp.Key} size (compressed for up/download only):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize)));
ImGui.Separator();
var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
if (vramUsage != null)
{
var actualVramUsage = vramUsage.Sum(f => f.OriginalSize);
ImGui.TextUnformatted($"{kvp.Key} VRAM usage:");
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"Files for {kvp.Key}");
ImGui.TableNextColumn();
ImGui.TextUnformatted(kvp.Value.Count.ToString());
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(actualVramUsage));
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
if (ImGui.IsItemHovered())
{
string text = string.Join(Environment.NewLine, groupedfiles.Select(f =>
$"{f.Key}: {f.Count()} files, size: {UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))}, compressed: {UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))}"));
ImGui.SetTooltip(text);
}
ImGui.TableNextColumn();
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{kvp.Key} size (actual):");
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize)));
ImGui.TableNextColumn();
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{kvp.Key} size (compressed for up/download only):");
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize)));
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TableNextColumn();
var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
if (vramUsage != null)
{
var actualVramUsage = vramUsage.Sum(f => f.OriginalSize);
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{kvp.Key} VRAM usage:");
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(actualVramUsage));
ImGui.TableNextColumn();
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
{
var currentVramWarning = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB;
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted("Configured VRAM threshold:");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{currentVramWarning} MiB.");
ImGui.TableNextColumn();
if (currentVramWarning * 1024 * 1024 < actualVramUsage)
{
UiSharedService.ColorText(
$"You exceed your own threshold by {UiSharedService.ByteToString(actualVramUsage - (currentVramWarning * 1024 * 1024))}",
UIColors.Get("LightlessYellow"));
}
}
}
var actualTriCount = kvp.Value.Sum(f => f.Value.Triangles);
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{kvp.Key} modded model triangles:");
ImGui.TableNextColumn();
ImGui.TextUnformatted(actualTriCount.ToString());
ImGui.TableNextColumn();
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
{
using var _ = ImRaii.PushIndent(10f);
var currentVramWarning = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB;
ImGui.TextUnformatted($"Configured VRAM warning threshold: {currentVramWarning} MiB.");
if (currentVramWarning * 1024 * 1024 < actualVramUsage)
var currentTriWarning = _playerPerformanceConfig.Current.TrisWarningThresholdThousands;
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted("Configured triangle threshold:");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{currentTriWarning * 1000} triangles.");
ImGui.TableNextColumn();
if (currentTriWarning * 1000 < actualTriCount)
{
UiSharedService.ColorText($"You exceed your own threshold by " +
$"{UiSharedService.ByteToString(actualVramUsage - (currentVramWarning * 1024 * 1024))}.",
ImGuiColors.DalamudYellow);
UiSharedService.ColorText(
$"You exceed your own threshold by {actualTriCount - (currentTriWarning * 1000)}",
UIColors.Get("LightlessYellow"));
}
}
ImGui.EndTable();
}
var actualTriCount = kvp.Value.Sum(f => f.Value.Triangles);
ImGui.TextUnformatted($"{kvp.Key} modded model triangles: {actualTriCount}");
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
ImGui.PopStyleVar(2);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
_uiSharedService.MediumText("Selected file:", UIColors.Get("LightlessBlue"));
ImGui.SameLine();
_uiSharedService.MediumText(_selectedHash, UIColors.Get("LightlessYellow"));
if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item))
{
using var _ = ImRaii.PushIndent(10f);
var currentTriWarning = _playerPerformanceConfig.Current.TrisWarningThresholdThousands;
ImGui.TextUnformatted($"Configured triangle warning threshold: {currentTriWarning * 1000} triangles.");
if (currentTriWarning * 1000 < actualTriCount)
var filePaths = item.FilePaths;
UiSharedService.ColorText("Local file path:", UIColors.Get("LightlessBlue"));
ImGui.SameLine();
UiSharedService.TextWrapped(filePaths[0]);
if (filePaths.Count > 1)
{
UiSharedService.ColorText($"You exceed your own threshold by " +
$"{actualTriCount - (currentTriWarning * 1000)} triangles.",
ImGuiColors.DalamudYellow);
ImGui.SameLine();
ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1)));
}
var gamepaths = item.GamePaths;
UiSharedService.ColorText("Used by game path:", UIColors.Get("LightlessBlue"));
ImGui.SameLine();
UiSharedService.TextWrapped(gamepaths[0]);
if (gamepaths.Count > 1)
{
ImGui.SameLine();
ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1)));
}
}
ImGui.Separator();
if (_selectedObjectTab != kvp.Key)
{
_selectedHash = string.Empty;
@@ -629,7 +703,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
{
string fileGroupText = fileGroup.Key + " [" + fileGroup.Count() + "]";
var requiresCompute = fileGroup.Any(k => !k.IsComputed);
using var tabcol = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.Color(ImGuiColors.DalamudYellow), requiresCompute);
using var tabcol = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.Color(UIColors.Get("LightlessYellow")), requiresCompute);
if (requiresCompute)
{
fileGroupText += " (!)";
@@ -668,7 +742,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.Checkbox("Enable BC7 Conversion Mode", ref _enableBc7ConversionMode);
if (_enableBc7ConversionMode)
{
UiSharedService.ColorText("WARNING BC7 CONVERSION:", ImGuiColors.DalamudYellow);
UiSharedService.ColorText("WARNING BC7 CONVERSION:", UIColors.Get("LightlessYellow"));
ImGui.SameLine();
UiSharedService.ColorText("Converting textures to BC7 is irreversible!", ImGuiColors.DalamudRed);
UiSharedService.ColorTextWrapped("- Converting textures to BC7 will reduce their size (compressed and uncompressed) drastically. It is recommended to be used for large (4k+) textures." +
@@ -676,7 +750,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
Environment.NewLine + "- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues." +
Environment.NewLine + "- Conversion will convert all found texture duplicates (entries with more than 1 file path) automatically." +
Environment.NewLine + "- Converting textures to BC7 is a very expensive operation and, depending on the amount of textures to convert, will take a while to complete."
, ImGuiColors.DalamudYellow);
, UIColors.Get("LightlessYellow"));
if (_texturesToConvert.Count > 0 && _uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start conversion of " + _texturesToConvert.Count + " texture(s)"))
{
_conversionCancellationTokenSource = _conversionCancellationTokenSource.CancelRecreate();
@@ -692,41 +766,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
}
}
}
ImGui.Separator();
ImGui.TextUnformatted("Selected file:");
ImGui.SameLine();
UiSharedService.ColorText(_selectedHash, ImGuiColors.DalamudYellow);
if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item))
{
var filePaths = item.FilePaths;
ImGui.TextUnformatted("Local file path:");
ImGui.SameLine();
UiSharedService.TextWrapped(filePaths[0]);
if (filePaths.Count > 1)
{
ImGui.SameLine();
ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1)));
}
var gamepaths = item.GamePaths;
ImGui.TextUnformatted("Used by game path:");
ImGui.SameLine();
UiSharedService.TextWrapped(gamepaths[0]);
if (gamepaths.Count > 1)
{
ImGui.SameLine();
ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1)));
}
}
}
public override void OnOpen()
@@ -823,8 +862,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
}
if (string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal))
{
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(ImGuiColors.DalamudYellow));
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(ImGuiColors.DalamudYellow));
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(UIColors.Get("LightlessYellow")));
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(UIColors.Get("LightlessYellow")));
}
ImGui.TextUnformatted(item.Hash);
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
@@ -855,7 +894,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
}
var filePath = item.FilePaths[0];
bool toConvert = _texturesToConvert.ContainsKey(filePath);
if (ImGui.Checkbox("###convert" + item.Hash, ref toConvert))
if (UiSharedService.CheckboxWithBorder("###convert" + item.Hash, ref toConvert, UIColors.Get("LightlessPurple"), 1.5f))
{
if (toConvert && !_texturesToConvert.ContainsKey(filePath))
{

View File

@@ -169,7 +169,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
var dlProgressPercent = transferredBytes / (double)totalBytes;
drawList.AddRectFilled(dlBarStart,
dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) },
UiSharedService.Color(50, 205, 50, transparency), 1);
UiSharedService.Color(UIColors.Get("LightlessPurple")));
if (_configService.Current.TransferBarsShowText)
{

View File

@@ -23,19 +23,25 @@ public class DrawEntityFactory
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly CharaDataManager _charaDataManager;
private readonly SelectTagForPairUi _selectTagForPairUi;
private readonly RenamePairTagUi _renamePairTagUi;
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
private readonly RenameSyncshellTagUi _renameSyncshellTagUi;
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
private readonly TagHandler _tagHandler;
private readonly IdDisplayHandler _uidDisplayHandler;
public DrawEntityFactory(ILogger<DrawEntityFactory> logger, ApiController apiController, IdDisplayHandler uidDisplayHandler,
SelectTagForPairUi selectTagForPairUi, LightlessMediator mediator,
SelectTagForPairUi selectTagForPairUi, RenamePairTagUi renamePairTagUi, LightlessMediator mediator,
TagHandler tagHandler, SelectPairForTagUi selectPairForTagUi,
ServerConfigurationManager serverConfigurationManager, UiSharedService uiSharedService,
PlayerPerformanceConfigService playerPerformanceConfigService, CharaDataManager charaDataManager)
PlayerPerformanceConfigService playerPerformanceConfigService, CharaDataManager charaDataManager,
SelectTagForSyncshellUi selectTagForSyncshellUi, RenameSyncshellTagUi renameSyncshellTagUi, SelectSyncshellForTagUi selectSyncshellForTagUi)
{
_logger = logger;
_apiController = apiController;
_uidDisplayHandler = uidDisplayHandler;
_selectTagForPairUi = selectTagForPairUi;
_renamePairTagUi = renamePairTagUi;
_mediator = mediator;
_tagHandler = tagHandler;
_selectPairForTagUi = selectPairForTagUi;
@@ -43,6 +49,9 @@ public class DrawEntityFactory
_uiSharedService = uiSharedService;
_playerPerformanceConfigService = playerPerformanceConfigService;
_charaDataManager = charaDataManager;
_selectTagForSyncshellUi = selectTagForSyncshellUi;
_renameSyncshellTagUi = renameSyncshellTagUi;
_selectSyncshellForTagUi = selectSyncshellForTagUi;
}
public DrawFolderGroup CreateDrawGroupFolder(GroupFullInfoDto groupFullInfoDto,
@@ -51,15 +60,24 @@ public class DrawEntityFactory
{
return new DrawFolderGroup(groupFullInfoDto.Group.GID, groupFullInfoDto, _apiController,
filteredPairs.Select(p => CreateDrawPair(groupFullInfoDto.Group.GID + p.Key.UserData.UID, p.Key, p.Value, groupFullInfoDto)).ToImmutableList(),
allPairs, _tagHandler, _uidDisplayHandler, _mediator, _uiSharedService);
allPairs, _tagHandler, _uidDisplayHandler, _mediator, _uiSharedService, _selectTagForSyncshellUi);
}
public DrawFolderGroup CreateDrawGroupFolder(string id, GroupFullInfoDto groupFullInfoDto,
Dictionary<Pair, List<GroupFullInfoDto>> filteredPairs,
IImmutableList<Pair> allPairs)
{
return new DrawFolderGroup(id, groupFullInfoDto, _apiController,
filteredPairs.Select(p => CreateDrawPair(groupFullInfoDto.Group.GID + p.Key.UserData.UID, p.Key, p.Value, groupFullInfoDto)).ToImmutableList(),
allPairs, _tagHandler, _uidDisplayHandler, _mediator, _uiSharedService, _selectTagForSyncshellUi);
}
public DrawFolderTag CreateDrawTagFolder(string tag,
Dictionary<Pair, List<GroupFullInfoDto>> filteredPairs,
IImmutableList<Pair> allPairs)
{
return new(tag, filteredPairs.Select(u => CreateDrawPair(tag, u.Key, u.Value, null)).ToImmutableList(),
allPairs, _tagHandler, _apiController, _selectPairForTagUi, _uiSharedService);
return new(tag, filteredPairs.Select(u => CreateDrawPair(tag, u.Key, u.Value, currentGroup: null)).ToImmutableList(),
allPairs, _tagHandler, _apiController, _selectPairForTagUi, _renamePairTagUi, _uiSharedService);
}
public DrawUserPair CreateDrawPair(string id, Pair user, List<GroupFullInfoDto> groups, GroupFullInfoDto? currentGroup)

View File

@@ -6,7 +6,9 @@ using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.WebAPI;
using LightlessSync.WebAPI.SignalR.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices;
@@ -16,6 +18,7 @@ namespace LightlessSync.UI;
public sealed class DtrEntry : IDisposable, IHostedService
{
private readonly ApiController _apiController;
private readonly ServerConfigurationManager _serverManager;
private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly ConfigurationServiceBase<LightlessConfig> _configService;
private readonly IDtrBar _dtrBar;
@@ -28,7 +31,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
private string? _tooltip;
private Colors _colors;
public DtrEntry(ILogger<DtrEntry> logger, IDtrBar dtrBar, ConfigurationServiceBase<LightlessConfig> configService, LightlessMediator lightlessMediator, PairManager pairManager, ApiController apiController)
public DtrEntry(ILogger<DtrEntry> logger, IDtrBar dtrBar, ConfigurationServiceBase<LightlessConfig> configService, LightlessMediator lightlessMediator, PairManager pairManager, ApiController apiController, ServerConfigurationManager serverManager)
{
_logger = logger;
_dtrBar = dtrBar;
@@ -37,6 +40,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
_lightlessMediator = lightlessMediator;
_pairManager = pairManager;
_apiController = apiController;
_serverManager = serverManager;
}
public void Dispose()
@@ -59,7 +63,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
public async Task StopAsync(CancellationToken cancellationToken)
{
_cancellationTokenSource.Cancel();
await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
try
{
await _runTask!.ConfigureAwait(false);
@@ -89,10 +93,43 @@ public sealed class DtrEntry : IDisposable, IHostedService
{
_logger.LogTrace("Creating new DtrBar entry");
var entry = _dtrBar.Get("Lightless Sync");
entry.OnClick = _ => _lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
entry.OnClick = interactionEvent => OnClickEvent(interactionEvent);
return entry;
}
private void OnClickEvent(DtrInteractionEvent interactionEvent)
{
if (interactionEvent.ClickType.Equals(MouseClickType.Left) && !interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
}
else if (interactionEvent.ClickType.Equals(MouseClickType.Left) && interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
}
if (interactionEvent.ClickType.Equals(MouseClickType.Right))
{
bool isConnectingOrConnected = _apiController.ServerState is ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting;
if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting))
{
if (isConnectingOrConnected && !_serverManager.CurrentServer.FullPause)
{
_serverManager.CurrentServer.FullPause = true;
_serverManager.Save();
}
else if (!isConnectingOrConnected && _serverManager.CurrentServer.FullPause)
{
_serverManager.CurrentServer.FullPause = false;
_serverManager.Save();
}
_ = _apiController.CreateConnectionsAsync();
}
}
}
private async Task RunAsync()
{

View File

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

View File

@@ -110,7 +110,7 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
{
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
UiSharedService.ColorTextWrapped("New events are available, press refresh to update", ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped("New events are available, press refresh to update", UIColors.Get("LightlessYellow"));
}
var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.FolderOpen, "Open EventLog Folder");
@@ -180,7 +180,7 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
var iconColor = ev.EventSeverity switch
{
EventSeverity.Informational => new Vector4(),
EventSeverity.Warning => ImGuiColors.DalamudYellow,
EventSeverity.Warning => UIColors.Get("LightlessYellow"),
EventSeverity.Error => ImGuiColors.DalamudRed,
_ => new Vector4()
};

View File

@@ -1,11 +1,16 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Dto.Group;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Style;
using LightlessSync.Utils;
using System;
using System.Numerics;
namespace LightlessSync.UI.Handlers;
@@ -22,6 +27,9 @@ public class IdDisplayHandler
private bool _popupShown = false;
private DateTime? _popupTime;
private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f);
private float _highlightBoost;
public IdDisplayHandler(LightlessMediator mediator, ServerConfigurationManager serverManager, LightlessConfigService lightlessConfigService)
{
_mediator = mediator;
@@ -43,9 +51,9 @@ public class IdDisplayHandler
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
var prevState = textIsUid;
if (_showIdForEntry.ContainsKey(group.GID))
if (_showIdForEntry.TryGetValue(group.GID, out bool value))
{
prevState = _showIdForEntry[group.GID];
prevState = value;
}
_showIdForEntry[group.GID] = !prevState;
}
@@ -89,11 +97,108 @@ public class IdDisplayHandler
{
ImGui.SameLine(textPosX);
(bool textIsUid, string playerText) = GetPlayerText(pair);
if (!string.Equals(_editEntry, pair.UserData.UID, StringComparison.Ordinal))
{
ImGui.AlignTextToFramePadding();
using (ImRaii.PushFont(UiBuilder.MonoFont, textIsUid)) ImGui.TextUnformatted(playerText);
var font = textIsUid ? UiBuilder.MonoFont : ImGui.GetFont();
Vector4? textColor = null;
Vector4? glowColor = null;
if (pair.UserData.HasVanity)
{
if (!string.IsNullOrWhiteSpace(pair.UserData.TextColorHex))
{
textColor = UIColors.HexToRgba(pair.UserData.TextColorHex);
}
if (!string.IsNullOrWhiteSpace(pair.UserData.TextGlowColorHex))
{
glowColor = UIColors.HexToRgba(pair.UserData.TextGlowColorHex);
}
}
var useVanityColors = _lightlessConfigService.Current.useColoredUIDs && (textColor != null || glowColor != null);
var seString = useVanityColors
? SeStringUtils.BuildFormattedPlayerName(playerText, textColor, glowColor)
: SeStringUtils.BuildPlain(playerText);
var rowStart = ImGui.GetCursorScreenPos();
var drawList = ImGui.GetWindowDrawList();
bool useHighlight = false;
float highlightPadX = 0f;
float highlightPadY = 0f;
if (useVanityColors)
{
float boost = Luminance.ComputeHighlight(textColor, glowColor);
if (boost > 0f)
{
var style = ImGui.GetStyle();
useHighlight = true;
highlightPadX = MathF.Max(style.FramePadding.X * 0.6f, 2f * ImGuiHelpers.GlobalScale);
highlightPadY = MathF.Max(style.FramePadding.Y * 0.55f, 1.25f * ImGuiHelpers.GlobalScale);
drawList.ChannelsSplit(2);
drawList.ChannelsSetCurrent(1);
_highlightBoost = boost;
}
else
{
_highlightBoost = 0f;
}
}
Vector2 itemMin;
Vector2 itemMax;
Vector2 textSize;
using (ImRaii.PushFont(font, textIsUid))
{
SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font);
itemMin = ImGui.GetItemRectMin();
itemMax = ImGui.GetItemRectMax();
//textSize = itemMax - itemMin;
}
if (useHighlight)
{
var style = ImGui.GetStyle();
var frameHeight = ImGui.GetFrameHeight();
var rowTop = rowStart.Y - style.FramePadding.Y;
var rowBottom = rowTop + frameHeight;
var highlightMin = new Vector2(itemMin.X - highlightPadX, rowTop - highlightPadY);
var highlightMax = new Vector2(itemMax.X + highlightPadX, rowBottom + highlightPadY);
var windowPos = ImGui.GetWindowPos();
var contentMin = windowPos + ImGui.GetWindowContentRegionMin();
var contentMax = windowPos + ImGui.GetWindowContentRegionMax();
highlightMin.X = MathF.Max(highlightMin.X, contentMin.X);
highlightMax.X = MathF.Min(highlightMax.X, contentMax.X);
highlightMin.Y = MathF.Max(highlightMin.Y, contentMin.Y);
highlightMax.Y = MathF.Min(highlightMax.Y, contentMax.Y);
var highlightColor = new Vector4(
0.25f + _highlightBoost,
0.25f + _highlightBoost,
0.25f + _highlightBoost,
1f
);
highlightColor = Luminance.BackgroundContrast(textColor, glowColor, highlightColor, ref _currentBg);
float rounding = style.FrameRounding > 0f ? style.FrameRounding : 5f * ImGuiHelpers.GlobalScale;
drawList.ChannelsSetCurrent(0);
drawList.AddRectFilled(highlightMin, highlightMax, ImGui.GetColorU32(highlightColor), rounding);
var borderColor = style.Colors[(int)ImGuiCol.Border];
borderColor.W *= 0.25f;
drawList.AddRect(highlightMin, highlightMax, ImGui.GetColorU32(borderColor), rounding);
drawList.ChannelsMerge();
}
if (ImGui.IsItemHovered())
{
@@ -173,10 +278,12 @@ public class IdDisplayHandler
{
_editEntry = string.Empty;
}
UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel");
}
}
public (bool isGid, string text) GetGroupText(GroupFullInfoDto group)
{
var textIsGid = true;

View File

@@ -17,61 +17,146 @@ public class TagHandler
_serverConfigurationManager = serverConfigurationManager;
}
public void AddTag(string tag)
{
_serverConfigurationManager.AddTag(tag);
}
/// <summary>
/// Creation of an pair tag
/// </summary>
/// <param name="tag">Name of the tag</param>
public void AddPairTag(string tag) => _serverConfigurationManager.AddPairTag(tag);
public void AddTagToPairedUid(string uid, string tagName)
{
_serverConfigurationManager.AddTagForUid(uid, tagName);
}
/// <summary>
/// Creation of an syncshell tag
/// </summary>
/// <param name="tag">Name of the tag</param>
public void AddSyncshellTag(string tag) => _serverConfigurationManager.AddSyncshellTag(tag);
public List<string> GetAllTagsSorted()
{
return
[
/// <summary>
/// Add pair to tag
/// </summary>
/// <param name="uid">UID that will be added to tag/param>
/// <param name="tagName">Name of the tag</param>
public void AddPairTagToPairedUid(string uid, string tagName) => _serverConfigurationManager.AddTagForUid(uid, tagName);
/// <summary>
/// Add syncshell to tag
/// </summary>
/// <param name="name">Syncshell that will be added to tag/param>
/// <param name="tagName">Name of the tag</param>
public void AddTagToSyncshell(string name, string tagName) => _serverConfigurationManager.AddTagForSyncshell(name, tagName);
/// <summary>
/// Get all pair tags
/// </summary>
public List<string> GetAllPairTagsSorted() => [
.. _serverConfigurationManager.GetServerAvailablePairTags()
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase)
.Order(StringComparer.OrdinalIgnoreCase)
,
];
}
public HashSet<string> GetOtherUidsForTag(string tag)
{
return _serverConfigurationManager.GetUidsForTag(tag);
}
/// <summary>
/// Get all syncshell tags
/// </summary>
public List<string> GetAllSyncshellTagsSorted() => [
.. _serverConfigurationManager.GetServerAvailableSyncshellTags()
.Order(StringComparer.OrdinalIgnoreCase)
,
];
public bool HasAnyTag(string uid)
{
return _serverConfigurationManager.HasTags(uid);
}
/// <summary>
/// Get all UIDs bound to an given tag
/// </summary>
/// <param name="name">Name of the tag</param>
public HashSet<string> GetOtherUidsForTag(string tag) => _serverConfigurationManager.GetUidsForPairTag(tag);
public bool HasTag(string uid, string tagName)
{
return _serverConfigurationManager.ContainsTag(uid, tagName);
}
/// <summary>
/// Get all syncshells bound to an given tag
/// </summary>
/// <param name="name">Name of the tag</param>
public HashSet<string> GetOtherSyncshellsForTag(string tag) => _serverConfigurationManager.GetNamesForSyncshellTag(tag);
/// <summary>
/// Checking if the UID is connected to any tag
/// </summary>
/// <param name="uid">Syncshell that needs to be checked</param>
public bool HasAnyPairTag(string uid) => _serverConfigurationManager.HasPairTags(uid);
/// <summary>
/// Checking if the syncshell is connected to the tag
/// </summary>
/// <param name="name">Syncshell that needs to be checked</param>
public bool HasAnySyncshellTag(string name) => _serverConfigurationManager.HasSyncshellTags(name);
/// <summary>
/// Checking if the UID is connected to the tag
/// </summary>
/// <param name="uid">UID that needs to be checked</param>
/// <param name="tagName">Name of the tag</param>
public bool HasPairTag(string uid, string tagName) => _serverConfigurationManager.ContainsPairTag(uid, tagName);
/// <summary>
/// Checking if the syncshell is connected to the tag
/// </summary>
/// <param name="name">Syncshell that needs to be checked</param>
/// <param name="tagName">Name of the tag</param>
public bool HasSyncshellTag(string name, string tagName) => _serverConfigurationManager.ContainsSyncshellTag(name, tagName);
/// <summary>
/// Is this tag opened in the paired clients UI?
/// </summary>
/// <param name="tag">the tag</param>
/// <returns>open true/false</returns>
public bool IsTagOpen(string tag)
public bool IsTagOpen(string tag) => _serverConfigurationManager.ContainsOpenPairTag(tag);
/// <summary>
/// Removal of Pair Tags from Storage
/// </summary>
/// <param name="tag">Name of the tag</param>
public void RemovePairTag(string tag) => _serverConfigurationManager.RemovePairTag(tag);
/// <summary>
/// Removal of Syncshell Tags from Storage
/// </summary>
/// <param name="tag">Name of the tag</param>
public void RemoveSyncshellTag(string tag) => _serverConfigurationManager.RemoveSyncshellTag(tag);
/// <summary>
/// Removal of UID in a Tag
/// </summary>
/// <param name="uid">UID of user thats bound to the tag</param>
/// <param name="tagName">Name of the tag</param>
public void RemoveTagFromPairedUid(string uid, string tagName) => _serverConfigurationManager.RemoveTagForUid(uid, tagName);
/// <summary>
/// Removal of Syncshell in a Tag
/// </summary>
/// <param name="name">Syncshell thats bound to the tag</param>
/// <param name="tagName">Name of the tag</param>
public void RemoveTagFromSyncshell(string name, string tagName) => _serverConfigurationManager.RemoveTagForSyncshell(name, tagName);
/// <summary>
/// Rename of a pair tag
/// </summary>
/// <param name="oldName">Old pair tag name</param>
/// <param name="newName">New pair tag name</param>
public void RenamePairTag(string oldName, string newName)
{
return _serverConfigurationManager.ContainsOpenPairTag(tag);
_serverConfigurationManager.RenamePairTag(oldName, newName);
}
public void RemoveTag(string tag)
/// <summary>
/// Rename of a syncshell tag
/// </summary>
/// <param name="oldName">Old syncshell tag name</param>
/// <param name="newName">New syncshell tag name</param>
public void RenameSyncshellTag(string oldName, string newName)
{
_serverConfigurationManager.RemoveTag(tag);
}
public void RemoveTagFromPairedUid(string uid, string tagName)
{
_serverConfigurationManager.RemoveTagForUid(uid, tagName);
_serverConfigurationManager.RenameSyncshellTag(oldName, newName);
}
/// <summary>
/// Changes the tag to open/close
/// </summary>
/// <param name="tag">The Tag that will be modified</param>
/// <param name="open">True/False</param>
public void SetTagOpen(string tag, bool open)
{
if (open)

View File

@@ -4,9 +4,9 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using LightlessSync.FileCache;
using LightlessSync.Localization;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Localization;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
@@ -78,7 +78,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
UiSharedService.ColorTextWrapped("Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients " +
"might look broken because of this or others players mods might not apply on your end altogether. " +
"If you want to use this plugin you will have to move your mods to Penumbra.", ImGuiColors.DalamudYellow);
"If you want to use this plugin you will have to move your mods to Penumbra.", UIColors.Get("LightlessYellow"));
if (!_uiShared.DrawOtherPluginState()) return;
ImGui.Separator();
if (ImGui.Button("Next##toAgreement"))
@@ -167,13 +167,13 @@ public partial class IntroUi : WindowMediatorSubscriberBase
}
else
{
UiSharedService.TextWrapped("To not unnecessary download files already present on your computer, Lightless Sync will have to scan your Penumbra mod directory. " +
UiSharedService.TextWrapped("To not unnecessarily download files already present on your computer, Lightless Sync will have to scan your Penumbra mod directory. " +
"Additionally, a local storage folder must be set where Lightless Sync will download other character files to. " +
"Once the storage folder is set and the scan complete, this page will automatically forward to registration at a service.");
UiSharedService.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed.");
UiSharedService.ColorTextWrapped("Warning: once past this step you should not delete the FileCache.csv of Lightless Sync in the Plugin Configurations folder of Dalamud. " +
"Otherwise on the next launch a full re-scan of the file cache database will be initiated.", ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped("Warning: if the scan is hanging and does nothing for a long time, chances are high your Penumbra folder is not set up properly.", ImGuiColors.DalamudYellow);
"Otherwise on the next launch a full re-scan of the file cache database will be initiated.", UIColors.Get("LightlessYellow"));
UiSharedService.ColorTextWrapped("Warning: if the scan is hanging and does nothing for a long time, chances are high your Penumbra folder is not set up properly.", UIColors.Get("LightlessYellow"));
_uiShared.DrawCacheDirectorySetting();
}
@@ -197,7 +197,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
_configService.Save();
}
UiSharedService.ColorTextWrapped("The File Compactor can save a tremendeous amount of space on the hard disk for downloads through Lightless. It will incur a minor CPU penalty on download but can speed up " +
"loading of other characters. It is recommended to keep it enabled. You can change this setting later anytime in the Lightless settings.", ImGuiColors.DalamudYellow);
"loading of other characters. It is recommended to keep it enabled. You can change this setting later anytime in the Lightless settings.", UIColors.Get("LightlessYellow"));
}
}
else if (!_uiShared.ApiController.ServerAlive)
@@ -255,7 +255,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Strongly consider to use OAuth2 to authenticate, if the server supports it (the current main server does). " +
"The authentication flow is simpler and you do not require to store or maintain Secret Keys. " +
"You already implicitly register using Discord, so the OAuth2 method will be cleaner and more straight-forward to use.", ImGuiColors.DalamudYellow, 500);
"You already implicitly register using Discord, so the OAuth2 method will be cleaner and more straight-forward to use.", UIColors.Get("LightlessYellow"), 500);
ImGuiHelpers.ScaledDummy(5);
ImGui.AlignTextToFramePadding();
@@ -308,7 +308,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
}
else
{
UiSharedService.ColorTextWrapped($"OAuth2 is connected. Linked to: Discord User {_serverConfigurationManager.GetDiscordUserFromToken(selectedServer)}", ImGuiColors.HealerGreen);
UiSharedService.ColorTextWrapped($"OAuth2 is connected. Linked to: Discord User {_serverConfigurationManager.GetDiscordUserFromToken(selectedServer)}", UIColors.Get("LightlessBlue"));
UiSharedService.TextWrapped("Now press the update UIDs button to get a list of all of your UIDs on the server.");
_uiShared.DrawUpdateOAuthUIDsButton(selectedServer);
var playerName = _dalamudUtilService.GetPlayerName();

View File

@@ -25,7 +25,7 @@ internal class JoinSyncshellUI : WindowMediatorSubscriberBase
private string _syncshellPassword = string.Empty;
public JoinSyncshellUI(ILogger<JoinSyncshellUI> logger, LightlessMediator mediator,
UiSharedService uiSharedService, ApiController apiController, PerformanceCollectorService performanceCollectorService)
UiSharedService uiSharedService, ApiController apiController, PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Join existing Syncshell###LightlessSyncJoinSyncshell", performanceCollectorService)
{
_uiSharedService = uiSharedService;
@@ -63,7 +63,7 @@ internal class JoinSyncshellUI : WindowMediatorSubscriberBase
"Joining a Syncshell will pair you implicitly with all existing users in the Syncshell." + Environment.NewLine +
"All permissions to all users in the Syncshell will be set to the preferred Syncshell permissions on joining, excluding prior set preferred permissions.");
ImGui.Separator();
ImGui.TextUnformatted("Note: Syncshell ID and Password are case sensitive. MSS- is part of Syncshell IDs, unless using Vanity IDs.");
ImGui.TextUnformatted("Note: Syncshell ID and Password are case sensitive. LLS- is part of Syncshell IDs, unless using Vanity IDs.");
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Syncshell ID");
@@ -88,7 +88,7 @@ internal class JoinSyncshellUI : WindowMediatorSubscriberBase
UiSharedService.ColorTextWrapped("Failed to join the Syncshell. This is due to one of following reasons:" + Environment.NewLine +
"- The Syncshell does not exist or the password is incorrect" + Environment.NewLine +
"- You are already in that Syncshell or are banned from that Syncshell" + Environment.NewLine +
"- The Syncshell is at capacity or has invites disabled" + Environment.NewLine, ImGuiColors.DalamudYellow);
"- The Syncshell is at capacity or has invites disabled" + Environment.NewLine, UIColors.Get("LightlessYellow"));
}
}
else
@@ -111,7 +111,7 @@ internal class JoinSyncshellUI : WindowMediatorSubscriberBase
|| _groupJoinInfo.GroupPermissions.IsPreferDisableAnimations() != _ownPermissions.DisableGroupAnimations)
{
ImGuiHelpers.ScaledDummy(2f);
UiSharedService.ColorText("Your current preferred default Syncshell permissions deviate from the suggested permissions:", ImGuiColors.DalamudYellow);
UiSharedService.ColorText("Your current preferred default Syncshell permissions deviate from the suggested permissions:", UIColors.Get("LightlessYellow"));
if (_groupJoinInfo.GroupPermissions.IsPreferDisableSounds() != _ownPermissions.DisableGroupSounds)
{
ImGui.AlignTextToFramePadding();

View File

@@ -102,7 +102,7 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
{
_ownPermissions.SetDisableAnimations(disableAnimations);
}
_uiSharedService.DrawHelpText("Disabling sounds will remove all animations synced with this user on both sides." + UiSharedService.TooltipSeparator
_uiSharedService.DrawHelpText("Disabling animations will remove all animations synced with this user on both sides." + UiSharedService.TooltipSeparator
+ "Note: this is bidirectional, either user disabling animation sync will stop animation sync on both sides.");
using (ImRaii.PushIndent(indentSize, false))
{
@@ -116,7 +116,7 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
{
_ownPermissions.SetDisableVFX(disableVfx);
}
_uiSharedService.DrawHelpText("Disabling sounds will remove all VFX synced with this user on both sides." + UiSharedService.TooltipSeparator
_uiSharedService.DrawHelpText("Disabling VFX will remove all VFX synced with this user on both sides." + UiSharedService.TooltipSeparator
+ "Note: this is bidirectional, either user disabling VFX sync will stop VFX sync on both sides.");
using (ImRaii.PushIndent(indentSize, false))
{

Some files were not shown because too many files have changed in this diff Show More